将一个 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.extensions
和 resolve.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.charset
为 false
在我这里并不生效。
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'
}
]
})
enforce
,apply
等参数详见使用插件。
按照推荐的配置,但发现 eslint 连 template 模板和 style 样式都检测了。观察 eslint 报错的地址,可以发现 vue 文件的模板和样式会被转换成地址格式为类似这样的地址:
...index.vue?vue&type=template&lang.js
...index.vue?vue&type=style&index=0&scoped=true&lang.css
那我们可以直接用正则去掉这两种类型的资源地址,我这里匹配存在字符串 type=template
和 type=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-middleware
的 glob
模式去匹配路径是有区别的。
// vite.config.js
export default defineConfig({
// ...
server: {
port: 9090,
proxy: {
'^(?=.*\.api).*$': proxyConfig,
'/api': proxyConfig,
}
},
})
构建配置
参考构建选项,基本都使用默认配置就行。也会自动帮你压缩 js 和 css,对于用 import()
引入的模块,也会打包成异步模块,基本没有要操心的地方,不配置都行。
静态资源处理
对于有强迫症的人,肯定接受不了把所有js,css,图片等文件都丢到一个文件夹下面的打包结果。所以这里要打算根据类型进行分类。
这里要通过 build.rollupOptions
向 rollup 注入打包配置。rollup 的配置可以看文档。
这里主要用到 output.entryFileNames
,output.chunkFileNames
和 output.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.ico
,robots.txt
等。
异步模块命名
打包出来的异步模块,是 <文件名>-<hash>.js
的格式,而多个业务模块的主页文件名都是 index.vue
,这就导致打包出来有很多个 index-xxx.js
的文件,让人非常不爽。
在 webpack5 中可以通过 optimization.moduleIds
,optimization.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 文件却不经过 chunkFileNames
,assetFileNames
拿不到具体路径,目前没有解决方法。
代码拆分
官方建议通过异步引入代码的形式来进行代码拆分,但也有手动拆分的配置,即 rollup 的 output.manualChunks
。因为不是很必要所以这次就没有配置。
总结
主要记录了遇到的问题和一些关键配置,知识比较浅,算是一个简单的教程吧。但应该学习的不是怎么配置,而是 vite 依赖解析,依赖预构建的原理。