翼度科技»论坛 编程开发 JavaScript 查看内容

带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法

3

主题

3

帖子

9

积分

新手上路

Rank: 1

积分
9
作者:京东零售 周明亮
写在前面

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

  • 分析器
  • 抽象语法树 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]方法,这也就意味着,每次遍历一次结点信息,也会退出当前结点。这样,我们就有两次机会获得所有的结点信息。
当我们遍历结束,如果找不到对应的结点信息,我们还可以进行额外的操作,进行代码结点补充操作。结点完整访问流程如下:
<ul>进入>Program<ul>
进入>node.body[0]<ul>
进入>node.declarations[0]<ul>
进入>node.id
退出node.init
退出

举报 回复 使用道具