乐趣区

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

作者:京东批发 周亮堂

写在后面

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

  • 分析器
  • 形象语法树 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');
// 转换字节 Buffer
const 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 编译器》

退出移动版