
vite已经出了一段时间了,鉴于其启动及热更新速度的大幅提升,也逐渐被大家所熟知,具体原理可以参见vite的官方文档,这里就不多说了。当前项目组是基于webpack 4.*构建的,脚手架采用vue-cli那一套,随着项目维护人员几经易手,再加上项目代码量快速增加,启动速度已经非常慢,同时热更新的实时性也没有保障。借助着项目框架改造升级的机会,打算将工程化切换到vite上,毕竟是新东西,很多参考和样例都不足,跌跌撞撞,花了两个星期时间逐步将工程改造完毕,基于此将改造中遇到的问题,记录并分享出来,供大家参考。
当前项目的技术栈:vue2.0 + elementUI
运行环境:node (v14.16.1)
一、安装 Vite 环境
1. 安装依赖
直接通过 npm install vite
安装即可,接下来要兼容vue 2.0的使用场景,还需要安装以下几个依赖:
- vite-plugin-vue2 (vue 2.*的兼容依赖)
- @vue/compiler-sfc (单文件组件兼容依赖)
- @vue/composition-api (组合api的兼容依赖)
备注:如果项目中使用了 css 预编译处理,比如我们用的是 scss,还需要安装 sass 依赖,vite 支持 sass 的开箱即用,所以没有之前烦人的 node-sass 安装的难题。
2. 配置 vite.config.js
和 vue-cli 需要配置 vue.config.js 一样,vite 也需要添加一个配置文件 vite.config.js,只不过前者用的是 cjs规范, 后者用的是esm 规范,vite.config.js 的基础配置如下:
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2';
export default function({ mode }) {
return defineConfig({
plugins: [createVuePlugin()]
});
};
是不是很简单,没有了那么多复杂的加载器(loader)配置,到目前为止,我们就基本拥有了最基础的 vite 运行环境了。
3.配置启动脚本
接下来我们在 package.json 文件中配置启动脚本:
"scripts": {
"devv": "vite",
"buildv": "vite build",
"pre": "vite preview"
},
接下来就可以把工程抱起来看看了,但是事与愿违,工程没有跑起来,反而出了一大推错误,下面就详细记录我遇到的问题,分享给大家:
二、遇到的问题
1. index.html 的位置问题
这个问题 vite 官网已经解释的很详细,就是说 index.html 现在跟着源码走(放在与 vite.config.js 同级),不在放在 public 文件夹下面了,如果你不介意,直接把 public 文件夹下的 index.html 拖出来就好了。
但是我不想动代码结构,还想放到public下面有没有办法?有,安装插件即可,已经有现成的插件了:vite-plugin-html,也可以自己写个插件,这里我以 vite-plugin-html 为例,详细配置可以参考插件的官方文档:
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2';
import { createHtmlPlugin } from 'vite-plugin-html';
export default function({ mode }) {
return defineConfig({
plugins: [
//……
createHtmlPlugin({
minify: true,
entry: '/public/index.html',
inject: {
data: {
title: 'demo',
injectScript: `<script src="./inject.js"></script>`
},
},
})
//……
]
});
};
由于 vite 是基于 esm 规范加载的,所以在 index.html 中需要标记入口文件的类型为 module:
<script src="src/main.js" type="module" charset="utf-8"></script>
2. 路径别名(alias)
vite 中别名设置的匹配规则有变化,比如你有如下配置:
{
resolve: {
alias: {
'@': path.join(__dirname, 'src'),
'~': path.join(__dirname, 'node_modules'), // 这个通常给 css import 用的
}
}
}
他们会被 @/xxxx
、~/xxxx
命中,但是不会被 @xxxx
、~xxxx
命中,有些引入css的代码如: @import ~normalize.css/normalize.css
,这种是无法命中的,需要将配置改写如下(精确匹配):
{
resolve: {
extensions: ['.js', '.vue', '.json', '.jsx', '.tsx', '.ts'],
alias: [
{ find: /* @/ *//^@(?=\/)/, replacement: path.join(__dirname, 'src') },
{ find: /* ~/ *//^~(?=\/)/, replacement: path.join(__dirname, 'node_modules') },
{ find: /* ~@/ *//^~@(?=\/)/, replacement: path.join(__dirname, 'src') },
{ find: /* ~ *//^~(?![\/|@])/, replacement: path.join(__dirname, 'node_modules/') },
]
}
}
3. 深度选择器:/deep/ —-> ::v-deep
这个问题是在升级 sass 或者 dart-sass(node-sass已停止更新,且安装过程容易出错)后出现的,/deep/
是 vue2.0 的语法,在vite中需要使用 vue 3.0 的语法: ::v-deep
。
需要全局替换深度选择器。
4. 加载 svg
在 vue-cli 中 svg 的加载需要引入对应的 svg 加载器(如:svg-sprite-loader);在 vite 中,vite-plugin-svg-icons
插件可以帮你干同样的事情,首先在 vite.config.js 中引入和配置插件:
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons';
// ……
plugins: [
// ……
createSvgIconsPlugin({
// 指定需要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/icons/svg')],
// 指定symbolId格式
symbolId: 'icon-[name]',
})
// ……
];
// ……
还需要一步,即在项目初始化时,提前加载 svg 图标,这里在 ./src/main.js
中全局注册 svg 图标:
import 'virtual:svg-icons-register'; // 全局加载svg到缓存
5. env文件的读取(.env、.dev.env、.prod.env)
在 webpack 中,我们使用 process.env
来读取.env文件中的变量,在 vite中,我们在不同的场景可以用不同的方式读取:
- 在 vite.config.js 文件中,需要用
loadEnv
方法来读取:
import { defineConfig, loadEnv } from 'vite';
export default function({ mode }) {
return defineConfig({
server: {
proxy: {
'^/(abc|bcd)': {
target: loadEnv(mode, process.cwd()).VITE_proxy,
changOrigin: true,
secure: false,
}
}
}
});
};
- 在工程代码中,我们可以用
import.meta.env
来读取(如 ./src/main.js):
if(import.meta.env.VITE_Mode === "test") {
// TODO
}
注意,vite 的本地化变量需要以 VITE_
开头,否则上述方式均读取不到,应该可以修改这个前缀,具体参考 vite 的官方文档。
6. require(../../image/sss.png) 加载图片的问题
vite 采用 esm 规范对模块进行加载,不支持 require 的方式对图片或其他资源进行加载,不过已经有很多插件对这一部分进行了兼容。vite 提供了通过原生 API 解决此问题的方式:
export default {
data() {
return {
bgBlank: new URL(`../../assets/bg-blank.png`, import.meta.url).href,
configParams: {}
};
}
}
注意:
- new URL() 中的路径必须是相对路径,且不支持alias别名路径。(vite未作兼容)
- 如果系统中使用new URL() 的地方比较多,你想抽象成一个通用函数,那么你最好不要这么做,因为抽象成函数后,在 development 模式下没有问题,但在 production 模式下 vite 将无法根据 new URL 关键字去解析资源路径,因为资源路径是通过参数传递的,真正执行时发现仍然是个相对路径字符串,在dist目录下是找不到该路径的, 所以会报错,这个应该 vite 打包时没有兼容好,希望后续能兼容这种情况。
另外,还有一种替代方式加载预加载图片,即将 require 改成 import 进行图片引入,一般通过 import 引入的图片均缓存在文件头部变量中:
import fitmgr from '../assets/cloud/fitmgr.png';
7.require.context、require 动态加载文件
和 require 加载图片一样,vite 也不支持 require.context 动态读取并加载本地文件,vite 提供了两个方法来做这件事:
1)import.meta.glob
异步读取文件,读取的文件在打包时以单独的 chunk 来加载;
2)import.meta.globEager
同步读取文件,打包时会将读取的内容打包到一个整体 chunk 中;
注意:这两个方法都支持alias别名,但不支持传参和模板字符串,也就是说给出的路径必须是可访问的全路径(相对或者绝对)
两者都可以用来读取本地目录,在根据目录中的文件名来处理相关逻辑时非常有用,如:
动态组织路由
采用 import.meta.glob
异步加载,这样每个路由对应的组件均能独立打包成一个chunk。
const vueModules = import.meta.glob("@/**/*.vue");
/**
* 将组件字符串路径转换为异步组件
* @param {string} component 组件的字符串路径
* @returns {promise}
*/
function lazyLoading(component) {
component = component.trim();
return vueModules[`../${component}.vue`];
}
routesData.map(item => {
item.meta.routeId = item.menuId;
// 根据名字动态查查读取的内容
item.component = lazyLoading(item.component);
});
动态加载 svg 图片
只是为了获取文件夹下 svg 文件的文件名,采用 import.meta.globEager
同步方式读取。
// 组件定义
<template>
<div class="svg-icon">
<!-- svg -->
<svg v-else-if="svgNames.includes(svgiconClass)" :class="className" aria-hidden="true" v-on="$listeners">
<use :xlink:href="`#${svgiconClass}`" />
</svg>
</div>
</template>
<script>
const svgNames = Object.keys(import.meta.globEager('@/icons/svg/*.svg')).reduce((r, c) => [...r, `icon-${c.match(/\/([^/]*)\.svg/)[1]}`], []);
export default {
data() {
return {
svgNames: svgNames
};
},
props: ['iconClass', 'className']
computed: {
svgiconClass() {
return `icon-${this.iconClass}`;
}
}
};
</script>
// 组件使用
<svg-icon icon-class="avatar" class="user-avatar" />
注意:这两个方法没有 require.context 那么灵活,更倾向于全局缓存,局部按需引用。
8.commonjs 依赖库的加载
这个问题是最棘手的,由于 vite 是基于 esm 规范打包的,所有的文件及依赖分析都是基于 ESModule 的方式进行的,如果我们的依赖或者代码中出现了 commonjs 规范的内容,则会报错。
处理方式有两种:
1)升级对应的依赖包、库,现在很多依赖包、库的新版本均支持 esm 规范,引入即可,注意升级后的使用方式可能有变化,如 vue-echarts 依赖,v6.* 之前都是 commonjs,v6.* 之后是 esm ;另外,对于业务代码中编写的 commonjs 代码,手动改造成 esm 规范代码;
2)引入兼容插件, @originjs/vite-plugin-commonjs
,该插件兼容处理 commonjs 模块,我试了下,并不是 100% 解决commonjs 模块依赖问题,具体用法可以查看插件官方文档。
3)迁移 commonjs 依赖包、库为外部引入,即下载对应依赖包的 cdn 版本,放到工程的静态文件夹(public)下进行管理,然后在 index.html 中前置引入即可,在生产模式下,需要配置 cdn 包的全局引入变量,详见下例。
index.html 中添加库的引入(以 @jiaminghi/data-view
为例,当前最新版本仍然是 commonjs 版本):
<script src="util/datav.min.vue.js"></script>
vite.config.js 文件 external 块中配置全局引入变量:
external: ["CryptoJS", "jsonlint"]
在业务代码中,就可以直接用 CryptoJS
、jsonlint
变量来调用库中的方法了,并且能顺利通过 vite 的依赖分析。
9.css 中:export 语法的兼容
在 scss 变量文件中,有时我们既要被其他 .scss 引入,又想在 js 代码中导入该变量集合,于是我们在 scss 变量文件中这么定义(如:common.variable.scss):
/* theme color */
$--color-primary: #409eff;
$--color-success: #1CB02C;
$--color-warning: #F28202;
$--color-danger: #F55036;
:export {
primary: $--color-primary;
success:$--color-success;
warning:$--color-warning;
danger:$--color-danger;
dialog_header:$--dialog-header-dark;
}
在 webpack 的环境下,上面这样跑起来没有问题,但在 vite 下就不行了,从官网中得知 vite 支持 css module,需要将上述文件名修改为:
common.variable.scss ---------> common.variable.module.scss
没错,就是添加了 .module
进行标识,vite 会对其采用 css module 的方式进行读取。
还有个问题,一般来说 scss 变量在公共文件中引入后,全局可用,如以下方式:
index.scss 文件中引入变量文件和其他 scss 文件
// index.scss 文件中引入变量
@import "./ariables.module.scss";
@import "./btn.scss";
按理说,在 "./btn.scss"
文件中应该能使用 "./ariables.module.scss"
中定义的变量,但是在 vite 下会报错,即找不到对应的变量。
具体原因未知,直接上处理方式,vite 支持全局配置引入 scss 变量,vite.config.js 中配置如下:
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/ariables.module.scss";`
}
}
}
然而事实并没有那么顺利,上述配置也会读取"./ariables.module.scss"
文件并在其头部引入自身,变成了循环加载:
[vite] Internal server error: This file is already being loaded.
感觉preprocessorOptions
应该不会这么差劲,应该有什么办法可以排除加载变量文件本身才对,此处没有深究,接下来对additionalData
节点进行改造,因为他支持函数:
css: {
preprocessorOptions: {
scss: {
additionalData: (content, loaderContext) => {
// 如果是变量文件本身,则直接返回内容
if (loaderContext.endsWith('variables.module.scss')) return content;
// 其他 scss 文件才拼接变量文件加载前缀
return `@import 'src/styles/variables.module.scss'; ${content}`
}
}
}
}
至此,对于 css 相关的调试,一切都安静了。
10.jsx语法的兼容
在 vue 的语法中,我们一般都是通过 template 进行页面 html 的定义和封装,但是有时候我们需要自定义渲染,不错,就是 render
函数,如下:
<script lang="jsx">
export default {
render() {
return <div class={['code-preview-wrapper', this.className]}>
<style lang='scss'>{this.scopedStyle}</style>
<span id={this.uid} />
{errorDom}
</div>;
},
}
</script>
在 vite 环境中,需要显式定义 script
模块的解析模式 lang="jsx"
,同时在 vite.config.js 中添加对应的配置:
plugins: [
createVuePlugin({
jsx: true, // jsx语法兼容处理
})
]
11.vue.extend语法的兼容处理
使用过 vue.extend
函数分装组件的同学都知道,这个函数的功能很强大,但是我翻遍了 vue3.0 官网、vite官网,都没有提及此 API 的升级兼容方案,实际情况是,通过此 API 封装的组件在 vite 模式下,无法正确的渲染。
解决办法,修改此类组件,用 sfc 的方式实现一遍,这个确实有点坑,希望后续能有更好的办法解决。
12.其他问题
其他的一些细节问题,不展开讲,归总如下:
1) vue 组件模板语法块中定义的 lang="html"
需要去掉;
<template lang="html"></template>
2) import path from 'path';
对 path 的引用,需要改写为 import path from 'path-browserify';