说明
在大型前端项目中,我们一般会使用 webpack、Rollup 等工具进行模块整合,但是庞大的代码量会使得我们在开发阶段花费更多的时间在(代码改动 --> 页面渲染)这个阶段,即使使用 HMR 这个问题也没有完全的解决,项目代码量达到一定规模,积少成多,HMR 带来的效率提升会逐渐不够
vite 可以解决的问题
- 快速启动本地开发服务器
- 代码更新快速显示在浏览器页面中
- 在开发阶段 vite 会使用 esbuild ,同时 基于 原生 ES 模块实现 高效的 HMR
- 在生产阶段 vite 会使用 rollup 来打包代码,输出优化过的静态资源
1. 简单搭建 vite 项目
通过脚手架搭建项目
npm init vite@latest # npm
yarn create vite # yarn
然后在命令行中按照提示选择项目类型 例如:vue-ts react-ts
安装外城后可以得到如下目录结构
.
├── dist # build 后输出目录
├── index.html # 页面入口
├── package.json
├── src # 源文件
├── tsconfig.json
├── vite.config.ts # vite 配置文件
└── yarn.lock
浏览器中使用 es module
打开 index.html 可以看到与其他SPA 项目不同的是 script 标签
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
可以发现 script 标签的
type=module
这个设置可以让我们在浏览器中使用 es6 的 module 模块,浏览器会构建一个依赖关系图,借助浏览器原生的 ESM 能力完成模块的查找、解析、实例化到执行的过程。每次代码修改,都只做单文件的编译,时间复杂度低
内置命令
打开 package.json 可以看到,脚手架代码中给出了三个用于开发的脚本
{
"scripts": {
"dev": "vite", // 启动开发服务器
"build": "vite build", // 为生产环境构建产物
"serve": "vite preview" // 本地预览生产构建产物 需要先 build 再 预览
}
}
命令行运行
yarn dev
然后做一些代码修改,可以看到页面刷新非常快
查看浏览器 Network 接收到的 HMR 资源 ,可以看到,获得的资源是经过转码的 es6 代码,也就是说如果我们改动的是一个 react 组件,那么组件的jsx 语法会被编译成 ES6 的 JS 语法然后更新,但是这个组件会保留 es6 以及 es module 的语法。
常用功能实现
针对常规的项目开发以及项目打包场景,Vite 有比 Webpack 更贴心的开箱即用能力,很多我们在配置 Webpack 时比较复杂的配置,在 Vite 中都有集成,而且有着不错的性能,下边会列举一些前端构建会考虑的功能在 Vite 中是如何实现的
1. 支持使用 JSX 语法及 HMR 热模块加载
通常一个 react 项目都是使用 JSX 语法编写的, 这种语法不是 ECMA 标准的语法,想要在浏览器中正常使用,就需要编译一下,在 webpack 项目中我们可以借助 babel-loader 实现对 JSX 语法的解析
vite 中要使用 React 需要安装
yarn add -D @vitejs/plugin-react
, 然后做如下配置
const react = require('@vitejs/plugin-react');
return defineConfig({
plugins: [react()]
});
与 webpack 相比,大部分情况下不需要我们在进行 babel 的配置了,@vitejs/plugin-react 这个插件内部已经内置了一些基础的 babel 处理,如果有定制化需求可以按照如下配置实现
const react = require('@vitejs/plugin-react');
return defineConfig({
plugins: [react({
babel: {
presets: [...],
plugins: [...],
babelrc: true, // 是否直接使用外部的 .babelrc 配置文件
configFile: true, // 是否直接使用外部的 babel.config.js 配置文件
}
})]
});
Vite 内置了 HMR 支持,React 的 HMR 会在使用
@vitejs/plugin-react
的同时支持, 不需要我们再手动配置
2. Typescript 支持
与 webpack 项目一样,我们只需要创建一个 tsconfig.json 来配置 Typescript 检查规范即可,然后开发阶段,借助开发工具的类型检查提示;生产阶段,直接通过 tsc 来进行整理的类型检查
不同的是 Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。 – Vite – Typescript
此外 vite 中的 TypeScript 还需要做如下处理
- TS 编译器选项 compilerOptions.isolatedModules = true
- TS 编译器选项 compilerOptions.useDefineForClassFields = true
- TS 编译器选项 compilerOptions.types = [“vite/client”]
- 声明一个 vite-env.d.ts 文件
/// <reference types="vite/client" />
来增加一些 vite 的类型定义补充
3. 样式处理
- 不需要 webpack 中类似 css-loader 等加载器,vite 内置支持 css 的导入
import '../style/xxx.css';
类似 less sass 等 css 预处理器,也不需要下载类似 less-loader 等工具,vite 中提供内置支持,但是相应的预处理器还是要安装的
yarn add -D less
yarn add -D sass
- vite 中的默认支持 css-module,但是需要样式文件名具有一定规则例如:
users.module.css
users.module.less
等都是支持 css-module 的写法
// 组件中使用
import userStyle from './styles/users.module.css';
const User = () => (<div className={userStyle.container}>user</div>)
- vite 中的 样式文件在打包时默认会随着 chunk 拆分出小块,来实现一定程度的样式按需加载
- vite 内置支持 postcss , 不过需要我们在项目根目录创建一个 postcss.config.js 配置文件,来定义样式的兼容性改动
如果对如上的默认样式处理有变更的需求,可以通过改动 vite.config.js 中的 css 配置项来实现
return defineConfig({
css: {
modules: { // css-module 配置,最终会传给 postcss-modules 处理
scopeBehaviour: 'local'
},
postcss: {}, // 内联的 postcss 配置,功能等同 postcss.config.js
preprocessorOptions: { // css 预处理器的配置
less: {
javascriptEnabled: true,
globalVars: {
cdnUrl: JSON.stringify('https://www.baidu.com'),
},
}
},
build: {
cssCodeSplit: true, // 开启或者禁用,打包时的 css 代码拆分
}
}
});
4. 静态资源处理
vite 支持 静态资源通过 import 导入,同时支持设置一个 public 目录,目录中的静态资源在开发时,可以通过 / 根路径直接访问到,并且打包时会被完整复制到目标目录(比较适合,不需要编译、必须保持原文件名等资源)
import imgUrl from './img.png';
import imgUrl2 from '/img2.png'; // 相对于根文件夹的 绝对路径 /img2.png
// 样式
// .bgimg {
// width: 150px;
// height: 150px;
// background-image: url('./huawei.jpeg');
// }
const User = () => (
<div>
<img src={imgUrl2} alt="" />
{/* 如下访问的时 vite /public/img3.png */}
<img src="/img3.png" alt="" />
<img src={imgUrl} alt="" />
<div className={RootContainerLess.bgimg} />
</div>
)
可以通过更改 vite.config.js 配置来自定义静态资源处理
return defineConfig({
css: {
publicDir: 'public', // 可以是文件系统的绝对路径path.resolve(__dirname, 'public') 也可以是相对路径
assetsInclude: ['**/*.gltf'], // 图片等会被默认当做静态资源,这个选项可以增加更多的静态资源文件类型
build: {
assetsInlineLimit: 4096, // 静态资源大小小于这个数值的,会在打包时内联为 base64 ,减少http 请求
}
}
});
更多资源处理细节可以查看官方文档
5. 低版本浏览器兼容
Vite 的目标浏览器是指能够 支持原生 ESM script 标签 和 支持原生 ESM 动态导入 的,传统浏览器可以通过 @vitejs/plugin-legacy 插件来支持,但是经测试, 只有打包后的结果是支持低版本浏览器的,dev-server 阶段的代码还是只能在 支持原生 ESM 的浏览器中跑
const react = require('@vitejs/plugin-react');
const legacy = require('@vitejs/plugin-legacy');
return defineConfig({
plugins: [
react(),
legacy({
additionalLegacyPolyfills: ['regenerator-runtime/runtime'] // 给 IE11 提供异步支持
})
]
});
此外可以通过创建 .browserslistrc 文件来定义兼容到哪些版本浏览器,这样 postcss 也可以依据它来进行兼容
6. 给文件夹设置别名 alias
大型项目的目录结构复杂,为了方便文件调用,一般会通过 “别名” 的方式,与 webpack 一样, vite 中通过 vite.config.js 中设置 resolve.alias 来实现
return defineConfig({
resolve: { // 文件别名
alias: {
store: path.join(context, 'src/store')
},
},
});
// 业务代码中,可以按照如下方式引用
import globalStore, { Provider } from 'store';
7. 构建阶段传入变量
在 webpack 中我们可以借助 webpack.DefinePlugin 来向项目中注入一些环境变量,在 vite 中可以有两种方式来实现
- 借助 vite.config.js 中的配置项 define 来定义一些常量
// vite.config.js 中定义传入的常量
return defineConfig({
resolve: { // 文件别名
define: {
RUNTIME_CONST: JSON.stringify(’CONST 常量‘),
},
},
});
// d.ts 文件中定义这个常量类型
declare global {
const RUNTIME_NODE_ENV: string;
}
// 业务代码中使用
console.log('构建阶段传入的常量:', RUNTIME_CONST);
- 借助 .env 文件,Vite 使用 dotenv 从你的 环境目录 中的下列文件加载额外的环境变量 --更多查看
- .env # 所有情况下都会加载
- .env.local # 所有情况下都会加载,但会被 git 忽略
- .env.[mode] # 只在指定模式下加载
- .env.[mode].local # 只在指定模式下加载,但会被 git 忽略
此外 vite 内置了一些环境变量
- import.meta.env.MODE: {string} 应用运行的模式。
- import.meta.env.BASE_URL: {string} 部署应用时的基本 URL。他由base 配置项决定。
- import.meta.env.PROD: {boolean} 应用是否运行在生产环境。
- import.meta.env.DEV: {boolean} 应用是否运行在开发环境 (永远与 import.meta.env.PROD相反)。
.env 文件中定义的环境变量也会挂载到 import.meta.env 对象上
// vite.config.js 中定义 env 文件存放地址及允许解析成常量的前缀
return defineConfig({
envDir: path.join(context, 'env'), // 加载 .env 文件的目录 默认根文件夹
envPrefix: 'VITE_ENV_', // .env 文件中环境变量格式
});
// 创建 /env/.env 文件
// 内容:VITE_ENV_XXX=xxx-xxx-xxx # 只有 VITE_ENV_ 为前缀的常量才会挂载到 import.meta.env 对象下
// 配置 TS 类型 vite-env.d.ts 文件中 增加如下
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_ENV_XXX: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// 业务代码中使用注入的常量
console.log(import.meta.env.VITE_ENV_XXX);
8. 自动加载模块,而不必到处 import 模块
webpack 中提供 webpack.ProvidePlugin 插件来实现这个功能,在 vite 中并没有官方提供这个功能,要实现类似功能可以借助 @rollup/plugin-inject 这个插件来实现,因为 vite 内部通过 rollup 来打包资源,可以使用一部分的 rollup 插件
// vite.config.js 中使用插件
const inject = require('@rollup/plugin-inject');
return defineConfig({
plugins: [
inject({
exclude: /\.less/,
include: /\.[tj]sx?$/,
_: 'lodash', // 自动导入的模块
React: ['react', '*'],
}),
react(),
]
});
// 业务代码中即可不 import lodash mobx react 等
9. 给入口 HTML 传入变量
webpack 中通过 html-webpack-plugin 插件,来做HTML文件的创建及定制化,vite 中可以使用 vite-plugin-html 插件来实现同样的功能
// vite.config.js 中使用插件
const {injectHtml} = require('vite-plugin-html');
return defineConfig({
plugins: [
injectHtml({
data: {
MAIN_API_URL: 'https://www.baidu.com' // 测试 html 模板传参
},
}),
]
});
// index.html 文件中使用传参
<html lang="en">
<body>
<script>
var API_URL = '<%- MAIN_API_URL %>';
</script>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
10. 按需加载
vite 默认支持 Tree-shaking:tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export。
vite 支持通过 Tree-shaking 实现的按需加载,但是有些第三方库,还提供了 样式文件的片段,这个时候我们可以借助 vite-plugin-style-import 来实现样式文件的按需加载,下边以 antd 为例
// vite.config.js 中使用插件
const styleImporter = require('vite-plugin-style-import').default;
return defineConfig({
plugins: [
styleImporter({
libs: [
{
libraryName: 'antd',
esModule: true,
resolveStyle: name => `antd/es/${name}/style/index`
}
]
}),
react()
]
});
// 业务代码中使用
import { Button } from 'antd';
// 相当于
import { Button } from 'ant-design-vue';
import 'antd/es/button/style/index.js';
11. 代码分离 Code-splitting 及打包后资源规划
vite 支持 动态导入(dynamic imports) 的主动的代码分割,同时 vite 内部也会分割出一些共享chunk,并对其做一些引用优化 modulepreload 预加载等
// vite.config.js 中使用插件
const styleImporter = require('vite-plugin-style-import').default;
return defineConfig({
build: {
outDir: path.resolve(__dirname, 'build'), // 指定输出路径
assetsDir: 'static', // 一个相对于 outDir 的静态资源输出路径
cssCodeSplit: true, // 输出的 css 是否是经过 拆分的
sourcemap: true,
emptyOutDir: true, // 构建时清空目标文件夹
chunkSizeWarningLimit: 500, // 生成 chunk 大于这个数值会在控制台warning
rollupOptions: { // vite 内部使用 rollup 做打包
output: {
manualChunks: { // 自定义一些可以共享的 chunk, 默认 node_modules 下的 package 只拆出一个 vendor
basic: ['react', 'react-dom', 'react-router-dom'],
vendor: ['antd', 'axios', 'lodash', 'qs']
},
chunkFileNames: path.join('static', 'chunk/[name]-[hash].js'),
entryFileNames: path.join('static', 'js/[name]-[hash].js'),
assetFileNames: path.join('static', '[ext]/[name]-[hash].[ext]')
}
}
},
});
// 生成的资源结构
.
├── index.html
└── static
├── chunk
│ ├── basic-84f9de30.js
│ ├── index-0ea6d4a0.js
│ ├── index-97edd7a4.js
│ └── vendor-5e54d301.js
├── css
│ ├── index-0fbc2448.css
│ └── index-697ec425.css
├── jpeg
│ └── huawei-05681d0e.jpeg
└── js
└── index-820fccbf.js
12. 构建结果分析
webpack 中借助 webpack-bundle-analyzer 插件能够生成一个 资源打包结果的分析图,在 vite 中可以借助 rollup-plugin-visualizer 来实现(虽然生产的图表功能没有 ebpack-bundle-analyzer 的强大)
// vite.config.js 中使用插件
const visualizer = require('rollup-plugin-visualizer').default;
return defineConfig({
plugins: [
visualizer({
open: true,
template: 'treemap',
gzipSize: true,
brotliSize: true,
filename: `report/index.html`
})
]
});
13. eslint 接入
在 vite 项目中建议在 开发阶段借助代码开发工具来实现 eslint 报错提示,构建阶段直接跑一个
eslint --fix src/
如果一定想将 eslint 检查绑定在 vite 编译上,可以使用 vite-plugin-eslint 插件
const eslint = require('vite-plugin-eslint').default;
return defineConfig({
plugins: [
react(),
{
...eslint({
fix: false,
cache: true,
cacheLocation: path.join(__dirname, 'node_modules/.vite/eslint'),
formatter: 'stylish',
throwOnWarning: isDev,
}), enforce: 'pre' // 在 vite 核心插件执行前执行
},
]
});
14. 开发阶段相关设置
const pkg = require('../package.json');
return defineConfig({
optimizeDeps: {
include: Object.keys(pkg.dependencies)
},
server: {
port: parseInt(params['port'], 10) || 3000,
host: true,
https: params['https'],
proxy: {},
open: true,
fs: {strict: true},
hmr: {
overlay: false
}
},
});
2. 测试 vite 过程中遇到的问题
1. 执行 yarn start 后项目是跑起来了,但是控制台有 Failed to load source map
字样的日志
5:58:03 PM [vite] Failed to load source map for /@fs/xxx/node_modules/.vite/vite/hoist-non-react-statics.js?v=684d5287.
5:58:03 PM [vite] Failed to load source map for /@fs/xxx/node_modules/.vite/vite/antd.js?v=684d5287.
Sourcemap for "/xxx/node_modules/react-router-dom/index.js" points to missing source files
Sourcemap for "/xxx/node_modules/react-router/index.js" points to missing source files
Sourcemap for "/xxx/node_modules/async-validator/dist-web/index.js" points to missing source files
解决办法,增加 optimizeDeps.include 配置项 ,将出现如上错误的包,强制预构建链接
return defineConfig({
optimizeDeps: {
include: Object.keys(pkg.dependencies); // 这里为了方便将所有 dependencies 的包都加上了
},
server: {
// ...
},
});