作者:京东零售 周明亮

写在前面

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

  • 分析器
  • 抽象语法树 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 编译器》

带你揭开神秘的Javascript AST面纱之Babel AST 四件套的使用方法的更多相关文章

  1. 云小课|带你揭开IP地址的神秘身份

    摘要:本文带你了解网络云产品和相关的知识内容. 华为云网络服务大家族提供了丰富的云产品,可以满足用户的各种网络互联需求.相应地,华为云帮助中心也贴心的奉上了你想了解的所有网络云产品知识. 可是小课最近 ...

  2. JavaScript获取客户端计算机硬件及系统等信息的方法

    JavaScript获取客户端计算机硬件及系统等信息的方法 JavaScript 获取客户端计算机硬件及系统信息 通过WMI来实现获取客户端计算机硬件及系统信息: function getSysInf ...

  3. Javascript获取数组中的最大值和最小值的方法汇总

    比较数组中数值的大小是比较常见的操作,下面同本文给大家分享四种放哪广发获取数组中最大值和最小值,对此感兴趣的朋友一起学习吧   比较数组中数值的大小是比较常见的操作,比较大小的方法有多种,比如可以使用 ...

  4. Atitit. 构造ast 语法树的总结attilax oao 1. Ast结构树形12. ast view (自是个160k的jar )22.1. 多条语句ast结构22.2. 变量定义 int b,c; 的ast结构22.3. 方法调用meth1(a=1,b=2,c=3);  的ast结构23. 误解的问题33.1. 语法书子能是个二叉树,实际上多叉树越好..33.2. 非要不个ast放到个s

    Atitit. 构造ast 语法树的总结attilax oao 1. Ast结构树形1 2. ast view (自是个160k的jar )2 2.1. 多条语句ast结构2 2.2. 变量定义 in ...

  5. JavaScript从数组中删除指定值元素的方法

    本文实例讲述了JavaScript从数组中删除指定值元素的方法.分享给大家供大家参考.具体分析如下: 下面的代码使用了两种方式删除数组的元素,第一种定义一个单独的函数,第二种为Array对象定义了一个 ...

  6. Javascript定义类(class)的三种方法

    将近20年前,Javascript诞生的时候,只是一种简单的网页脚本语言.如果你忘了填写用户名,它就跳出一个警告. 如今,它变得几乎无所不能,从前端到后端,有着各种匪夷所思的用途.程序员用它完成越来越 ...

  7. Atitti.java exp ast java表达式语法ast构造器

    Atitti.java exp ast java表达式语法ast构造器 /atiplat_cms/src/com/attilax/lang/AstParser.java 原理 分割tokens_sli ...

  8. 网页绘制图表 Google Charts with JavaScript #2 ....与ASP.NET网页结合 (ClientScriptManager.RegisterStartupScript 方法)

    此为文章备份,原文出处(我的网站) 网页绘制图表 Google Charts with JavaScript #2 ....与ASP.NET网页结合 (ClientScriptManager.Regi ...

  9. 【转】带checkbox的ListView实现(二)——自定义Checkable控件的实现方法

    原文网址:http://blog.csdn.net/harvic880925/article/details/40475367 前言:前一篇文章给大家展示了传统的Listview的写法,但有的时候我们 ...

  10. JavaScript中正则表达式判断匹配规则以及常用的方法

    JavaScript中正则表达式判断匹配规则以及常用的方法: 字符串是编程时涉及到的最多的一种数据结构,对字符串进行操作的需求几乎无处不在. 正则表达式是一种用来匹配字符串的强有力的武器.它的设计思想 ...

随机推荐

  1. openstack:OpenStack架构详解,

    OpenStack既是一个社区,也是一个项目和一个开源软件,提供开放源码软件,建立公共和私有云,它提供了一个部署云的操作平台或工具集,其宗旨在于:帮助组织运行为虚拟计算或存储服务的云,为公有云.私有云 ...

  2. 【线性DP】乌龟棋

    #include<bits/stdc++.h> using namespace std; int a[400],b[5]; int dp[50][50][50][50]; int main ...

  3. 树莓派 wiringPi的BCM与BOARD编码

    一.基础命令使用wiringPi库 1.1.获取管教信息 gpio readall ---获取管脚信息   1.2.BOARD编码和BCM一般都在python库中使用 import RPi.GPIO ...

  4. day47-Mysql初识

    1.数据库的演变过程-- 文件存储(不同用户之间数据格式不一致,杂乱)==> 软件开发目录规范(限定了储存的具体位置,不能网络通信)==>数据库 数据库就是一款基于网络通信操作文件的应用程 ...

  5. Kubernetes(k8s)pod详解

    一.简介 在Kubernetes集群中,Pod是所有业务类型的基础,也是K8S管理的最小单位级,它是一个或多个容器的组合.这些容器共享存储.网络和命名空间,以及如何运行的规范.在Pod中,所有容器都被 ...

  6. 121、商城业务---订单服务---rabbitmq消息积压、丢失、重复等解决方案

  7. python计算三角形的三个边的边长,首先计算两个坐标点的距离

    # -*- coding:utf-8 -*- """ @author: 14931 @file: trianglearea.py @time: 2023/02/14 @d ...

  8. 加密脚本分析—evil.py

    加密脚本分析-evil.py 1.题目 源文件 一共两个文件 enc_flag.txt evil.py(原文件无注释) 1 # coding: utf-8 2 3 import base64 4 im ...

  9. 这样封装echarts简单好用

    为什么要去封装echarts? 在我们的项目中,有很多的地方都使用了echarts图表展示数据. 在有些场景,一个页面有十多个的echarts图. 这些echarts只是展示的指标不一样. 如果我们每 ...

  10. 全网最详细中英文ChatGPT-GPT-4示例文档-从0到1快速入门解析非结构化数据应用——官网推荐的48种最佳应用场景(附python/node.js/curl命令源代码,小白也能学)

    目录 Introduce 简介 setting 设置 Prompt 提示 Sample response 回复样本 API request 接口请求 python接口请求示例 node.js接口请求示 ...