背景
项目中使用了 Vue-cli,实际上用的就是 Webpack,但是 Webpack 在项目启动和热更新的时候会有点慢,而 Vite 目前热度高并且速度快,趁有空将项目从Vue-cli 切换到 Vite。
转换工具
我使用的是 wp2vite 转换工具,支持将 Webpack 转为 Vite。
# 安装
npm install -g wp2vite
# 运行
wp2vite
wp2vite 命令运行完成后,会在根目录生成 vite.config.js 文件,运行一下项目,不出意外收获一片报错,接下来就是解决报错啦。
缺少扩展名
Vite 引入文件都是不加扩展名的,会报 [vite] Internal server error: Failed to resolve import "./c-dialog" from xxx 的错,需要配置:
resolve: {
extensions: ['.vue', '.js', '.json']
}
缺少 alias
wp2vite 转换后会自动加上 alias 别名,如 @,vue$,但我在项目的css中使用了 ~@/assets/icons/xxx.svg 这样的引入方式,而 Vite 不会解析 '~' 符号。转换一下思路,我可以给 ~@ 加到 alias 上:
resolve: {
'@': path.resolve(__dirname, './src'),
'~@': path.resolve(__dirname, './src'),
}
配置 less 全局变量
css: {
preprocessorOptions: {
less: {
modifyVars: {
variables: `true;@import '${path.resolve('./src/assets/styles/variables.less')}';`
},
// 支持内联 JavaScript
javascriptEnabled: true
}
}
}
将require改为import
由于 Vite 不支持 require 引入文件,需要将 require 全部改为 import,如:
require('script-loader!jsonlint')
// =》改为
import jsonlint from 'jsonlint-mod'
window.jsonlint = jsonlint
html 插入变量
在项目中,有将配置文件和构建时间插入到 html,Webpack 写法:
<script src="<%= htmlWebpackPlugin.options.configFile %>"></script>
<script>
console.log('build: ' + '<%= new Date().getTime() %>')
</script>
转为 Vite 后,我自己写了个插件简单地兼容一下:
const htmlPlugin = () => {
// 定义变量
const define = {
buildTime: new Date().getTime(),
configFile: 'xxx.js'
}
return {
name: 'html-transform',
transformIndexHtml (html) {
return html.replace(
/<%=(\s*)(.*?)(\s*)%>/img,
($1, $2, $3) => {
return define[$3] || $1
})
}
}
}
...
...
plugins: [
htmlPlugin()
]
那么在 html 文件中使用:
<script src="<%= configFile %>"></script>
<script>
console.log('build: ' + '<%= buildTime %>')
</script>
svg-icon 组件兼容
使用 vite-plugin-svg-icons 插件对 svg-icon 组件进行兼容:
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
...
...
plugins:[
// 插件作用:使用<svg-icon />组件
createSvgIconsPlugin({
// svg图标所在目录
iconDirs: [path.resolve(process.cwd(), 'src/assets/svg-icons')],
// 执行icon name的格式
symbolId: 'icon-[dir]-[name]',
svgoOptions: {
plugins: [
// 添加插件移除 svg 中的 fill 属性
{
name: 'removeAttrs',
params: {
attrs: 'fill'
}
},
// 设置默认不删除stroke
{
name: 'preset-default',
params: {
overrides: {
removeUselessStrokeAndFill: false
}
}
}
]
}
})
]
svg-icon 组件:
<template>
<svg class="svg-icon" :style="getStyle" :class="[link && 'is-link']">
<use :xlink:href="`#icon-${name}`" :fill="isHover && hoverColor ? hoverColor : color" />
</svg>
</template>
<script>
import 'virtual:svg-icons-register'
export default {
name: 'SvgIcon',
props: {
name: {
type: String,
required: true
},
color: {
type: String
},
hoverColor: {
type: String
},
width: {
type: String
},
height: {
type: String
},
size: {
type: Number
},
link: {
type: Boolean,
default: false
}
},
data () {
return {
isHover: false,
once: true
}
},
computed: {
getStyle () {
const width = typeof this.width === 'number' ? this.width + 'px' : this.width
const height = typeof this.height === 'number' ? this.height + 'px' : this.height
const size = typeof this.size === 'number' ? this.size + 'px' : this.size
return {
width,
height,
fontSize: size
}
}
},
methods: {
addEvent () {
if (!this.$el || !this.once || !this.hoverColor) return
this.$el.addEventListener('mouseenter', this.onMouseenter, false)
this.$el.addEventListener('mouseleave', this.onMouseleave, false)
this.once = false
},
removeEvent () {
if (!this.$el) return
this.$el.removeEventListener('mouseenter', this.onMouseenter)
this.$el.removeEventListener('mouseleave', this.onMouseleave)
},
onMouseenter () {
this.isHover = true
},
onMouseleave () {
this.isHover = false
}
},
watch: {
hoverColor: {
immediate: true,
handler () {
this.$nextTick(() => {
this.addEvent()
})
}
}
},
beforeDestroy () {
this.removeEvent()
}
}
</script>
<style lang="less" scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
&.is-link {
cursor: pointer;
}
}
</style>
不足: 使用
vite-plugin-svg-icons插件,会在首屏的时候将所有svg图标一次性引入,没办法做到按需引入。
Vite 打包问题
使用 Vite 打包后,我发现有些文件大小超乎我的想象,1M 左右的文件都有好几个,那我先装个文件分析工具看看吧。
import visualizer from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
visualizer({
open:true,
gzipSize: true,
brotliSize: true
})
],
})
经过分析,我发现第三方库都打包到一起了,那么,只要将第三方库的文件拆分出来就行了。
build: {
rollupOptions: {
output: {
manualChunks (id) {
if (id.includes('node_modules')) {
const arr = id.toString().split('node_modules/')[1].split('/')
switch (arr[0]) {
case 'vue':
case 'vue-router':
case 'vuex':
return 'vueBase'
default:
return '_' + arr[0]
}
}
}
}
}
}
完整配置
/* eslint-disable */
import legacyPlugin from '@vitejs/plugin-legacy'
import * as path from 'path'
import {
createVuePlugin
} from 'vite-plugin-vue2'
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import visualizer from 'rollup-plugin-visualizer'
// @see https://cn.vitejs.dev/config/
const htmlPlugin = () => {
// 定义变量
const define = {
buildTime: new Date().getTime(),
configFile: 'xxx.js'
}
return {
name: 'html-transform',
transformIndexHtml (html) {
return html.replace(
/<%=(\s*)(.*?)(\s*)%>/img,
($1, $2, $3) => {
return define[$3] || $1
})
}
}
}
export default ({
command,
mode
}) => {
process.env.VUE_APP_ENV = mode
let proxy = {
'/api': {
"target": "http://demo.api.com:8081",
"logLevel": "debug",
"changeOrigin": true
}
}
// todo 替换为原有变量
let define = {
'process.env.NODE_ENV': command === 'serve' ? '"development"' : '"production"',
'process.env.VUE_APP_ENV': `"${mode}"`
}
return {
base: './', // index.html文件所在位置
root: './', // js导入的资源路径,src
resolve: {
alias: {
'vue$': 'vue/dist/vue.runtime.esm.js',
'@': path.resolve(__dirname, './src'),
'~@': path.resolve(__dirname, './src'),
},
extensions: ['.vue', '.js', '.json']
},
define: define,
server: {
// 代理
proxy
},
build: {
target: 'es2015',
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
manifest: false, // 是否产出maifest.json
sourcemap: false, // 是否产出soucemap.json
outDir: 'release/dist', // 产出目录
rollupOptions: {
output: {
manualChunks (id) {
if (id.includes('node_modules')) {
const arr = id.toString().split('node_modules/')[1].split('/')
switch (arr[0]) {
case 'vue':
case 'vue-router':
case 'vuex':
return 'vueBase'
default:
return '_' + arr[0]
}
}
}
}
}
},
plugins: [
htmlPlugin(),
createVuePlugin(),
legacyPlugin({
targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
}),
// 插件作用:使用<svg-icon />组件
createSvgIconsPlugin({
// 指定要缓存的图标文件夹
iconDirs: [path.resolve(process.cwd(), 'src/assets/svg-icon')],
// 执行icon name的格式
symbolId: 'icon-[dir]-[name]',
svgoOptions: {
plugins: [
// 添加插件移除 svg 中的 fill 属性
{
name: 'removeAttrs',
params: {
attrs: 'fill'
}
},
// 设置默认不删除stroke
{
name: 'preset-default',
params: {
overrides: {
removeUselessStrokeAndFill: false
}
}
}
]
}
}),
visualizer({
open: true,
gzipSize: true,
brotliSize: true
})
],
css: {
preprocessorOptions: {
less: {
modifyVars: {
variables: `true;@import '${path.resolve('./src/assets/styles/variables.less')}';`
},
// 支持内联 JavaScript
javascriptEnabled: true
}
}
}
}
}
遇到的问题
-
不支持 contentHash,打包时不能根据内容生成hash,没办法利用浏览器缓存
-
没有找到图片转为base64的办法
-
Vite 打包时文件拆分没有 Webpack 那么智能
-
svg-icon 组件引入的图标无法按需加载
结尾
Vite 改造后,项目启动速度达到秒开级别,但是首屏页面加载需要10s左右,由于 Vite 的缓存,首屏之后页面加载和热更新几乎都是很快且无感知的。