经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Java » 查看文章
带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法
来源:cnblogs  作者:京东云开发者  时间:2023/4/12 16:13:22  对本文有异议

作者:京东零售 周明亮

写在前面

这里我们初步提到了一些基础概念和应用:

  • 分析器
  • 抽象语法树 AST
  • AST 在 JS 中的用途
  • AST 的应用实践

有了初步的认识,还有常规的代码改造应用实践,现在我们来详细说说使用 AST, 如何进行代码改造?

Babel AST 四件套的使用方法

其实在解析 AST 这个工具上,有很多可以使用,上文我们已经提到过了。对于 JS 的 AST 大家已经形成了统一的规范命名,唯一不同的可能是,不同工具提供的详细程度不一样,有的可能会额外提供额外方法或者属性。

所以,在选择工具上,大家按照各自喜欢选择即可,这里我们选择了babel这个老朋友。

初识 Babel

我相信在这个前端框架频出的时代,应该都知道babel的存在。 如果你还没听说过babel,那么我们通过它的相关文档,继续深入学习一下。

因为,它在任何框架里面,我们都能看到它的影子。

  • Babel JS 官网
  • Babel JS Github

作为使用最广泛的 JS 编译器,他可以用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。

而它能够做到向下兼容或者代码转换,就是基于代码解析和改造。接下来,我们来说说:如何使用@babel/core里面的核心四件套:@babel/parser、@babel/traverse、@babel/types及@babel/generator。

1. @babel/parser

@babel/parser 核心代码解析器,通过它进行词法分析及语法分析过程,最终转换为我们提到的 AST 形式。

假设我们需要读取React中index.tsx文件中代码内容,我们可以使用如下代码:

  1. const { parse } = require("@babel/parser")
  2. // 读取文件内容
  3. const fileBuffer = fs.readFileSync('./code/app/index.tsx', 'utf8');
  4. // 转换字节 Buffer
  5. const fileCode = fileBuffer.toString();
  6. // 解析内容转换为 AST 对象
  7. const codeAST = parse(fileCode, {
  8. // parse in strict mode and allow module declarations
  9. sourceType: "module",
  10. plugins: [
  11. // enable jsx and typescript syntax
  12. "jsx",
  13. "typescript",
  14. ],
  15. });

当然我不仅仅只读取React代码,我们甚至可以读取Vue语法。它也有对应的语法分析器,比如:@vue/compiler-dom。

此外,通过不同的参数传入 options,我们可以解析各种各样的代码。如果,我们只是读取普通的.js文件,我们可以不使用任何插件属性即可。

  1. const codeAST = parse(fileCode, {
  2. // parse in strict mode and allow module declarations
  3. sourceType: "module"
  4. });

通过上述的代码转换,我们就可以得到一个标准的 AST 对象。在上一篇文章中,已做详细分析,在这里不在展开。比如:

  1. // 原代码
  2. const me = "我"
  3. function write() {
  4. console.log("文章")
  5. }
  6. // 转换后的 AST 对象
  7. const codeAST = {
  8. "type": "File",
  9. "errors": [],
  10. "program": {
  11. "type": "Program",
  12. "sourceType": "module",
  13. "interpreter": null,
  14. "body": [
  15. {
  16. "type": "VariableDeclaration",
  17. "declarations": [
  18. {
  19. "type": "VariableDeclarator",
  20. "id": {
  21. "type": "Identifier",
  22. "name": "me"
  23. },
  24. "init": {
  25. "type": "StringLiteral",
  26. "extra": {
  27. "rawValue": "我",
  28. "raw": "\"我\""
  29. },
  30. "value": "我"
  31. }
  32. }
  33. ],
  34. "kind": "const"
  35. },
  36. {
  37. "type": "FunctionDeclaration",
  38. "id": {
  39. "type": "Identifier",
  40. "name": "write"
  41. },
  42. "generator": false,
  43. "async": false,
  44. "params": [],
  45. "body": {
  46. "type": "BlockStatement",
  47. "body": [
  48. {
  49. "type": "ExpressionStatement",
  50. "expression": {
  51. "type": "CallExpression",
  52. "callee": {
  53. "type": "MemberExpression",
  54. "object": {
  55. "type": "Identifier",
  56. "computed": false,
  57. "property": {
  58. "type": "Identifier",
  59. "name": "log"
  60. }
  61. },
  62. "arguments": [
  63. {
  64. "type": "StringLiteral",
  65. "extra": {
  66. "rawValue": "文章",
  67. "raw": "\"文章\""
  68. },
  69. "value": "文章"
  70. }
  71. ]
  72. }
  73. }
  74. }
  75. ]
  76. }
  77. }
  78. ]
  79. }
  80. }

