11971

公司项目基于 Vue2 + Webpack4 + TypeScript3.5 技术栈,不支持新语法与新特性 🆕   由于项目放于 monorepo 仓库中,并且仓库中所有项目共用一套基础设施,升级的话影响面较广,因此干脆将项目迁出,直接升级成 Vite,运行速度确实是 🆙 🆙 🆙

What!这也算技巧?没错,虽然朴实无华,但是不可或缺 🥷

如下所示,直接将不符合要求的代码重构成 Vite 支持的写法。

// Before ☑️
const config = {
  logo: require('@/assets/logo.png'),
// After ✅
import logo from '@/assets/logo.png';
const config = { logo };

开发阶段中,Vite 的开发服务器将所有代码视为原生 ES 模块。因此,Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM。详见原文

由于 Vite 只支持 ES Module 包,对于 CommonJS 或 UMD 包必须经过转换才可使用,而 Vite 的智能导入分析可能存在缺漏,因此需要手动配置让依赖被预构建处理。

export default defineConfig({
  optimizeDeps: {
    include: ['pica', 'tinycolor2'],

并不是有了预构建就万事大吉 🙅

NPM 包内容五花八门,你可能会遇到包含 .vue.html 等文件的包,这类非 .js、.ts 的文件无法被预构建处理,将导致报错。

error: No loader is configured for ".vue" files

此时就需要将其排除在预构建以外。

export default defineConfig({
  optimizeDeps: {
    exclude: ['xxx'],

CommonJS 插件

被排除在预构建外的 CommonJS 包怎么用? 🤔

为了解决这一问题,我对搜索到的数个 CommonJS 插件进行简单地试用,最终基于成熟、可靠、全面等几方面的考虑,采用 @rollup/plugin-commonjs

import commonjs from '@rollup/plugin-commonjs';
export default defineConfig({
  plugins: [
    commonjs({
      transformMixedEsModules: true,
      include: ['path/to/xxx.js'],

❗ 某些情况下,在 Vite 中使用 @rollup/plugin-commonjs 会遇到一些问题,见下方源码:

// "rollup/plugins/blob/master/packages/commonjs/src/index.js"
  !dynamicRequireModuleSet.has(normalizePathSlashes(id)) &&
  (!hasCjsKeywords(code, ignoreGlobal) || (isEsModule && !options.transformMixedEsModules))
  return { meta: { commonjs: { isCommonJS: false } } };

当条件满足时,将返回一个没有code属性的对象,code可存放转换后的代码字符串,在下方使用:

// "vite/src/node/server/pluginContainer.ts"
for (const plugin of plugins) {
  let result = await plugin.transform.call(ctx, code, id, ssr)
  if (isObject(result)) {
    code = result.code || ''
  } else {
    code = result

Vite 从插件接收到的返回值对象拿不到code的值,将认为转换后的代码是空的,由此产生问题。

1️⃣ 解决方案:对返回值做一下简单处理

const cjsPlugin = commonjs({/* ... */});
const { transform } = cjsPlugin;
cjsPlugin.transform = function (code, id) {
  const result = transform.call(this, code, id);
  return result && !result.code && result.meta?.commonjs?.isCommonJS === false ? null : result;

此外,我还发现另一个问题:插件通过path.extname(id)的方式来获取文件扩展名,当 Vite 传递的 id 是带查询参数的 URL (如:path/to/xxx.js?v=123456)时,将不能正常工作。

2️⃣ 解决方案:对 id 进行转换

cjsPlugin.transform = function (code, id) {
  if (/\.js\?v=[^\?]+$/.test(id)) {
    id = id.replace(/\?v=[^\?]+$/, '');  // 去除 ?v=xxx
  const result = transform.call(this, code, id);
  return result && !result.code && result.meta?.commonjs?.isCommonJS === false ? null : result;
  • 查询参数可能是 ?v=xxx 或 ?t=xxx 或其它,真遇到了需自行变通处理;
  • 不一定会遇到上述的两个问题,我目前采用 transformMixedEsModules 加 include 的配置运行正常;
  • 💡 再附赠一个小技巧:默认情况下 rollup 插件在开发 (serve) 和生产 (build) 模式中都会被调用,可通过设置 apply 属性进行控制。详见

    cjsPlugin.apply = 'serve';
    

    自定义插件

    当不方便直接修改代码或者通过配置和插件解决问题时,可以考虑编写插件来处理。

    以我遇到的情况为例,有个依赖包导入了大量的 .html 文件,在 webpack 中可以用raw-loader进行加载,在 Vite 中可通过添加?raw后缀处理。然而,对依赖包不方便直接修改源码,并且文件数量较多,最好的方法是通过插件来解析:

    import { createFilter, dataToEsm } from '@rollup/pluginutils';
    function createHtmlPlugin() {
      const filter = createFilter('**/*.html');
      return {
        name: 'vite-plugin-html-loader',
        transform(code, id) {
          if (filter(id)) return dataToEsm(code);
    

    更多内容可查阅 Vite 插件 API

    resolve.alias除了用来定义@/等路径,还有其它妙用。

    比如 NPM 包component-classes中一段代码如下:

    try {
      var index = require('indexof');
    } catch (err) {
      var index = require('component-indexof');
    

    实际上只有component-indexof包是存在的,因此会产生indexof不存在的错误。处理起来也很简单:

    export default defineConfig({
      resolve: {
        alias: [{
          find: /^indexof$/,
          replacement: 'component-indexof',
    

    再比如,常用工具库lodash不支持 Tree Shaking,可以改用 ESM 版本lodash-es

    export default defineConfig({
      resolve: {
        alias: [{
          find: /^lodash$/,
          replacement: 'lodash-es',
    

    改造依赖包

    依赖包质量参差不齐,必要时需动手改造代码。

    webworkify为 🌰 ,其代码如下:

    var bundleFn = arguments[3];
    var sources = arguments[4];
    var cache = arguments[5];
    module.exports = function (fn, options) {/* ... */};
    

    代码开头的arguments让人摸不着头脑 🧠 。如果在 webpack 中,代码将被包裹在函数体内,一切正常。而 Vite 的处理可能不会包裹函数,因而报错。

    处置:将arguments[x]改为undefined

    // "shims/webworkify.mjs"
    var bundleFn = undefined;
    var sources = undefined;
    var cache = undefined;
    export default function (fn, options) {/* ... */};
    

    改造后的代码存至“/shims/webworkify.mjs”,再通过别名映射过去:

    export default defineConfig({
      resolve: {
        alias: [{
          find: /^webworkify$/,
          replacement: path.resolve(__dirname, 'shims/webworkify.mjs'),
    

    Uncaught ReferenceError: require is not defined

    参照章节“技巧”的处理方法:

  • 优先通过修改源代码来解决;
  • 其次通过预构建或 CommonJS 插件处理;
  • Uncaught SyntaxError: The requested module 'xxx' does not provide an export named 'yyy'

    error: No loader is configured for ".vue" files

    被预构建的包导入了非 .js、.ts 的文件(如:.vue),导致报错。

    处置:排除在预构建之外,通过 CommonJS 插件或其它插件处理。

    Uncaught ReferenceError: global is not defined

    代码中使用了 Node.js 环境的全局对象,简单做下兼容处理:

    window.global = window;
    

    若是依赖于bufferstream之类的内置模块,可通过安装兼容包来解决:

    npm i buffer stream
    

    [vite] Internal server error: Failed to resolve import "./Xxx" from "yyy.js". Does the file exist?

    导入的文件省略了扩展名,且默认不被支持。将正确的扩展名添加至扩展名列表:

    export default defineConfig({
      resolve: {
        extensions: ['.vue', /* ... */],
    

    [plugin: vite:dep-scan] Failed to resolve entry for package "xxx"

    依赖包未在 package.json 正确配置mainmodule等字段,导致 Vite 无法找到包的入口。

    处置:设置别名将其映射到正确的文件上

    export default defineConfig({
      resolve: {
        alias: [{
          find: /^vue-react-dnd$/,
          replacement: 'vue-react-dnd/dist/vue-react-dnd.es.js',
    

    ReferenceError: React is not defined

    未启用 JSX 转换。

    // For Vue 2
    export default defineConfig({
      plugins: [
        createVuePlugin({ jsx: true }),
    

    error: Unexpected "<"

    JSX 代码解析出错。

  • 在 .vue 组件中,需设置<script lang="jsx"><script lang="tsx">
  • 在 .js 文件中,需修改文件名为 .jsx
  • 在 .ts 文件中,需修改文件名为 .tsx
  • [Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available

    Vue 组件使用了template选项,需要编译器进行解析,而默认情况vue包是不带编译器的。

  • 将代码重构成 .vue 组件的写法;
  • 使用render选项替代template选项;
  • 设置别名加载含编译器的版本;
  • // For Vue 2
    export default defineConfig({
      resolve: {
        alias: [{
          find: /^vue$/,
          replacement: 'vue/dist/vue',
    

    net::ERR_ABORTED 408 (Request Timeout)

    Vite 检测到对依赖包的请求,且该依赖尚未被 Vite 处理过时,可能会触发预构建,导致请求超时以及页面重载。 要么多刷新几次等它完成预构建,要么将依赖加入optimizeDeps.include提前处理。