作者:京东批发 周亮堂

写在后面

这里咱们初步提到了一些根底概念和利用:

  • 分析器
  • 形象语法树 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文件中代码内容,咱们能够应用如下代码:

const { parse } = require("@babel/parser")// 读取文件内容const fileBuffer = fs.readFileSync('./code/app/index.tsx', 'utf8');// 转换字节 Bufferconst fileCode = fileBuffer.toString();// 解析内容转换为 AST 对象const codeAST = parse(fileCode, {  // parse in strict mode and allow module declarations  sourceType: "module",  plugins: [    // enable jsx and typescript syntax    "jsx",    "typescript",  ],});

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

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

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

通过上述的代码转换,咱们就能够失去一个规范的 AST 对象。在上一篇文章中,已做详细分析,在这里不在开展。比方:

// 原代码const me = "我"function write() {  console.log("文章")}// 转换后的 AST 对象const codeAST = {  "type": "File",  "errors": [],  "program": {    "type": "Program",    "sourceType": "module",    "interpreter": null,    "body": [      {        "type": "VariableDeclaration",        "declarations": [          {            "type": "VariableDeclarator",            "id": {              "type": "Identifier",              "name": "me"            },            "init": {              "type": "StringLiteral",              "extra": {                "rawValue": "我",                "raw": "\"我\""              },              "value": "我"            }          }        ],        "kind": "const"      },      {        "type": "FunctionDeclaration",        "id": {          "type": "Identifier",          "name": "write"        },        "generator": false,        "async": false,        "params": [],        "body": {          "type": "BlockStatement",          "body": [            {              "type": "ExpressionStatement",              "expression": {                "type": "CallExpression",                "callee": {                  "type": "MemberExpression",                  "object": {                    "type": "Identifier",                    "computed": false,                    "property": {                      "type": "Identifier",                      "name": "log"                    }                  },                  "arguments": [                    {                      "type": "StringLiteral",                      "extra": {                        "rawValue": "文章",                        "raw": "\"文章\""                      },                      "value": "文章"                    }                  ]                }              }            }          ]        }      }    ]  }}

2. @babel/traverse

当咱们拿到一个规范的 AST 对象后,咱们要操作它,那必定是须要进行树结构遍历。这时候,咱们就会用到 @babel/traverse 。

比方咱们失去 AST 后,咱们能够进行遍历操作:

const { default: traverse } = require('@babel/traverse');// 进入结点const onEnter = pt => {   // 进入以后结点操作   console.log(pt)}// 退出结点const onExit = pe => {  // 退出以后结点操作}traverse(codeAST, { enter: onEnter, exit: onExit })

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

// 已省略局部有效值<ref *1> NodePath {  contexts: [    TraversalContext {      queue: [Array],      priorityQueue: [],      ...    }  ],  state: undefined,  opts: {    enter: [ [Function: onStartVist] ],    exit: [ [Function: onEndVist] ],    _exploded: true,    _verified: true  },  _traverseFlags: 0,  skipKeys: null,  parentPath: null,  container: Node {    type: 'File',    errors: [],    program: Node {      type: 'Program',      sourceType: 'module',      interpreter: null,      body: [Array],      directives: []    },    comments: []  },  listKey: undefined,  key: 'program',  node: Node {    type: 'Program',    sourceType: 'module',    interpreter: null,    body: [ [Node], [Node] ],    directives: []  },  type: 'Program',  parent: Node {    type: 'File',    errors: [],    program: Node {      type: 'Program',      sourceType: 'module',      interpreter: null,      body: [Array],      directives: []    },    comments: []  },  hub: undefined,  data: null,  context: TraversalContext {    queue: [ [Circular *1] ],    priorityQueue: [],    ...  },  scope: Scope {    uid: 0,    path: [Circular *1],    block: Node {      type: 'Program',      sourceType: 'module',      interpreter: null,      body: [Array],      directives: []    },    ...  }}

