🚧 🚧 🚧 🚧 我更推荐你看 🖖 Vue2.x 改造 ⚡️ Vite - 2.0 🚧 🚧 🚧 🚧
2021-09-28
前言
vite
已经发布大半年了势头很猛,github 活跃度非常高- 从
2.x
开始加入了预编译,能够很好的兼容CommonJs
模式,预编译后有着相当快的冷启动速度 - 当系统维护越来越大启动速度就会越慢
@vue/cli
创建的项目(vue2.x)使用的webpack@4.x
版本,这个问题愈发严重 是时候集成到“年迈”vue2
的老项目中了
📢 注意: 本次改造只推荐在开发模式下运行 vite 生产环境依然用之前的方式;毕竟 webpack 在打包方面更加成熟
项目背景
- 本次改造的工程是公司一个很重要,迭代又很频繁的系统;现在已经有 100+ 张页面了
- 工程模板由
@vue/cli
创建的vue2.x
版本,内部使用webpack4.x
构建 - 随着项目越来越大(一年50增加张页面左右),对项目冷启动速度的追求就越显得迫切
技术分析
-
虽然
vite
发展很快,npm 上面关于 vite 的插件也跟进的很快;但是总有一些鞭长莫及的情况出现在我们的老项目中 -
这里我主要以我实际改造中碰到的问题做下技术总结,如果你在使用过程中还有碰到其他的问题,本文的解决问题思路也有一定的参考价值
-
以下是我碰到的改造问题点
- 入口文文件需要指向
public/index.html
----vite
启动入口 - 转换
@import '~normalize.css/normalize.css
中的~
别名 ----vite
报错 - 转换
import('@/pages/xxxx')
----vite
警告、报错 - 转换
require
为import
形式 ----vite
报错
- 入口文文件需要指向
vite-plugins
- 我们对于 vite 工程的改造都是基于插件的,可以理解为就是写了好多插件解决对应问题
- 你可能需要先了解下如何写一个 vite 插件
- 这次几个关于转换的插件都是用的插件中的
transform
钩子,相对比较简单容易理解
public/index.html -> index.html
- 摘自
vite
官网的一句话
你可能已经注意到,在一个 Vite 项目中,index.html 在项目最外层而不是在 public 文件夹内。这是有意而为之的:在开发期间 Vite 是一个服务器,而 index.html 是该 Vite 项目的入口文件。
vite
入口文件为 html 文件(webpack为 js 文件);
为此我们需要将 public/index.html
作为入口,并且要正确处理 js 的引入
-
这里我们通过插件的
configureServer
钩子做一层拦截并处理(public/index.htm),使其可以再vite
中工作 -
template.ts
import fs from 'fs'
import path from 'path'
import { Plugin as VitePlugin } from 'vite'
import _template from 'lodash.template'
export function template(options: {
/**
* @default public/index.html
*/
template?: string
// 兼容 html-webpack-plugin 中的编译注入
templateDate?: Record<string, unknown>
// index.html 中的 js 文件入口
entry?: string
}): VitePlugin {
return {
name: 'cxmh:template',
configureServer(server) {
server.middlewares.use((req, res, next) => {
// 跳过非 index.html 请求
if (!req.url?.endsWith('.html') && req.url !== '/') {
return next()
}
const templatePath = options.template || path.join(process.cwd(), 'public/index.html')
const entry = options.entry || '/src/main.js'
try {
// 读取 public/index.html
let indexHtml = fs.readFileSync(templatePath, 'utf8')
const compiled = _template(indexHtml, { interpolate: /<%=([\s\S]+?)%>/g })
// 注入 html-webpack-plugin 变量
indexHtml = compiled(options.templateDate)
// 指定 src 入口
indexHtml = indexHtml.split('\n')
.map(line => line.includes('</body>')
? ` <script type="module" src="${entry}"></script>${line}`
: line
).join('\n')
res.end(indexHtml)
} catch (error) {
res.end(`<h2>${error}</h2>`)
}
})
},
}
}
转换 @import ~ 别名
gonzales-pe
cssAST
工具node-source-walk
cssAST
遍历工具- style-import.ts
import path from 'path' import { Plugin } from 'vite' import { convertVueFile } from './utils' import Walker from 'node-source-walk' import gonzales from 'gonzales-pe' export function styleImport(options?: Record<string, unknown>): Plugin { const walker = new Walker as any // 判断是否为 @import 语句 const isImportStatement = (node) => { if (node.type !== 'atrule') { return false } if (!node.content.length || node.content[0].type !== 'atkeyword') { return false } const atKeyword = node.content[0] if (!atKeyword.content.length) { return false } const importKeyword = atKeyword.content[0] if (importKeyword.type !== 'ident' || importKeyword.content !== 'import') { return false } return true } // 去掉字符串两边的引号部分 const extractDependencies = (importStatementNode) => { return importStatementNode.content .filter(function (innerNode) { return innerNode.type === 'string' || innerNode.type === 'ident' }) .map(function (identifierNode) { return identifierNode.content.replace(/["']/g, '') }) } return { enforce: 'pre', name: 'vite-plugin-vue2-compatible:styleImport', transform(code, id) { if (!id.endsWith('.vue')) return let _code = code try { // 提出所有的 @import 语句 const imports = convertVueFile(code).styles.reduce((dependencies, cur) => { const ast = (gonzales as any).parse(cur.content, { syntax: cur.lang }) let deps = dependencies walker.walk(ast, (node: any) => { if (!isImportStatement(node)) return deps = deps.concat(extractDependencies(node)) }) return deps }, []) // 转换 @import 语句中的 ~ 别名 for (const importPath of imports) { if (importPath.startsWith('~')) { const node_modules = path.join(process.cwd(), 'node_modules') const targetPath = path.join( path.relative(path.parse(id).dir, node_modules), importPath.slice(1), ) // Replace alias '~' to 'node_modules' _code = _code.replace(importPath, targetPath) } } return _code } catch (error) { throw error } }, } }
转换 import('@/pages/xxxx')
-
这个还是挺麻烦的,需要考虑两个点
@
这种别名替换 ---- vite 报错xxxx
动态路径分析 ---- vite 警告
-
实现原理
impot('@/pages/' + path)
本质上是将 pages 下的所有文件列举处理,然后生成一个switch
提供匹配
如有目录结构如下:
src pages foo.vue bar/index.vue
将会生成:
function __variableDynamicImportRuntime__(path) { switch (path) { case '../pages/foo': return import('../pages/foo.vue'); case '../pages/foo.vue': return import('../pages/foo.vue'); break; case '../pages/bar': return import('../pages/bar/index.vue'); case '../pages/bar/index': return import('../pages/bar/index.vue'); case '../pages/bar/index.vue': return import('../pages/bar/index.vue'); break; } }
- 参考链接 dynamic-import-vars
-
dynamic-import 代码有点长,完整代码 github 链接
require
to import
-
这个问题就是 CommonJs to ESModule 方案,npm 上面找了好几个包都没实现我的功能(要么不转化,要么注入环境变量报错); 索性自己写了一个简化版的,也算给自己拓宽下技术线路(不能吃现成的,得会自己做不是)
-
技术选型
acorn
js 抽象语法树(AST)工具acorn-walk
语法树 遍历工具
-
实现原理
- 先用 acorn 将代码转化为
AST
- 在使用 acorn-walk 遍历
AST
分析出 require 加载得文件,然后转换成 import 格式即可
- 先用 acorn 将代码转化为
-
cjs-esm
代码有点长,完整代码 github 链接 -
基于 cjs-esm 写一个 vite-plugin-commonjs
如果有代码如下
const pkg = require('../package.json'); const routers = [{ path: '/foo', component: require('@/pages/foo.vue').default; }];
将会生成:
import pkg from "../package.json"; import _MODULE_default___EXPRESSION_object__ from "@/pages/foo.vue"; const routers = [{ path: '/foo', component: _MODULE_default___EXPRESSION_object__; }];
最后我们将所有插件打包到一个 npm 包中
- 完整代码 github 链接
- 在项目根目录添加
vite.config.ts
注意:下面的配置可能需要结合你项目的情况做一些调整
import path from 'path'
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import { vitePluginCommonjs } from 'vite-plugin-commonjs'
import {
dynamicImport,
styleImport,
template,
} from 'vite-plugin-vue2-compatible'
import pkg from './package.json'
export default defineConfig({
plugins: [
/**
* @Repository https://github.com/underfin/vite-plugin-vue2
*/
createVuePlugin({
jsx: true,
jsxOptions: {
compositionAPI: true,
},
}),
/**
* 处理 webpack 项目中 require 写法
*/
vitePluginCommonjs(),
/**
* 兼容 import('@xxxx') 写法别名
*/
dynamicImport(),
/**
* 兼容 @import alias
* @import '~normalize.css/normalize.css'
* ↓
* @import 'node_modules/normalize.css/normalize.css'
*/
styleImport(),
/**
* 使用 public/index.html
*/
template({
// 指向 @vue/cli 创建项目的 public/index.html 文件
template: path.join(__dirname, 'public/index.html'),
// 告诉 vite 的入口文件 index.html 加载哪个 js;既 webpack 配置中的 entry
entry: '/src/main.js',
// 兼容 html-webpack-plugin 中的编译注入
templateDate: {
BASE_URL: '/',
htmlWebpackPlugin: {
options: {
title: pkg.name,
},
},
},
}),
],
resolve: {
alias: {
// 同 webpack 中的 alias
'@': path.join(__dirname, './src'),
},
// 同 webpack 中的 extensions
extensions: ['.vue', '.ts', '.tsx', '.js', '.jsx', '.mjs'],
},
define: {
// 同 webpack.DefinePlugin
'process.env': process.env,
}
})
运行
npm i -D vite vite-plugin-vue2 vite-plugin-vue2-compatible
- 添加 packge.json 中 scripts 命令
{
"scripts": {
+ "vite": "export NODE_ENV=development; vite"
}
}
npm run vite
🎉 Boom shakalaka!