近期将几个项目的脚手架从 Vue-CLI 替换成了 Vite,直呼真香,原来冷启动2分多钟,现在只要几秒,对于需要频繁切项目的人来说,真的是开发利器。
当前 Vite 的优点不止于此,这篇文章不探讨 Vite 的优势,只记录下从 Vue-CLI 转 Vite 踩的一些坑。
提前说明下,以下问题的解决方法可能有多种,这里选用的是对业务库改动最小的,原因是:
Vite 在一个特殊的 import.meta.env 对象上暴露环境变量,Vue-CLI 是基于webpack,它是在 process.env 上挂载的。
此外,还有一个不同点是,原来的 vue.config.js 是能直接通过 process.env 拿到环境变量的,vite.config.js 却不能直接拿到,需要开发者自己调用 loadEnv 加载。
还有 Vite 只暴露以 VITE_ 开头的环境变量给客户端,Vue-CLI 中是 VUE_APP_ 开头。
对应的处理如下,通过 define 替换全局变量,这种方式目前来看是安全的。
import { loadEnv } from 'vite';
const ENV_PREFIX = ['VITE_', 'VUE_APP'];
export default ({ mode, serverProxy }) => {
const envMap = loadEnv(mode, process.cwd(), ENV_PREFIX) || {};
const appDir = envMap.VUE_APP_DIR;
return defineConfig({
root: `${path.resolve(curDirname, `./src/${appDir}`)}/`,
define: {
'process.env': {
...envMap,
NODE_ENV: mode,
},
},
})
}
Vite 中默认 index.html 在项目根目录下,也就是和 vite.config.js 同一层级,但是我们的大多数项目是 monorepo 模式,index.html 在 src/project/some-project下。
解决方法是设置root:
{
root: `${path.resolve(curDirname, `./src/${appDir}`)}/`,
}
但是,只有这个还不行,默认的 index.html 中是没有 <script type="module" src="./src/project/some-project/main.js"></script>
这一句的,所以要写个插件,当加载 index.html 时,动态插入这一句。
打包的时候发现另一个问题,只打包出来 index.html,其他js等文件没有被打包,猜测是打包的时候找不到 main.js,于是给插件增加配置enforce: pre
。
下面是插件核心代码:
return {
name: 'vite-plugin-transform-html',
enforce: 'pre',
transformIndexHtml(html) {
return html.replace(
/<\/head>/,
`<script type="module" src="./main.js"></script>
</head>`,
);
},
};
后面发现,生产环境不会触发transformIndexHtml
方法,上面代码并没有效果,于是优化成:
const res = {
name: 'vite-plugin-transform-html',
enforce: 'pre',
};
if (mode === 'development') {
return {
...res,
transformIndexHtml(code) {
return transformIndexHtml(code, mode);
},
};
}
return {
...res,
transform(code, id) {
if (id?.endsWith('.html')) {
return transformIndexHtml(code, mode);
}
},
};
Vite有个预构建阶段,用于将commonjs/UMD
模块转为ESM,和合并多个模块。就是把一些模块处理后放在node_modules/.vite/deps
目录下,项目启动时直接引用这个目录下的内容。
值得注意的是,这一阶段是有缓存的,且存在两处缓存,一处是.vite/deps
下的缓存,一处是浏览器的缓存。如果发现修改了插件,但是观察不到效果,可以尝试npx vite --fore
,以及禁用浏览器缓存。
有个公共库是同时支持vue2/vue3的,比如有个extend-comp功能,用来扩展组件,代码如下:
import Vue, { createApp } from 'vue';
export function extendComp(arg: ExtendCompParam) {
if (Vue?.version?.startsWith?.('2')) {
return extendV2(arg);
}
if (typeof createApp === 'function') {
return extendV3(arg);
}
return extendV2(arg);
}
它的顶部会尝试引用 createApp,如果是vue2的项目,它会报错,之前的兼容方案是扩展下vue的类型声明:
import 'vue/types/index';
declare module 'vue/types/index' {
function createApp(c: any, d?: any): any;
}
现在vite的预构建,会直接报错,因为vue依然没导出createApp,想到一个方式是写个插件在最底部加上createApp的导出,核心代码如下:
return {
transform(source, id) {
if (id.indexOf('vue.js') > -1 || id.indexOf('vue.runtime.esm.js') > -1) {
return `${source}
export const createApp = () => {}
`;
}
return source;
},
};
vue动态导入有多种方式,Vite可以支持 xxComp: ()=>import('xx.vue')
,不支持 xxComp(resolve){ require(['xx.vue'], resolve) }
,可以手动改业务库,但我们的目标是尽可能少的改项目,所以也可以写个插件,用于替换源代码,核心代码如下:
return {
transform(source, id) {
if (id.indexOf('.vue') === -1) {
return source;
}
const reg = new RegExp(/([a-zA-Z]+?)\(resolve\)(?:\s*?)\{(?:\n\s*)require\(\['(.*?)'\],(?:\s*?)resolve\);(?:\n\s*)\}/, 'g');
const match = source.match(reg);
if (match?.[1] && match[2]) {
const res = source.replace(reg, (match, originA, originB) => `${originA}: () => import('${originB}')`);
return res;
}
return source;
},
};
这个问题只要使用一下vite-plugin-style-import
就可以。
import {
createStyleImportPlugin,
VantResolve,
} from 'vite-plugin-style-import';
// ...
plugins: [
createStyleImportPlugin({
resolves: [
VantResolve(),
],
}),
]
关于external的Vite插件众多,这里用的是vite-plugin-externals
。
import { viteExternalsPlugin } from 'vite-plugin-externals';
// ...
plugins: [
viteExternalsPlugin({
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
'vue-lazyload': 'VueLazyload',
}),
]
部分项目需要配置proxy,配置如下。
const serverProxy = {
'/xxx-cgi': {
target: 'http://localhost:3000',
changeOrigin: true,
ws: true,
rewrite: path => path.replace(/^\/xxx-cgi/, ''),
},
};
// ...
server: {
proxy: {
...serverProxy,
},
},
Vite 中要想支持scss文件,需要安装sass,注意不是node-sass,这会引起另一个问题,/deep/
会报错,需要将 /deep/
换成 ::v-deep
,这两个作用一样,都可以在scoped下修改子组件样式,一些文章说::v-deep
性能更佳。
此外,某些项目有这种写法:
$--font-path: "~element-ui/lib/theme-chalk/fonts";
这种引用方式Vite默认情况下是无法识别的,最简单的方式是改成:
$--font-path: "node_modules/element-ui/lib/theme-chalk/fonts";
之前index.html中的这种写法会报错:
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
报错信息为:
[vite] Internal server error: URI malformed
解决方法是写个插件替换下:
res = code.replace(/<%=\s+BASE_URL\s+%>/g, baseDir);
值得注意的是下面这行代码不会报错,所以当要找的样式文件不存在时,可以直接用空字符串替换。
<style lang="scss" scoped src=""></style>
如何判断要找的文件存不存在呢,如何判断当前操作的文件目录呢?用path.dirname(id)
就可以,相关插件代码如下:
transform(source, id) {
let res = source;
if (res.indexOf(STYLE_KEYWORD) !== -1) {
const styleName = getStyleName(appDir);
const curDir = path.dirname(id);
let pureCSSLink = `./css/${styleName}.scss`;
const cssLink = path.resolve(curDir, pureCSSLink);
const isExist = fs.existsSync(cssLink);
if (!isExist) {
pureCSSLink = '';
}
res = res.replace(new RegExp(STYLE_KEYWORD, 'g'), pureCSSLink);
}
return res;
}
关于分包策略没有标准答案,每个项目都有自己的特点,目前我们项目采用的是这种:
const SPLIT_CHUNK_CONFIG = [
{
match: /[\\/]src[\\/]_?common(.*)/,
output: 'chunk-common',
},
{
match: /[\\/]src[\\/]_?component(.*)/,
output: 'chunk-component',
},
{
match: /[\\/]src[\\/]_?logic(.*)/,
output: 'chunk-logic',
},
];
const rollupOptions = {
output: {
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/static/[name]-[hash].[ext]',
manualChunks(id) {
for (const item of SPLIT_CHUNK_CONFIG) {
const { match, output } = item;
if (match.test(id)) {
return output;
}
}
if (id.includes('node_modules')) {
return id.toString().split('node_modules/')[1].split('/')[0].toString();
}
},
},
},
在Vue-CLI中是默认支持Vue2+JSX的,也就是不需额外配置,但是vite+vue2项目中,如果直接写jsx会报错,报错信息如下:
[vite] Internal server error: Failed to parse source for import analysis because the content contains invalid JS syntax. Install @vitejs/plugin-vue to handle .vue files.
尽管上面提示让你安装@vitejs/plugin-vue
这个库,但是这个是for Vue3 版本的,如果加上它,会报额外的的错误,说这个库仅服务于Vue3。
那怎么办呢,很简单,在使用JSX的script的地方加上:
<script lang="jsx">
然后在vite.config.js
中,为vite-plugin-vue2
这个插件增加jsx: true
的选项。
import { createVuePlugin } from 'vite-plugin-vue2';
// ...
plugins: [
createVuePlugin({
jsx: true,
}),
// ...
]
报错信息如下:
TypeError: Cannot read properties of undefined (reading '_android')
这个问题其实是判断的不严谨,已经有很多issue了,比如159,也有人提了PR,甚至合了,但是没发布版本。。
if (this._android && this._android <= 2.1) {
var factor = 1 / window.devicePixelRatio;
// ...
}
知道了问题,解决办法就很多了,可以fork下,自己发个包,也可以写个Vite插件转换下代码。
此外,有个问题是,在Vue-CLI中为什么不会报错呢?
因为Vite中使用的是ESM模块,默认会使用严格模式,“禁止this指向全局对象”。而Vue-CLI中使用的是UMD方式加载,在浏览器中会顶层的this等于window,所以不会报错。
不要在前端项目中使用:
import path from 'path'
会报错:
Error in render: "Error: Module "path" has been externalized for browser compatibility and cannot be accessed in client code."
而应该使用path-browserify
:
import path from 'path-browserify';
如果是用 path.resolve 方法,这样还是不行的,因为 resolve 方法里面使用了 process.cwd 方法,而 Vite 是没有注入 process 这个变量的。
有多个解决方法:
process
包,然后在项目中执行 window.process = process
,注意不要与vite.config.js中define变量冲突。process.cwd()
对应的字符串,生产环境替换成/
path.resolve
方法,不用第三方库// 模拟path.resolve()
function resolve(...paths) {
let resolvePath = '';
let isAbsolutePath = false;
let cwd;
for (let i = paths.length - 1; i >= -1; i--) {
let path;
if (isAbsolutePath) {
break;
}
if (i >= 0) {
path = paths[i];
} else {
if (cwd === undefined) {
cwd = process.cwd();
}
path = cwd;
}
if (!path) {
continue;
}
resolvePath = `${path}/${resolvePath}`;
isAbsolutePath = path.charCodeAt(0) === 47;
}
if (/^\/+$/.test(resolvePath)) {
resolvePath = resolvePath.replace(/(\/+)/, '/');
} else {
resolvePath = resolvePath.replace(/(?!^)\w+\/+\.{2}\//g, '')
.replace(/(?!^)\.\//g, '')
.replace(/\/+$/, '');
}
return resolvePath;
}
console.log(resolve('/aa', '../bb', 'cc', 'dd')); // => /bb/cc/dd
console.log(resolve('/aa', '../bb', './cc', 'dd')); // => bb/cc/dd
console.log(resolve('/', '/system', 'user', 'userIndex')); // => /system/user/userIndex
console.log(resolve('', 'system', 'user', 'userIndex')); // => ${cwd}/system/user/userIndex
base是开发或生产环境服务的公共基础路径,也就是文件引用路径,默认是/
。合法的值包括以下几种:
/foo/
https://foo.com/
./
(用于开发环境)我们项目会把静态文件上传到CDN,所以生产环境会应该是第二种——完整的URL,所以可以这么设置:
base: envMap.VUE_APP_PUBLIC_PATH || './'