目前一个用webpack构建的vue2.X 项目由于业务扩展越来越大,导致项目在本地编译时热更新很慢,页面更新需要10几秒以上。为提高团体开发速度和效率,最近打算把底部打包构建的环境由webpack迁移为vite。
兼容性注意
Vite 需要 Node.js 版本 >= 12.0.0。
底层实现上, Vite 是基于 esbuild 预构建依赖的
esbuild 使用 go 编写,并且比以 js 编写的打包器预构建依赖, 快 10 - 100 倍。 因为 js 跟 go 相比实在是太慢了,js 的一般操作都是毫秒计,go 则是纳秒
两者的启动方式也有所差异
webpack启动方式:
Webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。
vite启动方式:
Vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。
vue3.x中webpack迁移vite,主要借助wp2vite脚手架,改造过程中也会遇到很多问题,可参考这篇博文 项目 Webpack 转 Vite 实战
vue2.x中webpack转vite, 主要借助 vite-plugin-vue2插件,具体可参考 Vue2老项目使用vite2升级
<script type="module" src="/src/main.js"></script>
除上述博文提及的坑之外,还遇到了一些其它的问题,总结如下:
文件后缀省略导致页面报错404(例:vue文件引入时,webpack只需要文件名),在vite.config.js配置resolve.extensions中添加对应后缀,vite默认有[’.mjs’, ‘.js’, ‘.ts’, ‘.jsx’, ‘.tsx’, ‘.json’]
入口文件不是index.html,或者不在根目录,导致资源报错404;需将 /public/index.html 挪出到根目录路径下, 或者改变vite.config.js 默认的入口路径地址。
@别名的使用,修改相对路径地址
sass语言的扩展,默认生成的vite配置文件,只支持less语言,如项目中涉及sass语言,需额外扩展引入
webpack中使用require引入文件, 但vite中需要改成 important 引入;
因为vite的底层有使用到Rollup组件打包,但Rollup不支持common.js语法风格,所以需要改成esModule语法才能正确编译;
vite.config.js 中 optimizeDeps 也是此原理;目前社区中大部分模块都没有设置默认导出 esm,而是导出了 cjs 的包,目前在 vite 项目里直接使用 lodash 之类的包会报错;
optimize 命令专门为解决模块引用的坑而开发;将原来common.js语法转译为esm风格的语法供vite使用
...
optimizeDeps: {
// @iconify/iconify: The dependency is dynamically and virtually loaded by @purge-icons/generated, so it needs to be specified explicitly
include: [
'@iconify/iconify',
'ant-design-vue/es/locale/zh_CN',
'moment/dist/locale/zh-cn',
'ant-design-vue/es/locale/en_US',
'moment/dist/locale/eu',
],
exclude: ['vue-demi'],
},
...
const req = require.context('./svg', false, /\.svg$/)
const requireAll = requireContext => requireContext.keys().map(requireContext)
requireAll(req)
vite环境下,transformIndexHtml钩子函数读取到html源文件,并将svg标签插入到html的body中
核心代码如下:
import { readFileSync, readdirSync } from 'fs'
// id 前缀
let idPerfix = ''
// 识别svg标签的属性
const svgTitle = //
// 有一些svg文件的属性会定义height和width,要把它清除掉
const clearHeightWidth = /(width|height)="([^>+].*?)"/g
// 没有viewBox的话就利用height和width来新建一个viewBox
const hasViewBox = /(viewBox="[^>+].*?")/g
// 清除换行符
const clearReturn = /(\r)|(\n)/g
/**
* @param dir 路径
*/
function findSvgFile(dir: string): string[] {
const svgRes: string[] = []
const dirents = readdirSync(dir, {
withFileTypes: true
})
for (const dirent of dirents) {
const path = dir + dirent.name
if (dirent.isDirectory()) {
svgRes.push(...findSvgFile(path + '/'))
} else {
const svg = readFileSync(path)
.toString()
.replace(clearReturn, '')
.replace(svgTitle, ($1, $2) => {
let width = 0
let height = 0
let content = $2.replace(
clearHeightWidth,
(s1, s2, s3) => {
s3 = s3.replace('px', '')
if (s2 === 'width') {
width = s3
} else if (s2 === 'height') {
height = s3
}
return ''
}
)
if (!hasViewBox.test($2)) {
content += `viewBox="0 0 ${width} ${height}"`
}
return `${idPerfix} -${dirent.name.replace(
'.svg',
''
)}" ${content}>`
})
.replace('', '')
svgRes.push(svg)
}
}
return svgRes
}
vite默认只支持template模板渲染,不支持render(h, context) {}渲染;
template----html的方式做渲染
render----js的方式做渲染
解决方式: (1) 将render渲染写法改为template渲染写法
(2)通过扩展vite插件Plugin 转译
build编译后,vendor.js入口文件打包过大;
可通过cdn方式引入业务所需组件,如:Vue、ElementUI、moment、ECharts、tinymce、axios 等
然后通过 viteExternalsPlugin 插件排除以上组件的打包,以减小构建生成包的体积;
界面上则通过 window 对象获取各组件实例
除viteExternalsPlugin组件+ index.html中script引入外, 还可以通过扩展vite-plugin-cdn插件 + 修改vite.config.js文件中 rollup配置来实现排除打包。
css 深度作用选择器 >>> 写法改为 ::v-deep, vite中不支持>>>书写形式,不生效,且开发环境build构建时也会报警告
build编译后,有几处css文件 warning: “@charset” must be the first rule in the file, 意思是“@charset”必须是文件中的第一条规则, 初步怀疑是有其它css代码先于"@charset"编译了;但是目前还没排查出具体问题,暂时只是警告,不影响项目正常运行,若有知道解决方式的童鞋,望给出解决建议!
通过vite-plugin-html插件,搭配 .env 文件,可以在开发或构建项目时,对 index.html 注入动态数据,例如替换网站标题、区分正式、开发文件引入等。
项目打包时报@charset警告
解决方式: postcss.config.js 中去除@charset警告
module.exports = {
plugins: [
//require('autoprefixer'),
// 移除打包element时的@charset警告
{
postcssPlugin: 'internal:charset-removal',
AtRule: {
charset: (atRule) => {
if (atRule.name === 'charset') {
atRule.remove()
}
}
}
}
]
}
还有些细枝末节的问题,未记录。 至此,项目在本地和线上都能正常编译运行,经测试无大碍。
import legacyPlugin from '@vitejs/plugin-legacy'
plugins: [
...
legacyPlugin({
targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
}),
...
]
兼容原理查看此文章
使用感受:vite环境下热更新确实比webpack 快很多,从根本上解决了开发环境编译很慢的问题,从一定程度上大力的节省了开发人员的时间成本。但vite毕竟是后起之秀,生态环境、社区都尚不成熟,有遇到无法解决之坑的风险,所以谨慎迁移!!
建议: 页面较少的小项目,无需迁移;项目较大,有迁移需求的,多看几遍官方文档说明!! 新项目可直接使用vite的脚手架搭建。有需要的还可以自己封装vite-plugin组件使用。
vite官方api
Vite 官方中文文档
附件1(vite.config.js源码):
/* eslint-disable */
import legacyPlugin from '@vitejs/plugin-legacy';
import { svgBuilder } from './src/icons/builder.js';
import { viteExternalsPlugin } from 'vite-plugin-externals'
import {
viteMockServe
} from 'vite-plugin-mock';
import * as path from 'path';
import {
createVuePlugin
} from 'vite-plugin-vue2';
// @see https://cn.vitejs.dev/config/
export default ({
command,
mode
}) => {
let rollupOptions = {};
let optimizeDeps = {};
let proxy = {};
let define = {
'process.env.NODE_ENV': '"development"',
}
let esbuild = {}
return {
base: './', // index.html文件所在位置
root: './', // js导入的资源路径,src
resolve: {
extensions: ['.vue', '.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.node', '.scss'],
alias: {
'@': path.resolve(__dirname, 'src'),
'~@': path.resolve(__dirname, 'src'),
'vue$': 'vue/dist/vue.runtime.esm.js',
}
},
define: define,
server: {
host: '127.0.0.1',
port: '8085',
strictPort: true,
//open: '/#/dashboard',
// 代理
proxy,
fs: {
//strict: false,
// Allow serving files from one level up to the project root
//allow: ['..']
allow: ['.','..'],
}
},
build: {
target: 'es2015',
minify: 'terser', // 是否进行压缩,boolean | 'terser' | 'esbuild',默认使用terser
manifest: false, // 是否产出maifest.json
sourcemap: false, // 是否产出soucemap.json
outDir: 'dist', // 产出目录
rollupOptions,
},
esbuild,
optimizeDeps,
plugins: [
// legacyPlugin({
// targets: ['Android > 39', 'Chrome >= 60', 'Safari >= 10.1', 'iOS >= 10.3', 'Firefox >= 54', 'Edge >= 15'],
// }),
viteMockServe({
mockPath: 'mock',
localEnabled: command === 'serve',
}),
createVuePlugin(),
svgBuilder('./src/icons/svg/'),
viteExternalsPlugin({
vue: 'Vue',
'element-ui': 'ElementUI',
moment: 'moment',
echarts: 'echarts',
//ECharts: 'ECharts',
axios: 'axios',
tinymce: 'tinymce',
}),
],
css: {
preprocessorOptions: {
less: {
// 支持内联 JavaScript
javascriptEnabled: true,
},
sass: {
//additionalData: '@import "./src/styles/index.scss";', //global css
javascriptEnabled: true,
},
}
},
}
}