在大型前端项目中,我们一般会使用 webpack、Rollup 等工具进行模块整合,但是庞大的代码量会使得我们在开发阶段花费更多的时间在(代码改动 --> 页面渲染)这个阶段,即使使用 HMR 这个问题也没有完全的解决,项目代码量达到一定规模,积少成多,HMR 带来的效率提升会逐渐不够
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
打开 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 中是如何实现的
通常一个 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
的同时支持, 不需要我们再手动配置
与 webpack 项目一样,我们只需要创建一个 tsconfig.json 来配置 Typescript 检查规范即可,然后开发阶段,借助开发工具的类型检查提示;生产阶段,直接通过 tsc 来进行整理的类型检查
不同的是 Vite 使用 esbuild 将 TypeScript 转译到 JavaScript,约是 tsc 速度的 20~30 倍,同时 HMR 更新反映到浏览器的时间小于 50ms。 – Vite – Typescript
此外 vite 中的 TypeScript 还需要做如下处理
///
来增加一些 vite 的类型定义补充
- 不需要 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 代码拆分
}
}
});
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 请求
}
}
});
更多资源处理细节可以查看官方文档
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 也可以依据它来进行兼容
大型项目的目录结构复杂,为了方便文件调用,一般会通过 “别名” 的方式,与 webpack 一样, vite 中通过 vite.config.js 中设置 resolve.alias 来实现
return defineConfig({
resolve: { // 文件别名
alias: {
store: path.join(context, 'src/store')
},
},
});
// 业务代码中,可以按照如下方式引用
import globalStore, { Provider } from 'store';
在 webpack 中我们可以借助 webpack.DefinePlugin 来向项目中注入一些环境变量,在 vite 中可以有两种方式来实现
// 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);
此外 vite 内置了一些环境变量
.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 文件中 增加如下
///
interface ImportMetaEnv {
readonly VITE_ENV_XXX: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
// 业务代码中使用注入的常量
console.log(import.meta.env.VITE_ENV_XXX);
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 等
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>
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 'antd';
import 'antd/es/button/style/index.js';
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
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`
})
]
});
在 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 核心插件执行前执行
},
]
});
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
}
},
});
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: {
// ...
},
});