babel: 转换require为 import 写法

背景

我们有很多业务系统,在写 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种思路实现

  1. 我们可以找到所有的 component, 然后改写component后面的内容, 对于后面的内容,本质属于 component的属性, 不管后面是什么(对象也好, 箭头函数也好,函数表达式也行),本质就是 component的 属性。 我们通过 Property 函数(@babel/traverse 包 提供的),实现拦截
  2. 第二个思路是: 因为 (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在线网站查看到,一个节点是什么类型,就有对应的方法, 比如上面的 PropertyArrowFunctionExpression 它是我们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相关的内容有帮助

喜欢就点个赞吧 😊😊😊

相关链接

相关推荐
庸俗今天不摸鱼27 分钟前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下34 分钟前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox1 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞1 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行1 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox
m0_593758101 小时前
firefox 136.0.4版本离线安装MarkDown插件
前端·firefox
掘金一周1 小时前
金石焕新程 >> 瓜分万元现金大奖征文活动即将回归 | 掘金一周 4.3
前端·人工智能·后端
三翼鸟数字化技术团队1 小时前
Vue自定义指令最佳实践教程
前端·vue.js
Jasmin Tin Wei2 小时前
蓝桥杯 web 学海无涯(axios、ecahrts)版本二
前端·蓝桥杯
圈圈编码2 小时前
Spring Task 定时任务
java·前端·spring