JavaScrpit AST 实战

前言

每个编程语言都有自己的AST,了解AST并能进行一些开发,会给我们的项目开发提供很大的便利。下面就带大家一探究竟

通过本文能了解到什么

  • JS AST结构和属性
  • babel插件开发

JS AST简介

AST也就是抽象语法树。简单来说就是把程序用树状形式展现。

每种语言(HTML,CSS,JS等)都有自己的AST,而且还有多种AST解析器。

回归JS本身,常见的AST解析器有:

  • acorn
  • @babel/parser
  • Typescript
  • Uglify-js
  • 等等

不同解析器解析出来的AST有些许差异,但本质上是一样的。

本文将基于@babel/parser来进行示例和讲解

下面来看一句常见的代码

1
import ajax from 'axios'

转换后的AST结构如下:

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
{
"type": "ImportDeclaration",
"start": 0,
"end": 24,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 1,
"column": 24
}
},
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 7,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 11
}
},
"local": {
"type": "Identifier",
"start": 7,
"end": 11,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 11
},
"identifierName": "ajax"
},
"name": "ajax"
}
}
],
"importKind": "value",
"source": {
"type": "StringLiteral",
"start": 17,
"end": 24,
"loc": {
"start": {
"line": 1,
"column": 17
},
"end": {
"line": 1,
"column": 24
}
},
"extra": {
"rawValue": "axios",
"raw": "'axios'"
},
"value": "axios"
}
}

内容是不是比想象的多?莫慌,我们一点一点看。

来一张简略图:

ImportDeclaration

语句的类型,表明是一个import的声明。

常见的有:

  • VariableDeclaration:var x = ‘init’
  • FunctionDeclaration:function func(){}
  • ExportNamedDeclaration:export function exp(){}
  • IfStatement:if(1>0){}
  • WhileStatement:while(true){}
  • ForStatement:for(;;){}
  • 不一一列举

既然是一个引入表达式,自然分左右两部分,左边的是specifiers,右边的是source

specifiers

specifiers节点会有一个列表来保存specifier

如果左边只声明了一个变量,那么会给一个ImportDefaultSpecifier

如果左边是多个声明,就会是一个ImportSpecifier列表

什么叫左边有多个声明?看下面的示例

1
import {a,b,c} from 'x'

变量的声明要保持唯一性

而Identifier就是鼓捣这个事情的

source

source包含一个字符串节点StringLiteral,对应了引用资源所在位置。示例中就是axios

AST是如何转换出来的呢?

以babel为例子:

1
2
3
4
5
6
7
8
9
10
const parser = require('@babel/parser')
let codeString = `
import ajax from 'axios'
`;


let file = parser.parse(codeString,{
sourceType: "module"
})
console.dir(file.program.body)

在node里执行一下,就能打印出AST

通过这个小示例,大家应该对AST有个初步的了解,下面我们谈谈了解它有什么意义

应用场景以及实战

实际上,我们在项目中,AST技术随处可见

  • Babel对es6语法的转换
  • Webpack对依赖的收集
  • Uglify-js对代码的压缩
  • 组件库的按需加载babel-plugin
  • 等等

为了更好的理解AST,我们定义一个场景,然后实战一下。

场景:把import转换成require,类似于babel的转换

目标:通过AST转换,把语句

1
import ajax from 'axios'

转为

1
var ajax = require('axios')

要达到这个效果,首先我们要写一个babel-plugin。先上代码

babelPlugin.js代码如下:

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
const t = require('@babel/types');

module.exports = function babelPlugin(babel) {
function RequireTranslator(path){
var node = path.node
var specifiers = node.specifiers
//获取变量名称
var varName = specifiers[0].local.name;
//获取资源地址
var source = t.StringLiteral(path.node.source.value)
var local = t.identifier(varName)
var callee = t.identifier('require')
var varExpression = t.callExpression(callee,[source])
var declarator = t.variableDeclarator(local, varExpression)
//创建新节点
var newNode = t.variableDeclaration("var", [declarator])
//节点替换
path.replaceWith(newNode)
}
return {
visitor: {
ImportDeclaration(path) {
RequireTranslator.call(this,path)
}
}
};
};

测试代码:

1
2
3
4
5
6
7
8
9
10
const babel = require('@babel/core');
const babelPlugin = require('./babelPlugin')


let codeString = `
import ajax from 'axios'
`;
const plugins = [babelPlugin]
const {code} = babel.transform(codeString,{plugins:plugins});
console.dir(code)

输出结果:

1
'var ajax = require("axios");'

目标达成!

babel-plugin

在babel的官网有开发文档,这里只是简单的描述一下注意要点:

插件要求返回一个visitor对象。

可以拦截所有的节点,函数名称就是节点类型,入参是path,可以通过path.node来获取当前节点

@babel/types提供了大量节点操作的API,同样可以在官网看的详细的说明

transform

这里的代码大家是不是看着很熟悉。没错,就是.babelrc里的配置。我们开发的插件,配置到.babelrc的plugins里,就可以全局运行了。

写在最后

JS的AST,给我们提供了实现各种可能得机会。我们可以自定义一个语法,可以将组件的按需引入过程简化等等。同时不仅仅是JS,CSS,HTML,SQL都可以在ast语法级别去进行一些有趣的操作。该篇文章只是带大家简单入门。写在最后:前端不仅仅是UI,可玩的东西还有很多

参考文章

  • 版权声明: 本博客所有文章,未经许可,任何单位及个人不得做营利性使用!如有侵权请联系作者。
  • Copyrights © 2015-2020 翟天野

请我喝杯咖啡吧~