AST 抽象语法树无处不在-第一回合


简介:
抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。

AST 抽象语法树,似乎我们平时并不会接触到。实际上在我们的项目当中,CSS 预处理,JSX 亦或是 TypeScript 的处理,代码格式化美化工具,Eslint, Javascript 转译,代码压缩,Webpack, Vue-Cli,ES6 转 ES5,当中都离不开 AST 抽象语法树这个绿巨人。先卖个关子,让我们看一下 AST 到的官方解释:

It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code.

中文的解释有:
抽象语法树(abstract syntax tree或者缩写为 AST ),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。和抽象语法树相对的是具体语法树(concrete syntaxtree),通常称作分析树(parse tree)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦 AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。
抽象语法树,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
通过操作这棵树,可以精确的定位到声明、赋值、运算语句,从而实现对代码的优化、变更等操作。这些并不是我们想要看到的。

对于 AST 面纱的神秘感,似乎已经将要揭开。不错,在我刚接触到他的时候,同样感觉确实是难。但是当你开始了解了它以后,你就会越来越喜欢它。
因为他实在太强大了。AST 的水深是毋庸置疑的。但是,这才能更加激起我们探索他的欲望。

一、 一探究竟 AST

通过 astexplorer [1] (AST树查看网站),通过他你可以方便的查看代码的语法树,挑你喜欢的库。你可以在线把玩 AST,而且除了 JavaScript,HTML, CSS 还有很多其它语言的 AST 库,让我们对他有一个感性而直观的认识。
请看下图,看看AST语法树长什么样子:


此图看到的是一个 ExportDefaultDeclaration 也就是export default {},还有他的位置信息,注释失信,tokens等等。

国际惯例,先来一个小demo
输入数据
1
2
3
function square(n) {
return n * n;
}
处理数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
astFn() {
const code = `function square(n) {
return n * n;
}`;
//得到 AST 语法树
const ast = babylon.parse(code);

traverse(ast, {
enter(path) {
console.log("enter: " + path.node.type);
//如下的语句匹配到 return 中的 n 与参数 n,并且将它替换为 x。
if (path.node.type === "Identifier" && path.node.name === "n") {
path.node.name = "x";
}
}
});
generate(ast, {}, code);//将 AST 转换为代码
console.log(generate(ast, {}, code).code );//打印出转换后的 JavaScript 代码
}
输出数据
1
2
3
function square(x) {
return x * x;
}

我们看一下我们得到的 AST 树


二、 简单粗暴的版本

没有使用 AST 将 VUE 组件转换成小程序组件的简易版本介绍
下方是两段代码,简单的逻辑,实现思路,匹配目标字符串,替换字符,然后生成文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
regs:{//通过标签匹配来替换对应的小程序支持的标签
toViewStartTags: /(<h1)|(<s)|(<em)|(<ul)|(<li)|(<dl)|(<i)|(<span)/g,
toViewEndTags: /(<\/h1>)|(<\/s>)|(<\/em>)|(<\/ul>)|(<\/li>)|(<\/dl>)|(<\/i>)|(<\/span>)/g,
toBlockStartTags: /(<div)|(<p)/g,
toBlockEndTags: /(<\/div>)|(<\/p>)/g,
},
signObj: {//通过标签查找来分离脚本,模板和CSS
tempStart: '<template>',
tempEnd: '</template>',
styleStart: '<style scoped>',
styleEnd: '</style>',
scriptStart: '<script>',
scriptEnd: '</script>'
}

