我從 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


歡迎關注微信公眾號 :深圳灣碼農