Vite 实践:Vue 旧项目迁移

1,515 阅读5分钟

将一个 Vue2 旧项目的打包工具从 Webpack 迁移到 Vite。详细记录过程中遇到的问题及解决方案,以及一些比较个性化的配置。

初始化

根据官网,装一下 vite,和对应 vue2 的插件 vite-plugin-vue2

npm i -D vite vite-plugin-vue2

配置一下命令:

{
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "serve": "vite preview",
    }
}

在根目录新建 vite.config.js 文件,然后按官网写配置。

import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'

export default defineConfig({
  plugins: [
    createVuePlugin()
  ]
})

入口文件

入口的 html 文件需要放在项目根目录下,并加上这一句,加载入口 js 文件。

<script type="module" src="/src/main.js"></script>

或者使用 vite-plugin-html 插件,类似于 webpack 的 html-webpack-plugin

npm i -D vite-plugin-html

支持模板语法,可以插入script,修改 title,压缩 html 等。

// vite.config.js
import html from 'vite-plugin-html'

export default defineConfig({
  plugins: [
    html({
        inject: {
          data: {
            title: 'vite 改造',
            injectScript: '<script type="module" src="/src/main.js"></script>',
          },
          minify: true
        },
      })
  ]
})

资源导入 & 文件扩展和别名

resolve.extensionsresolve.alias 配置于 webpack 差不多,直接复制过来就行。

// vite.config.js
const path = require('path')

export default defineConfig({
  resolve: {
    extensions: ['.js', '.vue', 'scss', '.css', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  }
})

需要注意的是,我们在 css 预处理器中使用 @import 加载别的样式文件,都会在路径别名前加上 ~(比如 @import '~@/style/index.scss')。但这在 vite 是不需要的,官网也有说明。

Vite 为 Sass 和 Less 改进了 @import 解析,以保证 Vite 别名也能被使用。另外,url() 中的相对路径引用的,与根文件不同目录中的 Sass/Less 文件会自动变基以保证正确性。

有三个办法,一是把代码里的 @import~ 都去掉,但数量多的话不好使,所以推荐后面两种方法,再设置一个 ~@ 的别名。

{
  alias: {
    '~@': path.resolve(__dirname, 'src')
  }
}

或者是 vite 的 issue 中提到的解决方法,通过 alias 配置把 ~ 去掉。

export default defineConfig({
  // ...
  resolve: {
    alias: [
      { find: /^~/, replacement: '' }
    ],
  }
});

此外,vite 是基于 ESM 的,所以不能用 require 导入资源。可以用 import.meta.url 代替,详见new URL(url, import.meta.url)

webpack 的 require.context 在 vite 下也有替代方案:import.meta.glob。一般用于批量导入文件,如图标。

css 预处理器

这里配置也很简单,官网说得很详细,很多情况都有 demo。原来的项目用的是 scss。

首先要装 sass。

npm i -D sass

如果没有特殊情况,就没有别的配置了。但是要注意,在单文件 vue 组件的 <style> 标签中一定要注明 lang。这里就是 <style lang="scss">。因为原来不注明 lang,webpack 通过 loader 还是能正常解析,现在换成 vite 就不行了。

sass 全局变量注入,即 sass-resources-loader 的功能。加上以下配置:

// vite.config.js

export default defineConfig({
  // ...
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: '@import "style/variable.scss"; @import "style/mixin.scss";'
      }
    }
  }
})

注意 ; 必写,否则会报错。

同时,用 /deep/>>> 修改组件内部样式不行了,只能用最新的 ::v-deep,这个只能全局替换了。

这里在打包的时候还遇到一个 warning:

warning: "@charset" must be the first rule in the file.

@charset: 'utf-8'; 是用于解决 css 预处理器不支持中文注释的问题,从这个警告上看,是打包合并多个 css 文件的时候,这行代码不在文件开始。

最后找到的方法是这样的:

// vite.config.js