上方是正则版本的一些模板匹配规则,经过后续的一系列处理把一个 VUE组件处理得到对应的小程序的 WXML ,WXSS,JSON,JS,4个文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//文件
const wxssFilePath = path.join(dirPath, `${mpFile}.wxss`);
const jsFilePath = path.join(dirPath, `${mpFile}.js`);
const wxmlFilePath = path.join(dirPath, `${mpFile}.wxml`);
const jsonFilePath = path.join(dirPath, `${mpFile}.json`);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
fs.writeFile(wxssFilePath, wxssContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxss文件成功`);
fs.writeFile(jsFilePath, jsContent, err => {
if (err) throw err;
// console.log(`dist目录下生成${mpFile}.js文件成功`);
fs.writeFile(wxmlFilePath, wxmlContent, err => {
if (err) throw err;
//console.log(`dist目录下生成${mpFile}.wxml文件成功`);
fs.writeFile(jsonFilePath, jsonContent, err => {
if (err) throw err;
console.log(`dist目录下生成${mpFile}.json文件成功`)
resolve(`生成${mpFile}.json文件成功`);
})
})
});
});

上方是处理得到的 WXML ,WXSS,JSON,JS,4个文件,并且生成到对应的目录下。代码实现用时较短,后续更改方案,并没有做优化,这里就不做详细展开讨论这个实现方案了。

三、 AST 三大法宝

Babel 是 JavaScript 编译器,更确切地说是源码到源码的编译器,通常也叫做“ 转换编译器(transpiler)”。 意思是说你为 Babel 提供一些 JavaScript 代码,Babel 更改这些代码,然后返回给你新生成的代码。

babel-core:Babel 的编译器;它暴露了 babel.transform 方法。

[1] babylon:Babylon 是 Babel 的解析器。用于生成 AST 语法树。

[2] babel-traverse:Babel 的遍历器,所有的transformers都使用该工具遍历所有的 AST (抽象语法树),维护了整棵树的状态,并且负责替换、移除和添加节点。我们可以和 Babylon 一起使用来遍历和更新节点。

[3] babel-generator:Babel 的代码生成器。它读取AST并将其转换为代码。

整个编译器就被分成了三部分:分析器、转换器、生成器,大致的流程是:

输入字符串 -> babylon分析器 parse -> 得到 AST -> 转换器 -> 得到 AST -> babel-generator -> 输出

总结核心三步:

AST 三大法宝
babylon.parse => traverse 转换 AST => generate(ast, {}, code).code) 生成

感兴趣的童鞋,可以在网上或者看参考资料都有介绍,这里篇幅有限,感兴趣我们下回详细展开。

四、 AST 到底怎么转换呢?

使用 AST 将 VUE 组件转换为小程序组件的部分实现。
今天我们主要分析 JavaScript 的部分处理。

转换之前的 VUE 组件代码 Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
//组件的对外属性
props: {
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num:10000
}
},
//组件的方法
methods: {
textFn() {
this.num = 2
},
onMyButtonTap: function(){
this.num = 666
},
}
}
处理后我们得到的小程序组件 JavaScript 部分代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
export default {
properties: {
//组件的对外属性
max: {
type: Number,
value: 99
}
},
//组件的内部数据
data(){
return {
num: 10000
}
},
//组件的方法
methods: {
textFn() {
this.setData({
num: 2
});
},
onMyButtonTap: function () {
this.setData({
num: 666
});
}
}
};
我们对js动了什么手脚(亦可封装成babel插件):
1
2
3
4
5
//to AST
const ast = babylon.parse(code, {
sourceType: "module",
plugins: ["flow"]
});
1
2
3
4
5
6
7
//AST 转换 node,nodetype很关键
traverse(ast, {
enter(path) {
//打印出node.type
console.log("enter: " + path.node.type);
}
})
1
2
3
4
5
6
ObjectProperty(path) {
//props 替换为 properties
if (path.node.key.name === "props") {
path.node.key.name = "properties";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//修改methods中使用到数据属性的方法中。this.prop至this.data.prop等 与 this.setData的处理  感兴趣的童鞋可以尝试一下,代码仅供参考,提供一个思路,实际使用请详细阅读相关的文档来处理。
MemberExpression(path) {
let datasVals = datas.map((item,index) =>{
return item.key.name //拿到data属性中的第一级
})
if (//含有this的表达式 并且包含data属性
path.node.object.type === "ThisExpression" &&
datasVals.includes(path.node.property.name)
) {
path.get("object").replaceWithSourceString("this.data");
//判断是不是赋值操作
if (
(t.isAssignmentExpression(path.parentPath) &&
path.parentPath.get("left") === path) ||
t.isUpdateExpression(path.parentPath)
) {
const expressionStatement = path.findParent(parent =>
parent.isExpressionStatement()
);
//篇幅有限 省略部分
}
}
}
转换之前的js代码的部分 AST 树:


具体的 API 使用,童鞋们看一下babel相关的文档了解一下,一回生,二回熟嘛。

五、总结:

通过以上我们的介绍,我们大概对抽象语法树有了初步的了解。总体思路是:我们用Babel的解析器 把 JavaScript 源码转化为抽象语法树,
再通过 Babel 的遍历器遍历 AST (抽象语法树),替换、移除和添加节点,得到一个新的 AST 树。最后, 使用,Babel 的代码生成器 Babel Generator模块 读取 处理后的 AST 并将其转换为代码。任务就完成了!

本文对于 VUE 组件转换为小程序组件的转换部分包括如下内容:

  1. VUE 组件对外属性转换为小程序对外属性的处理
  2. VUE 组件内部数据的转换为小程序内部数据的处理
  3. VUE 组件中 methods 中的赋值语句转换为小程序赋值语句的处理简单介绍
    具体转换过程中涉及到的问题还有很多,有机会再做展开!
    感谢大家的耐心阅读,希望本文对大家有所帮助,在技术探索的路上,我们一往无前。
    也欢迎大家关注【全栈探索公众号】,每周都会有技术好文推出!

扩展阅读

[1] https://astexplorer.net/

[2] https://babeljs.io/docs/en/next/babel-types.html#objectexpression

[3] https://github.com/babel/babel/tree/6.x/packages/babel-types

[4] http://esprima.org/demo/parse.html#

[5] https://segmentfault.com/a/1190000014178462?utm_source=tag-newest

[6] https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E8%AA%9E%E6%B3%95%E6%A8%B9

[7] https://itnext.io/ast-for-javascript-developers-3e79aeb08343