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

lxf2023-03-18 12:24:02

前言

Vite 经过一段时间的发展,目前的生态已经非常丰富了。它不仅用于 VueReactSvelteSolidMarkoAstroShopify Hydrogen,以及 StorybookLaravelRails项目都已经接入了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)。

  1. yaml 由原来的 yaml-loader 替换为 rollup-plugin-yamlx
    rules: [
      {
        test: /\.ya?ml$/,
        use: 'yaml-loader'
      }
    ]
import PluginYamlX from 'rollup-plugin-yamlx'

plugins: [
  ...other,
  PluginYamlX()
]
  1. 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]'
  }),
]
  1. 注意文件加载方式的一致性,比如原来的 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_VERSIONVITE_APP_VERSIONENV_TESTVITE_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']
  }),
]

基本上所有在用的插件都可以找到对应替换的,甚至像 monacoqiankunsentry使用量相对没那么大的都有。

这里只是举例兼容旧代码,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 的循环引用问题

可以看到 ViteIssues 有很多相关的问题讨论。

github.com/vitejs/vite… github.com/vitejs/vite…

如果是 Vue SFC 文件的循环引用,按官方文档来就可以解决。

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

如果是其他文件的循环引用,也可以梳理更改。但是吊诡的地方在于,调用栈会出现 null。这个在开发中出现了根本没办法debug。有时候有上下文,只是中间出现null还能推断一下,如果提示一串null,那根本没办法开发。

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

CommonJs 与 ESM 对于循环依赖的处理的策略是截然不同的,webpack 在运行时注入的 webpack_require 逻辑在处理循环依赖时的表现与 CommonJs 规范一致。Webapck 根据 moduleId,先到缓存里去找之前有没有加载过,如果有加载过,就直接拿缓存中的模块。如果没有,就新建一个 module,并赋值给缓存中,然后调用 moduleId 模块。所以由于缓存的存在,出现循环依赖时才不会出现无限循环调用的情况。

由于 ESM 的静态 import 能力,可以在代码运行之前对依赖链路进行静态分析。所以在 ESM 模式下,一旦发现循环依赖,ES6 本身就不会再去执行依赖的那个模块了,所以程序可以正常结束。这也说明了 ES6 本身就支持循环依赖,保证程序不会因为循环依赖陷入无限调用。

正是因为处理机制的不同,导致 Vite 下循环引用的文件都会出现调用栈为 null 的情况。

找了个webpack插件circular-dependency-plugin 检查了一下循环引用的文件,发现像下面这样跨多组件引用的地方有几十处。改代码也不太现实,只能先换回webpack了。

我从 webpack 换到 vite,又换回了 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 相差不多了。

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

看了下 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框架单独移了出来,以脱离框架适配 SWCTSC 或者 ESBuildjsx 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 基本一致了。至于开发,两者的逻辑不一样,热更新确实是慢。

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

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

结束

Vite 的生态已经很丰富了,基本能满足绝大多数的需求了。我们这次迁移由于平时开发遗留的一些问题而失败了。应该反省平时写代码不能只为了快,而忽略一些细节。

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