export default defineConfig({
  // ...
  css: {
    postcss: {
      plugins: [
        {
          postcssPlugin: 'internal:charset-removal',
          AtRule: {
            charset: (atRule) => {
              if (atRule.name === 'charset') {
                atRule.remove()
              }
            }
          }
        }
      ],
    },
  }
})

有些人说的设置 css.preprocessorOptions.scss.charsetfalse 在我这里并不生效。

eslint 配置

找了很多资料,vite 场景下的 eslint 大多数都是配置 vscode 的。不符合我的要求,我希望还是在项目里配置,然后保存代码时候就会进行检测。

webpack 中是通过 loader 实现的,在 vite 中只能通过插件实现。vite 没有 eslint 插件,但是有些 rollup 插件 vite 是可以兼容的,在 Vite Rollup 插件 可以找到 @rollup/plugin-eslint 插件。

// vite.config.js
import eslint from '@rollup/plugin-eslint'

export default defineConfig({
  // ...
  plugins: [
    {
      ...eslint({
        include: ['**/*.{js,vue}']
      }),
      enforce: 'pre',
      apply: 'serve'
    }
  ]
})

enforceapply 等参数详见使用插件

按照推荐的配置,但发现 eslint 连 template 模板和 style 样式都检测了。观察 eslint 报错的地址,可以发现 vue 文件的模板和样式会被转换成地址格式为类似这样的地址:

...index.vue?vue&type=template&lang.js
...index.vue?vue&type=style&index=0&scoped=true&lang.css

那我们可以直接用正则去掉这两种类型的资源地址,我这里匹配存在字符串 type=templatetype=style 的地址:

// vite.config.js

export default defineConfig({
  // ...
  plugins: [
    {
      ...eslint({
        include: ['**/*.{js,vue}'],
        exclude: [/^(?=.*type=(template|style)).*&/]
      }),
      enforce: 'pre',
      apply: 'serve'
    }
  ]
})

因为是改造,@rollup/plugin-eslint 回去读原来的 .eslintrc.js 文件的配置。试了一下,能正确校验,没发现其他问题。

Babel 配置

个人觉得,既然用 vite 了,把文件都打包成 ESM 了,就不必再过分追求旧浏览器的兼容性了。

官方推荐 @vitejs/plugin-legacy 插件来兼容旧浏览器。下面是推荐配置:

// vite.config.js
import legacy from '@vitejs/plugin-legacy'

export default defineConfig({
  // ...
  plugins: [
    legacy({
      targets: ['defaults', 'not IE 11']
    }),
  ]
})

从打包结果上看,每个 js 文件会多一个对应的 legacy.js 文件,以及多一个 ployfill.js 文件。

如果想要更多的个性化配置,还有 @rollup/plugin-babel 插件可以选择,但官方不是很推荐。

开发服务器配置

这个跟 webpack 区别不大,但是要注意 proxy 使用正则表达式匹配的,与 webpack 用 http-proxy-middlewareglob 模式去匹配路径是有区别的。

// vite.config.js

export default defineConfig({
  // ...
  server: {
    port: 9090,
    proxy: {
      '^(?=.*\.api).*$': proxyConfig,
      '/api': proxyConfig,
    }
  },
})

构建配置

参考构建选项,基本都使用默认配置就行。也会自动帮你压缩 js 和 css,对于用 import() 引入的模块,也会打包成异步模块,基本没有要操心的地方,不配置都行。

静态资源处理

对于有强迫症的人,肯定接受不了把所有js,css,图片等文件都丢到一个文件夹下面的打包结果。所以这里要打算根据类型进行分类。

这里要通过 build.rollupOptions 向 rollup 注入打包配置。rollup 的配置可以看文档

这里主要用到 output.entryFileNamesoutput.chunkFileNamesoutput.assetFileNames。类似 webpack,output.entryFileNames 处理入口(同步) js 模块,output.chunkFileNames 处理异步 js 模块,output.assetFileNames 处理 css,图片,字体等其他资源。

assetFileNames 可以传入一个函数,入参为资源信息:

type AssetInfo = {
  fileName: string;
  name?: string;
  source: string | Uint8Array;
  type: 'asset';
};