2. @babel/traverse

当我们拿到一个标准的 AST 对象后,我们要操作它,那肯定是需要进行树结构遍历。这时候,我们就会用到 @babel/traverse 。

比如我们得到 AST 后,我们可以进行遍历操作:

  1. const { default: traverse } = require('@babel/traverse');
  2. // 进入结点
  3. const onEnter = pt => {
  4. // 进入当前结点操作
  5. console.log(pt)
  6. }
  7. // 退出结点
  8. const onExit = pe => {
  9. // 退出当前结点操作
  10. }
  11. traverse(codeAST, { enter: onEnter, exit: onExit })

那么我们访问的第一个结点,打印出pt的值,是怎样的呢?

  1. // 已省略部分无效值
  2. <ref *1> NodePath {
  3. contexts: [
  4. TraversalContext {
  5. queue: [Array],
  6. priorityQueue: [],
  7. ...
  8. }
  9. ],
  10. state: undefined,
  11. opts: {
  12. enter: [ [Function: onStartVist] ],
  13. exit: [ [Function: onEndVist] ],
  14. _exploded: true,
  15. _verified: true
  16. },
  17. _traverseFlags: 0,
  18. skipKeys: null,
  19. parentPath: null,
  20. container: Node {
  21. type: 'File',
  22. errors: [],
  23. program: Node {
  24. type: 'Program',
  25. sourceType: 'module',
  26. interpreter: null,
  27. body: [Array],
  28. directives: []
  29. },
  30. comments: []
  31. },
  32. listKey: undefined,
  33. key: 'program',
  34. node: Node {
  35. type: 'Program',
  36. sourceType: 'module',
  37. interpreter: null,
  38. body: [ [Node], [Node] ],
  39. directives: []
  40. },
  41. type: 'Program',
  42. parent: Node {
  43. type: 'File',
  44. errors: [],
  45. program: Node {
  46. type: 'Program',
  47. sourceType: 'module',
  48. interpreter: null,
  49. body: [Array],
  50. directives: []
  51. },
  52. comments: []
  53. },
  54. hub: undefined,
  55. data: null,
  56. context: TraversalContext {
  57. queue: [ [Circular *1] ],
  58. priorityQueue: [],
  59. ...
  60. },
  61. scope: Scope {
  62. uid: 0,
  63. path: [Circular *1],
  64. block: Node {
  65. type: 'Program',
  66. sourceType: 'module',
  67. interpreter: null,
  68. body: [Array],
  69. directives: []
  70. },
  71. ...
  72. }
  73. }

是不是发现,这一个遍历怎么这么多东西?太长了,那么我们进行省略,只看关键部分:

  1. // 第1次
  2. <ref *1> NodePath {
  3. listKey: undefined,
  4. key: 'program',
  5. node: Node {
  6. type: 'Program',
  7. sourceType: 'module',
  8. interpreter: null,
  9. body: [ [Node], [Node] ],
  10. directives: []
  11. },
  12. type: 'Program',
  13. }

我们可以看出是直接进入到了程序program结点。 对应的 AST 结点信息:

  1. program: {
  2. type: 'Program',
  3. sourceType: 'module',
  4. interpreter: null,
  5. body: [
  6. [Node]
  7. [Node]
  8. ],
  9. },

