我從 webpack 換到 vite,又換回了 webpack
前言
Vite 經過一段時間的發(fā)展,目前的生態(tài)已經非常豐富了。它不僅用于 Vue,React、Svelte、Solid、Marko、Astro、Shopify Hydrogen,以及 Storybook、Laravel、Rails 等項目都已經接入了Vite,而且也趨于穩(wěn)定,所以就著手把項目的 Webpack 替換為 Vite。
切換為 Vite
Vite 生態(tài)現(xiàn)在很豐富了,基本上插件按名稱搜索一下,照著文檔就可以把 webpack 替換到 Vite。因為每個項目的配置都不一樣,所以也沒有什么統(tǒng)一的操作步驟,下面列一些典型替換的例子。
入口
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' })
]
每個替換的插件都要看一下文檔,也許某個配置就是你需要的功能。
全局常量
比如開發(fā)的版本信息,開發(fā)環(huán)境變量等等。
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 默認暴露的環(huán)境變量前綴不一樣。
自動加載模塊
比如 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 方法動態(tài)解析模塊。比較常用的一個做法就是指定某個目錄,通過正則匹配等方式加載某些模塊,這樣在后續(xù)增加新的模塊后,可以起到動態(tài)自動導入的效果。
比如 layout,router 的自動注冊都可以這樣用。
const modules = require.context('base/assets/icons/svg', false, /\.svg$/)
Vite 支持使用特殊的 import.meta.glob 函數(shù)從文件系統(tǒng)導入多個模塊:
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,這個下面再講。
自動化轉換
社區(qū)也有一些自動化從Wepback轉為Vite的工具,比如vue-cli-plugin-vite,webpack-to-vite,wp2vite等等。
如果是小項目,可以嘗試一下。大項目不建議使用,不可控。感興趣的可以去看對應的文檔。
ESM 的循環(huán)引用問題
可以看到 Vite 的 Issues 有很多相關的問題討論。
github.com/vitejs/vite… github.com/vitejs/vite…
如果是 Vue SFC 文件的循環(huán)引用,按官方文檔來就可以解決。
如果是其他文件的循環(huán)引用,也可以梳理更改。但是吊詭的地方在于,調用棧會出現(xiàn) null。這個在開發(fā)中出現(xiàn)了根本沒辦法debug。有時候有上下文,只是中間出現(xiàn)null還能推斷一下,如果提示一串null,那根本沒辦法開發(fā)。
CommonJs 與 ESM 對于循環(huán)依賴的處理的策略是截然不同的,webpack 在運行時注入的 webpack_require 邏輯在處理循環(huán)依賴時的表現(xiàn)與 CommonJs 規(guī)范一致。Webapck 根據 moduleId,先到緩存里去找之前有沒有加載過,如果有加載過,就直接拿緩存中的模塊。如果沒有,就新建一個 module,并賦值給緩存中,然后調用 moduleId 模塊。所以由于緩存的存在,出現(xiàn)循環(huán)依賴時才不會出現(xiàn)無限循環(huán)調用的情況。
由于 ESM 的靜態(tài) import 能力,可以在代碼運行之前對依賴鏈路進行靜態(tài)分析。所以在 ESM 模式下,一旦發(fā)現(xiàn)循環(huán)依賴,ES6 本身就不會再去執(zhí)行依賴的那個模塊了,所以程序可以正常結束。這也說明了 ES6 本身就支持循環(huán)依賴,保證程序不會因為循環(huán)依賴陷入無限調用。
正是因為處理機制的不同,導致 Vite 下循環(huán)引用的文件都會出現(xiàn)調用棧為 null 的情況。
找了個webpack插件circular-dependency-plugin 檢查了一下循環(huán)引用的文件,發(fā)現(xiàn)像下面這樣跨多組件引用的地方有幾十處。改代碼也不太現(xiàn)實,只能先換回webpack了。
webpack 的優(yōu)化
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
React 關于全新 JSX 轉換的思想@lancercomet/vue2-jsx-runtime github
大概就是把 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.
}
}
類型檢查
類型檢查這塊開發(fā)時可以交給 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 }])
}
優(yōu)化結果
這是 Vue-CLI 優(yōu)化之后的打包,已經和 Vite 基本一致了。至于開發(fā),兩者的邏輯不一樣,熱更新確實是慢。
結束
Vite 的生態(tài)已經很豐富了,基本能滿足絕大多數(shù)的需求了。我們這次遷移由于平時開發(fā)遺留的一些問題而失敗了。應該反省平時寫代碼不能只為了快,而忽略一些細節(jié)。
這就是本篇文章的全部內容了,感謝大家的觀看。
作者:ARRON
鏈接:https://juejin.cn/post/7160670274521104397
作者:ARRON
歡迎關注微信公眾號 :深圳灣碼農