是不是发现,这一个遍历怎么这么多货色?太长了,那么咱们进行省略,只看要害局部:

// 第1次<ref *1> NodePath {  listKey: undefined,  key: 'program',  node: Node {    type: 'Program',    sourceType: 'module',    interpreter: null,    body: [ [Node], [Node] ],    directives: []  },  type: 'Program',}

咱们能够看出是间接进入到了程序program结点。 对应的 AST 结点信息:

  program: {    type: 'Program',    sourceType: 'module',    interpreter: null,    body: [      [Node]      [Node]    ],  },

接下来,咱们持续打印输出的结点信息,咱们能够看出它拜访的是program.body结点。

// 第2次<ref *2> NodePath {  listKey: 'body',  key: 0,  node: Node {    type: 'VariableDeclaration',    declarations: [ [Node] ],    kind: 'const'  },  type: 'VariableDeclaration',}// 第3次<ref *1> NodePath {  listKey: 'declarations',  key: 0,  node: Node {    type: 'VariableDeclarator',    id: Node {      type: 'Identifier',      name: 'me'    },    init: Node {      type: 'StringLiteral',      extra: [Object],      value: '我'    }  },  type: 'VariableDeclarator',}// 第4次<ref *1> NodePath {  listKey: undefined,  key: 'id',  node: Node {    type: 'Identifier',    name: 'me'  },  type: 'Identifier',}// 第5次<ref *1> NodePath {  listKey: undefined,  key: 'init',  node: Node {    type: 'StringLiteral',    extra: { rawValue: '我', raw: "'我'" },    value: '我'  },  type: 'StringLiteral',}
  • 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 结点的办法。

比方,咱们想把代码转换为:

// 革新前代码const me = "我"function write() {  console.log("文章")}// 革新后的代码let you = "你"function write() {  console.log("文章")}

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

  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咱们能够这么操作,一起看看区别:

const bbt = require('@babel/types');const { default: traverse } = require('@babel/traverse');// 进入结点const onEnter = p => {  // 计划一,全结点替换  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {    // 间接替换为新的结点    p.replaceWith(      bbt.variableDeclaration('let', [        bbt.variableDeclarator(bbt.identifier('you'),                   bbt.stringLiteral('你')),      ]),    );  }  // 计划二,单结点逐个替换  if (bbt.isVariableDeclaration(p.node) && p.listKey == 'body') {    // 替换申明变量形式    p.node.kind = 'let';  }  if (bbt.isIdentifier(p.node) && p.node.name == 'me') {    // 替换变量名    p.node.name = 'you';  }  if (bbt.isStringLiteral(p.node) && p.node.value == '我') {    // 替换字符串内容    p.node.value = '你';  }  };traverse(codeAST, { enter: onEnter });

咱们发现,不仅能够进行整体结点替换,也能够替换属性的值,都能达到预期成果。

当然 咱们不仅仅能够全副遍历,咱们也能够只遍历某些属性,比方VariableDeclaration,咱们就能够这样进行定义:

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

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

罕用的办法:p.stop()能够提前终止内容遍历, 还有其余的增删改查办法,能够本人缓缓摸索应用!它就是一个树结构,咱们能够操作它的兄弟结点,父节点,子结点。

4. @babel/generator

实现革新当前,咱们须要把 AST 再转换回去,这时候咱们就须要用到 @babel/generator 工具。只拆不组装,那是二哈【狗头】。能装能组,才是一个残缺工程师该干的事件。

废话不多说,上代码:

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

配置项比拟多,大家能够参考具体的阐明,依照理论需要进行配置。

这里特地提一下:jsescOption: { minimal: true }这个属性,次要是用来保留中文内容,避免被转为unicode模式。

Babel AST 实际

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

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

留个课后练习:

// 革新前代码const me = "我"function write() {  console.log("文章")}// 革新后的代码const you = "你"function write() {  console.log("文章")}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 编译器》