接下来,我们继续打印输出的结点信息,我们可以看出它访问的是program.body结点。

  1. // 第2次
  2. <ref *2> NodePath {
  3. listKey: 'body',
  4. key: 0,
  5. node: Node {
  6. type: 'VariableDeclaration',
  7. declarations: [ [Node] ],
  8. kind: 'const'
  9. },
  10. type: 'VariableDeclaration',
  11. }
  12. // 第3次
  13. <ref *1> NodePath {
  14. listKey: 'declarations',
  15. key: 0,
  16. node: Node {
  17. type: 'VariableDeclarator',
  18. id: Node {
  19. type: 'Identifier',
  20. name: 'me'
  21. },
  22. init: Node {
  23. type: 'StringLiteral',
  24. extra: [Object],
  25. value: '我'
  26. }
  27. },
  28. type: 'VariableDeclarator',
  29. }
  30. // 第4次
  31. <ref *1> NodePath {
  32. listKey: undefined,
  33. key: 'id',
  34. node: Node {
  35. type: 'Identifier',
  36. name: 'me'
  37. },
  38. type: 'Identifier',
  39. }
  40. // 第5次
  41. <ref *1> NodePath {
  42. listKey: undefined,
  43. key: 'init',
  44. node: Node {
  45. type: 'StringLiteral',
  46. extra: { rawValue: '我', raw: "'我'" },
  47. value: '我'
  48. },
  49. type: 'StringLiteral',
  50. }
  • node当前结点
  • parentPath父结点路径
  • scope作用域
  • parent父结点
  • type当前结点类型

现在我们可以看出这个访问的规律了,他会一直找当前结点node属性,然后进行层层访问其内容,直到将 AST 的所有结点遍历完成。

这里一定要区分NodePath和Node两种类型,比如上面:pt是属于NodePath类型,pt.node才是Node类型。

其次,我们看到提供的方法除了进入 [enter]还有退出 [exit]方法,这也就意味着,每次遍历一次结点信息,也会退出当前结点。这样,我们就有两次机会获得所有的结点信息。

当我们遍历结束,如果找不到对应的结点信息,我们还可以进行额外的操作,进行代码结点补充操作。结点完整访问流程如下:

  • 进入>Program
    • 进入>node.body[0]
      • 进入>node.declarations[0]
        • 进入>node.id
        • 退出<node.id
        • 进入>node.init
        • 退出<node.init
      • 退出<node.declarations[0]
    • 退出<node.body[0]
    • 进入>node.body[1]
      • ...
      • ...
    • 退出<node.body[1]
  • 退出<Program

3. @babel/types

有了前面的铺垫,我们通过解析,获得了相关的 AST 对象。通过不断遍历,我们拿到了相关的结点,这时候我们就可以开始改造了。@babel/types 就提供了一系列的判断方法,以及将普通对象转换为 AST 结点的方法。

比如,我们想把代码转换为:

  1. // 改造前代码
  2. const me = "我"
  3. function write() {
  4. console.log("文章")
  5. }
  6. // 改造后的代码
  7. let you = "你"
  8. function write() {
  9. console.log("文章")
  10. }

首先,我们要分析下,这个代码改了哪些内容?

  1. 变量声明从const改为let
  2. 变量名从me改为you
  3. 变量值从"我"改为"你"

那么我们有两种替换方式:

  • 方案一:整体替换,相当于把program.body[0]整个结点进行替换为新的结点。
  • 方案二:局部替换,相当于逐个结点替换结点内容,即:program.body.kind,program.body[0].declarations[0].id,program.body[0].declarations[0].init。

