Bootstrap

babel: 转换require为 import 写法

背景

我们有很多业务系统,在写 vue2的时候,可能路由是 require 写法,我们想改为 import , 当做技术升级,比如从webpack 迁移到 vite 的时候,会更简单。

举例说明

vue-router 中的内容

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

环境说明

node: 20.10.0
pnpm init

安装babel相关的核心包

pnpm add @babel/generator @babel/parser @babel/traverse @babel/types -S

安装babel相关的核心包

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, 可以不装

pnpm add typescript tslib ts-node -D
  • tslib 与 ts-node 搭档, ts-node依赖的
  • ts-node 直接运行ts文件

安装类型声明的包

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

"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

export default [
  {
    component:  (resolve) => require(['@/views/gogoggo'], resolve)
  }
]

image.png

我们看看 import 的 AST的样子

export default [
  {
    component:  () => import('@/views/test.vue')
  }
]

image.png

从上面截图对比分析, 我们可以有2种思路实现

  1. 我们可以找到所有的 component, 然后改写component后面的内容, 对于后面的内容,本质属于 component的属性, 不管后面是什么(对象也好, 箭头函数也好,函数表达式也行),本质就是 component的 属性。 我们通过 Property 函数(@babel/traverse 包 提供的),实现拦截
  2. 第二个思路是: 因为 (resolve) => require 写法,本质是 箭头函数,则我们只要拦截所有的箭头函数做相应的处理即可, 通过 ArrowFunctionExpression 函数(@babel/traverse 包 提供的),实现拦截

2个方式实现上有一点点差异(关注点不一样), 只要掌握了一种,基本就掌握了修改AST 的能力

代码实现

测试的 js文件就是上面的, 也可以自己随意写一个, 我是放在 demo/08.js 下面的

代码目录

- package.json
- src
    - 08.ts
- demo
    - 08.js
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

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相关的内容有帮助

喜欢就点个赞吧 😊😊😊

相关链接

;