背景
我们有很多业务系统,在写 vue2的时候,可能路由是 require 写法,我们想改为 import , 当做技术升级,比如从webpack 迁移到 vite 的时候,会更简单。
举例说明
vue-router 中的内容
js
import Layout from '@/layout'
export default [
{
path: '/businessSystem',
component: Layout,
hidden: true,
children: [{
path: '/',
component: (resolve) => require(['@/views/businessSystem'], resolve),
name: 'businessSystem',
meta: {
title: '业务系统',
isMain: true
}
}]
},
{
component: () => import('@/veiws/2222')
},
{
component: (resolve) => require(['@/views/gogoggo'], resolve)
}
]
希望将 require 写法 改为 import
环境说明
makefile
node: 20.10.0
csharp
pnpm init
安装babel相关的核心包
sql
pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S
安装babel相关的核心包
shell
pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S
- @babel/parser 把字符串解析成 AST
- @babel/traverse 操作/新增/删除/修改 AST节点
- @babel/types 手动构建AST节点
- @babel/generator 把处理完成后的AST节点树,变成代码, 最终是一个字符串
安装ts, 不想使用ts, 可以不装
csharp
pnpm add typescript tslib ts-node -D
- tslib 与 ts-node 搭档, ts-node依赖的
- ts-node 直接运行ts文件
安装类型声明的包
shell
pnpm add @types/node @types/babel-types @types/babel__generator @types/babel__traverse -D
注意:
- 不要把
package.json
中的type
设置为module
, 直接不设置这个字段 - 修改ts.config.json里面的
module: "NodeNext"
, 其他配置可以不动, 保证 ts-node 运行ts文件的时候不会有问题
package.json
json
"dependencies": {
"@babel/generator": "^7.23.6",
"@babel/parser": "^7.23.9",
"@babel/traverse": "^7.23.9",
"@babel/types": "^7.23.9"
},
"devDependencies": {
"@types/babel-types": "^7.0.15",
"@types/babel__core": "^7.20.5",
"@types/babel__generator": "^7.6.8",
"@types/babel__traverse": "^7.20.5",
"@types/node": "^20.11.19",
"ts-node": "^10.9.2",
"tslib": "^2.6.2",
"typescript": "^5.3.3",
}
过程分析
我们先使用 最简单的代码结构查看 AST, 查看AST 通过 在线网站 AST explorer
js
export default [
{
component: (resolve) => require(['@/views/gogoggo'], resolve)
}
]
我们看看 import 的 AST的样子
javascript
export default [
{
component: () => import('@/views/test.vue')
}
]
从上面截图对比分析, 我们可以有2种思路实现
- 我们可以找到所有的 component, 然后改写component后面的内容, 对于后面的内容,本质属于 component的
属性
, 不管后面是什么(对象也好, 箭头函数也好,函数表达式也行),本质就是 component的属性
。 我们通过Property
函数(@babel/traverse
包 提供的),实现拦截 - 第二个思路是: 因为
(resolve) => require
写法,本质是 箭头函数,则我们只要拦截所有的箭头函数做相应的处理即可, 通过ArrowFunctionExpression
函数(@babel/traverse
包 提供的),实现拦截
2个方式实现上有一点点差异(关注点不一样), 只要掌握了一种,基本就掌握了修改AST 的能力
代码实现
测试的 js文件就是上面的, 也可以自己随意写一个, 我是放在 demo/08.js 下面的
代码目录
json
- package.json
- src
- 08.ts
- demo
- 08.js
ts
import { NodePath } from '@babel/traverse'
/**
* 转换require为 import 写法
*/
import generate from '@babel/generator'
import { parse } from '@babel/parser'
import traverse from '@babel/traverse'
import * as types from '@babel/types'
import fs from 'node:fs'
import path from 'node:path'
const target = path.resolve(__dirname, '../demo/08.js')
const content = fs.readFileSync(target, {
encoding: 'utf-8',
})
let ast = parse(content, {
sourceType: 'module',
})
/**
* 监控属性节点,进行操作
* @param path
*/
const handleNode = (path) => {
// 一定要断言这里的node类型,node是一个泛型
const node = path.node as types.ObjectProperty
// 需要给key 做一个类型限定
if (types.isIdentifier(node.key) && node.key.name === 'component') {
// 找到对象key为 component才处理
// 找到为箭头函数的 节点
if (types.isArrowFunctionExpression(node.value)) {
const arrayExpression = node.value as types.ArrowFunctionExpression
const body = arrayExpression.body as types.CallExpression
if (types.isArrayExpression(body.arguments[0])) {
if (
body.arguments[0].elements.length &&
types.isStringLiteral(body.arguments[0].elements[0])
) {
// 拿到里面的字符串,也就是组件的路径的定义
const routePathStr = body.arguments[0].elements[0].value
// 重新构造一个AST节点。 将这块内容替换为 import写法
const newNode = genArrowFunction(routePathStr)
// 替换
path.node.value = newNode
}
}
}
}
}
// 第二种操作方式,监控箭头函数 钩子, 需要对 path做一个类型判断,否则,下面的 replaceWith 方法无法出现
const changeByArrowFunctionExpression = (
path: NodePath<types.ArrowFunctionExpression>
) => {
// 从函数调体里面拿到 组件的路径
const node = path.node as types.ArrowFunctionExpression
const body = node.body as types.CallExpression
if (!body.arguments) return
if (types.isArrayExpression(body.arguments[0])) {
let elements = body.arguments[0].elements as Array<types.StringLiteral>
// 拿到路径字符串
let routePathStr = elements[0].value
if (!routePathStr) return
// 重新构造一个AST节点。 将这块内容替换为 import写法
const newNode = genArrowFunction(routePathStr)
// 节点替换
path.replaceWith(newNode)
}
}
/**
* 重新生成一个箭头函数
* @param str
*/
function genArrowFunction(str: string): types.ArrowFunctionExpression {
return types.arrowFunctionExpression(
[],
types.importExpression(types.stringLiteral(str))
)
}
traverse(ast, {
/**
* 监听属性
* @param path
*/
// Property(path) {
// handleNode(path)
// },
/**
* 监听箭头函数
*/
ArrowFunctionExpression(path) {
changeByArrowFunctionExpression(path)
},
})
// 生成代码
let dir = path.resolve(__dirname, '../demo/08-1.js')
fs.writeFileSync(dir, generate(ast).code)
最终效果
demo/08-1.js
js
import Layout from '@/layout';
export default [{
path: '/businessSystem',
component: Layout,
hidden: true,
children: [{
path: '/',
component: () => import("@/views/businessSystem"),
name: 'businessSystem',
meta: {
title: '业务系统',
isMain: true
}
}]
}, {
component: () => import('@/veiws/2222')
}, {
component: () => import("@/views/gogoggo")
}];
问题
1. 我知道字符串的表达式要怎么写,但是AST,我不知道要怎么表达和编写?
答:通过 AST 在线网站,实现简单案例编写,查看AST 结构, 本质是一个 树
, 每一个结构(变量声明,字符串,表达式,函数,导入,导出)都在这个树上有对应的节点(Node
), 特别要注意节点类型。
2. 当使用 @babel/traverse
我如何知道要使用什么方法拦截,实现AST修改处理?
答: 其实这个是不需要记忆的,一般我们通过 AST
在线网站查看到,一个节点是什么类型,就有对应的方法, 比如上面的 Property
和 ArrowFunctionExpression
它是我们AST中的节点类型, @babel/traverse
也就提供了对应的方法
当一个节点为属性节点的时候, @babel/traverse
在遍历到这个AST 树,遇到属性节点,会调用对应的 Property()
方法, 我们就可以做对应处理, 其他类型都是一样的思路
3. 我一定要使用 ts吗 ?
答: 不是的, 如果你不会使用ts, 可以使用js, 但是在修改和 通过 @babel/types
包 手动构建
AST节点的时候, 可能会遇到各种各样的问题, 代码在运行的时候,babel
提供的 error
信息不是很明显,可能会花费较多时间让你去修改 error
ts 可以帮助你,每次编写的类型,都符合期望, 减少传参的类型错误等。
4. 代码中ts类型,我不知道写什么?
各种拦截函数中,我不知道类型是什么,有的是 泛型
, 但是代码在运行的时候,类型是确定的,我在编写的时候,ts各种报错, 要怎么处理? 比如上面的 NodePath<types.ArrowFunctionExpression>
通过 debug 或者 console打日志, 都会告诉你这个对象是一个什么类型, 然后对应引入即可
总结
我们通过上面的案例,学会了多种方式实现对 AST的修改, 也学会了怎么查看AST节点类型, 手动构建AST节点
希望对你学习 babel 和 AST相关的内容有帮助
喜欢就点个赞吧 😊😊😊