借助@babel/types我们可以这么操作,一起看看区别:

  1. const bbt = require('@babel/types');
  2. const { default: traverse } = require('@babel/traverse');
  3. // 进入结点
  4. const onEnter = p => {
  5. // 方案一,全结点替换
  6. if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
  7. // 直接替换为新的结点
  8. p.replaceWith(
  9. bbt.variableDeclaration('let', [
  10. bbt.variableDeclarator(bbt.identifier('you'),
  11. bbt.stringLiteral('你')),
  12. ]),
  13. );
  14. }
  15. // 方案二,单结点逐一替换
  16. if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {
  17. // 替换声明变量方式
  18. p.node.kind = 'let';
  19. }
  20. if (bbt.isIdentifier(p.node) && p.node.name == 'me') {
  21. // 替换变量名
  22. p.node.name = 'you';
  23. }
  24. if (bbt.isStringLiteral(p.node) && p.node.value == '我') {
  25. // 替换字符串内容
  26. p.node.value = '你';
  27. }
  28. };
  29. traverse(codeAST, { enter: onEnter });

我们发现,不仅可以进行整体结点替换,也可以替换属性的值,都能达到预期效果。

当然 我们不仅仅可以全部遍历,我们也可以只遍历某些属性,比如VariableDeclaration,我们就可以这样进行定义:

  1. traverse(codeAST, {
  2. VariableDeclaration: function(p) {
  3. // 只操作类型为 VariableDeclaration 的结点
  4. p.node.kind = 'let';
  5. }
  6. });

@babel/types提供大量的方法供使用,可以通过官网查看。对于@babel/traverse返回的可用方法,可以查看 ts 定义:
babel__traverse/index.d.ts 文件。

常用的方法:p.stop()可以提前终止内容遍历, 还有其他的增删改查方法,可以自己慢慢摸索使用!它就是一个树结构,我们可以操作它的兄弟结点,父节点,子结点。

4. @babel/generator

完成改造以后,我们需要把 AST 再转换回去,这时候我们就需要用到 @babel/generator 工具。只拆不组装,那是二哈【狗头】。能装能组,才是一个完整工程师该干的事情。

废话不多说,上代码:

  1. const fs = require('fs-extra');
  2. const { default: generate } = require('@babel/generator');
  3. // 生成代码实例
  4. const codeIns = generate(codeAST, { retainLines: true, jsescOption: { minimal: true } });
  5. // 写入文件内容
  6. fs.writeFileSync('./code/app/index.js', codeIns.code);

配置项比较多,大家可以参考具体的说明,按照实际需求进行配置。

这里特别提一下:jsescOption: { minimal: true }这个属性,主要是用来保留中文内容,防止被转为unicode形式。

Babel AST 实践

嘿嘿~ 都到这里了,大家应该已经能够上手操作了吧!

什么?还不会,那再把 1 ~ 4 的步骤再看一遍。慢慢尝试,慢慢修改,当你发现其中的乐趣时,这个 AST 的改造也就简单了,并不是什么难事。

留个课后练习:

  1. // 改造前代码
  2. const me = "我"
  3. function write() {
  4. console.log("文章")
  5. }
  6. // 改造后的代码
  7. const you = "你"
  8. function write() {
  9. console.log("文章")
  10. }
  11. console.log(you, write())

大家可以去尝试下,怎么操作简单的 AST 实现代码改造!写文章不易,大家记得一键三连哈~

AST 应用是非常广泛,再来回忆下,这个 AST 可以干嘛?

  1. 代码转换领域,如:ES6 转 ES5, typescript 转 js,Taro 转多端编译,CSS预处理器等等。
  2. 模版编译领域,如:React JSX 语法,Vue 模版语法 等等。
  3. 代码预处理领域,如:代码语法检查(ESLint),代码格式化(Prettier),代码混淆/压缩(uglifyjs) 等等
  4. 低代码搭建平台,拖拽组件,直接通过 AST 改造生成后的代码进行运行。

下一期预告

《带你揭开神秘的Javascript AST面纱之手写一个简单的 Javascript 编译器》

原文链接:https://www.cnblogs.com/Jcloud/p/17308807.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号