前言
Vite
经过一段时间的发展,目前的生态已经非常丰富了。它不仅用于 Vue
,React
、Svelte
、Solid
、Marko
、Astro
、Shopify Hydrogen
,以及 Storybook
、Laravel
、Rails
等项目都已经接入了Vite
,而且也趋于稳定,所以就着手把项目的 Webpack
替换为 Vite
。
切换为 Vite
Vite
生态现在很丰富了,基本上插件按名称搜索一下,照着文档就可以把 webpack
替换到 Vite
。因为每个项目的配置都不一样,所以也没有什么统一的操作步骤,下面列一些典型替换的例子。
入口
index.html
的位置需要放到项目的最外层,而不是 public
文件夹内。同样 entry
的入口文件也需要从 pages
里换到 index.html
里。由 <script type="module" src="...">
引入。
module.exports = defineConfig({
pages: {
index: {
// page 的入口
entry: 'src/main.ts',
// 模板来源
template: 'index.html',
chunks: ['chunk-vendors', 'chunk-common', 'index']
}
}
})
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="/assets/favicon.ico" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
文件loader
这里挑几个例子(下面例子 webpack 版本都为 webpack5)。
yaml
由原来的yaml-loader
替换为rollup-plugin-yamlx
rules: [
{
test: /\.ya?ml$/,
use: 'yaml-loader'
}
]
import PluginYamlX from 'rollup-plugin-yamlx'
plugins: [
...other,
PluginYamlX()
]
svg-sprite
由原来的svg-sprite-loader
替换为vite-plugin-svg-icons
const resolve = (...dirs) => require('path').resolve(__dirname, ...dirs)
chainWebpack(config) {
const svgRule = config.module.rule('svg')
svgRule.exclude.add(resolve('base/assets/icons')).end()
config.module
.rule('icons')
.test(/\.svg$/)
.include.add(resolve('base/assets/icons'))
.end()
.use('svg-sprite-loader')
.loader('svg-sprite-loader')
.options({
symbolId: 'ys-svg-[name]'
})
.end()
}
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import { resolve } from 'path'
const pathResolve = (dir: string): string => {
return resolve(__dirname, '.', dir)
}
plugins: [
...other,
createSvgIconsPlugin({
// Specify the icon folder to be cached
iconDirs: [pathResolve('base/assets/icons/svg')],
// Specify symbolId format
symbolId: 'ys-svg-[name]'
}),
]
- 注意文件加载方式的一致性,比如原来的
svg-loader
直接import
引用的是路径地址,而vite-svg-loader
默认是Vue
组件。 所以Vite
需要把默认方式改成和webpack lodaer
一致。
plugins: [
svgLoader({ defaultImport: 'url' })
]
每个替换的插件都要看一下文档,也许某个配置就是你需要的功能。
全局常量
比如开发的版本信息,开发环境变量等等。
new webpack.DefinePlugin({
APP_VERSION: process.env.VUE_APP_VERSION,
ENV_TEST: process.env.VUE_ENV_TEST
})
import { defineConfig, loadEnv } from 'vite'
const { VITE_SENV_TEST, VITE_APP_VERSION } = loadEnv(mode, process.cwd())
export default ({ mode }: { mode: string }) => {
return defineConfig({
define: {
APP_VERSION: VITE_APP_VERSION,
ENV_TEST:VITE_SENV_TEST
}
})
})
这里注意,Vite 和 webpack 默认暴露的环境变量前缀不一样。
自动加载模块
比如 lodash
plugins: [
new webpack.ProvidePlugin({
_: 'lodash'
}),
]
import inject from '@rollup/plugin-inject'
plugins: [
inject({
_: 'lodash',
exclude: ['**/*.css', '**/*.yaml'],
include: ['**/*.ts', '**/*.js', '**/*.vue', '**/*.tsx', '**/*.jsx']
}),
]
基本上所有在用的插件都可以找到对应替换的,甚至像
monaco
,qiankun
,sentry
使用量相对没那么大的都有。
这里只是举例兼容旧代码,lodash 最好还是写个工具替换成 es-loadsh。
webpack require context
在 webpack 中我们可以通过 require.context
方法动态解析模块。比较常用的一个做法就是指定某个目录,通过正则匹配等方式加载某些模块,这样在后续增加新的模块后,可以起到动态自动导入的效果。
比如 layout,router
的自动注册都可以这样用。
const modules = require.context('base/assets/icons/svg', false, /\.svg$/)
Vite 支持使用特殊的 import.meta.glob
函数从文件系统导入多个模块:
const modules = import.meta.glob('base/assets/icons/svg/*.svg')
externals
externals: {
config: 'config',
}
import { viteExternalsPlugin } from 'vite-plugin-externals'
plugins: [
viteExternalsPlugin({
config: 'config'
})
]
ESM 模块
由于 Vite
使用了 ESM 模块方式,所以 commonJs模块
都需要替换成 ESM模块
。
const path = require('path')
import path from 'path'
也正是因为这个原因,所以才会又换回了 webpack,这个下面再讲。
自动化转换
社区也有一些自动化从Wepback
转为Vite
的工具,比如vue-cli-plugin-vite,webpack-to-vite,wp2vite等等。
如果是小项目,可以尝试一下。大项目不建议使用,不可控。感兴趣的可以去看对应的文档。
ESM 的循环引用问题
可以看到 Vite
的 Issues
有很多相关的问题讨论。
github.com/vitejs/vite… github.com/vitejs/vite…
如果是 Vue SFC 文件的循环引用,按官方文档来就可以解决。
如果是其他文件的循环引用,也可以梳理更改。但是吊诡的地方在于,调用栈会出现 null
。这个在开发中出现了根本没办法debug
。有时候有上下文,只是中间出现null
还能推断一下,如果提示一串null
,那根本没办法开发。
CommonJs 与 ESM 对于循环依赖的处理的策略是截然不同的,webpack 在运行时注入的 webpack_require 逻辑在处理循环依赖时的表现与 CommonJs 规范一致。Webapck 根据 moduleId,先到缓存里去找之前有没有加载过,如果有加载过,就直接拿缓存中的模块。如果没有,就新建一个 module,并赋值给缓存中,然后调用 moduleId 模块。所以由于缓存的存在,出现循环依赖时才不会出现无限循环调用的情况。
由于 ESM 的静态 import 能力,可以在代码运行之前对依赖链路进行静态分析。所以在 ESM 模式下,一旦发现循环依赖,ES6 本身就不会再去执行依赖的那个模块了,所以程序可以正常结束。这也说明了 ES6 本身就支持循环依赖,保证程序不会因为循环依赖陷入无限调用。
正是因为处理机制的不同,导致 Vite
下循环引用的文件都会出现调用栈为 null
的情况。
找了个webpack
插件circular-dependency-plugin
检查了一下循环引用的文件,发现像下面这样跨多组件引用的地方有几十处。改代码也不太现实,只能先换回webpack
了。
webpack 的优化
webpack
还是用官方封装的 Vue CLI
。
缓存
webpack4
还是使用 hard-source-webpack-plugin
为模块提供中间缓存的,但是 webpack5
已经内置了该功能。
module.exports = {
chainWebpack(config) {
config.cache(true)
}
}
hard-source-webpack-plugin
作者已经被webpack
招安了,原插件也已经不维护了,所以有条件还是升级到webpack5
。
esbuild 编译
编译可以使用 esbuild-loader
来替换 babel-loader
,打包这一块就和 Vite
相差不多了。
看了下 vue-cli
的配置,需要换的 rule
是这几个。大概的配置如下:
chainWebpack(config) {
const rule = config.module.rule('js')
// 清理自带的babel-loader
rule.uses.clear()
// 添加esbuild-loader
rule
.use('esbuild-loader')
.loader('esbuild-loader')
.options({
jsxFactory: 'h',
jsxFragment: 'Fragment',
loader: 'jsx',
target: 'es2015'
})
.end()
const tsRule = config.module.rule('typescript')
tsRule.uses.clear()
tsRule
.use('ts')
.loader('esbuild-loader')
.end()
}
注意,上面的
jsx
配置只适用于Vue3
,因为Vue2
没有暴露h
方法。
如果要在 Vue2
上使用 jsx
解析,得需要一个解析 Vue2
语法完整运行时的包。
pnpm i @lancercomet/vue2-jsx-runtime -D
大概就是把 jsx transform
从框架单独移了出来,以脱离框架适配 SWC
,TSC
或者 ESBuild
的 jsx transform
。
const rule = config.module.rule('js')
// 清理自带的babel-loader
rule.uses.clear()
// 添加esbuild-loader
rule
.use('esbuild-loader')
.loader('esbuild-loader')
.options({
target: 'es2015',
loader: 'jsx',
jsx: 'automatic',
jsxImportSource: '@lancercomet/vue2-jsx-runtime'
})
.end()
同时需要修改 tsconfig.json
{
"compilerOptions": {
...
"jsx": "react-jsx", // Please set to "react-jsx".
"jsxImportSource": "@lancercomet/vue2-jsx-runtime" // Please set to package name.
}
}
类型检查
类型检查这块开发时可以交给 IDE
来处理,没必要再跑一个线程。
chainWebpack(config) {
// disable type check and let `vue-tsc` handles it
config.plugins.delete('fork-ts-checker')
}
代码压缩
这些其实性能影响已经不大了,聊胜于无。
const { ESBuildMinifyPlugin } = require('esbuild-loader')
chainWebpack(config) {
config.optimization.minimizers.delete('terser')
config.optimization.minimizer('esbuild').use(ESBuildMinifyPlugin, [{ minify: true, css: true }])
}
优化结果
这是 Vue-CLI
优化之后的打包,已经和 Vite
基本一致了。至于开发,两者的逻辑不一样,热更新确实是慢。
结束
Vite
的生态已经很丰富了,基本能满足绝大多数的需求了。我们这次迁移由于平时开发遗留的一些问题而失败了。应该反省平时写代码不能只为了快,而忽略一些细节。
这就是本篇文章的全部内容了,感谢大家的观看。