我从 webpack 换到 vite,又换回了 webpack

前言
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

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.
  }
}

类型检查
类型检查这块开发时可以交给 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 的生态已经很丰富了,基本能满足绝大多数的需求了。我们这次迁移由于平时开发遗留的一些问题而失败了。应该反省平时写代码不能只为了快,而忽略一些细节。

这就是本篇文章的全部内容了,感谢大家的观看。

作者:ARRON

链接:https://juejin.cn/post/7160670274521104397

作者:ARRON


欢迎关注微信公众号 :深圳湾码农