可以通过正则表达式匹配 name 属性来判断是什么类型的资源。

// vite.config.js

const getAssetsDir = (name) => {
  switch (true) {
  case /\.(eot|ttf|otf|woff2?)(\?\S*)?$/.test(name):
    return '/font/'
  case /\.(png|jpe?g|gif|svg)(\?.*)?$/.test(name):
    return '/images/'
  case /\.css$/.test(name):
    return '/css/'
  default:
    return '/'
  }
}

export default defineConfig({
  // ...
  build: {
    rollupOptions: {
        entryFileNames: 'assets/js/[name]-[hash].js',
        chunkFileNames: 'assets/js/[name]-[hash].js',
        assetFileNames: (assetInfo) => {
          const assetsDir = getAssetsDir(assetInfo.name)
          return `assets${assetsDir}[name]-[hash][extname]`
        }
    }
  },
})

有些较小的资源,可以直接编译成 base64 放到代码里,而不必打包成单独的文件,以减少网络请求。通过 build.assetsInlineLimit 进行配置,类似 webpack 的 Rule.parser.dataUrlCondition.maxSize

除此之外,一些不希望处理的静态资源可以放到根目录的 public 目录下,或者用 publicDir 设置 public 文件夹。在构建时这些资源会直接复制到输出目录的根目录。一般用来处理 favicon.icorobots.txt 等。

异步模块命名

打包出来的异步模块,是 <文件名>-<hash>.js 的格式,而多个业务模块的主页文件名都是 index.vue,这就导致打包出来有很多个 index-xxx.js 的文件,让人非常不爽。

在 webpack5 中可以通过 optimization.moduleIdsoptimization.chunkIds 或者魔术注释 /* webpackChunkName: xxx */ 来定义异步模块的名称,在 vite 中我们只能通过 output.chunkFileNames 模拟一下。

这里我们可以将异步模块相对于 src 目录的路径用 _ 拼接,再加上 hash 作为其名称,例如 /src/views/main/index.vue 的模块名称为 views_main_index-xxxx.js

output.chunkFileNames 可以接收一个函数作为参数,入参为 ChunkInfo

type ChunkInfo = {
  code: string;
  dynamicImports: string[];
  exports: string[];
  facadeModuleId: string | null;
  fileName: string;
  implicitlyLoadedBefore: string[];
  imports: string[];
  importedBindings: { [imported: string]: string[] };
  isDynamicEntry: boolean;
  isEntry: boolean;
  isImplicitEntry: boolean;
  map: SourceMap | null;
  modules: {
    [id: string]: {
      renderedExports: string[];
      removedExports: string[];
      renderedLength: number;
      originalLength: number;
      code: string | null;
    };
  };
  name: string;
  referencedFiles: string[];
  type: 'chunk';
};

facadeModuleId 就是模块路径。

// vite.config.js

const getAsyncModuleName = (moduleId) => {
  const relativePath = path.relative(__dirname, moduleId)
  return relativePath
    .replace(/\.\w+/, '')
    .split(path.sep)
    .filter(v => !['..', '.', 'src'].includes(v))
    .join('_')
}

export default defineConfig({
  // ...
  build: {
    rollupOptions: {
      chunkFileNames: chunkFileNames: (chunkInfo) => {
        let name = chunkInfo.isDynamicEntry
          ? getAsyncModuleName(chunkInfo.facadeModuleId)
          : '[name]'
        return `assets/js/${name}-[hash].js`
      },
    }
  },
})

这样处理之后,js 文件确实没问题了,但对应的异步 css 文件却不经过 chunkFileNamesassetFileNames 拿不到具体路径,目前没有解决方法。

代码拆分

官方建议通过异步引入代码的形式来进行代码拆分,但也有手动拆分的配置,即 rollup 的 output.manualChunks。因为不是很必要所以这次就没有配置。

总结

主要记录了遇到的问题和一些关键配置,知识比较浅,算是一个简单的教程吧。但应该学习的不是怎么配置,而是 vite 依赖解析,依赖预构建的原理。