1. 抽象语法树(Abstract Syntax Tree)

webpackLint等很多的工具和库的核心都是通过 Abstract Syntax Tree 抽象语法数这个概念来实现对代码的检查、分析等操作的。通过了解抽象语法数这个概念,你也可以随手编写类似的工具

2. 抽象语法树用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • 如JSLint、JSHint对代码错误或风格的检查,发现一些潜在的错误
    • IDE的错误提示、格式化、高亮、自动补全等等
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

3. 抽象语法树定义

这些工具的原理都是通过JavaScript Parser把代码转化为一颗抽象语法树(AST),这颗树定义了代码的结构,通过操纵这棵树,我们可以精准的走位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作

在计算机科学中,抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。

Javascript的语法是为了给开发者更好的编程而设计的,但是不适合程序的理解。所以需要转化为AST来更适合程序分析,浏览器编译器一般会把源码转化为AST来进行进一步的分析等其他操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var AST = "is Tree";


{
"type": "Program",
"body": [{
"type": "variableDeclaration",
"kind": "var",
"declaration": [{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "AST"
},
"init": {
"type": "Literal",
"value": "is tree",
"raw": "\"is Tree\""
}
}]
}]
}

4. JavaScript Parser

  • JavaScript Parser,把js源码转化为抽象语法树的解析器。
  • 浏览器会把js源码通过解析器转化为抽象语法树,再进一步转化为字节码或直接生成机器码。
  • 一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准

    4.1 常用的JavaScript Parser有:

  • esprima
  • traceur
  • acorn
  • shift

    4.2 esprima

  • 通过 esprima 把源码转化为AST
  • 通过 estraverse 遍历并更新AST
  • 通过 escodegen 将AST重新生成源码
  • astexplorer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let estraverse = require('estraverse')
    let escodegen = require('escodegen')
    let code = "function ast(){}";
    let ast = esprima.parse(code);
    console.log('ast', ast)
    estraverse.traverse(ast, {
    enter(node){
    console.log('enter', node.type)
    if (node.type === 'Identifier') {
    node.name += '_enter'
    }
    },
    leave(node){
    console.log('leave', node.type)
    if (node.type === 'Identifier') {
    node.name += '_leave'
    }
    }
    })
    console.log('ast', ast)
    let result = escodegen.generate(ast)
    console.log('result', result)

5. 转换箭头函数

  • 访问者模式Visitor对于某个对象或者一组对象,不同的访问者,产生的结果不同,执行操作也不同
  • babel-core
  • babel-types
  • Babel 插件手册
    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
    // babel核心库,用来实现核心的转换引擎的
    let babel = require('babel-core')
    // 可以实现类型判断,生成ast零部件
    let types = require('babel-types')

    let code = `let sum = (a,b) =>a+b` // let sum = function(a,b){return a+b}
    // 这个访问者可以对特定的类型的节点进行处理
    let visitor = {
    ArrowFunctionExpression(path){
    let params = path.node.params;
    let blockStatement = types.blockStatement([
    types.returnStatement(path.node.body)
    ])
    let func = types.functionExpression(null, params, blockStatement, false, false);
    path.replaceWith(func);
    }
    }

    let arrayPlugin = {visitor}
    // 在babel内部先把代码转成AST,然后在进行遍历,
    let result = babel.transform(code,{
    plugins: [
    arrayPlugin
    ]
    })
    console.log(result.code)

6. babel插件

预计算简单表达式的插件

1
const result = 1 + 2;
1
const result = 3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let code = `const result = 1 + 2`
let babel = require('babel-core')
let types = require('babel-types')
// 预计算
let visitor = {
BinaryExpression(path){
let node = path.node;
if (!isNaN(node.left.value) && !isNaN(node.right.value)) {
let result = eval(node.left.value + node.operator + node.right.value)
result = types.numericLiteral(result)
path.replaceWith(result)
}
}
}
let result = babel.transform(code, {
plugins: [
{visitor}
]
})
console.log(result.code)

7. webpack babel插件

7.1 实现按需加载

  • Lodash
    1
    import { join, flatten } from 'ladash

转换为

1
2
import join from "lodash/join"
import flatten from "lodash/flatten"

7.2 babel配置

  • transform-runtime Babel默认只转换新的 JavaScript 语法,而不是转换新的 API。例如,literator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转义,启用插件babel-plugin-transform-runtime后,Babel就会使用babel-runtime下的工具函数
    1
    2
    3
    4
    5
    6
    7
    {
    "presets": ["react", "stage-0", "env"],
    "plugins": [
    ["extract", {"library": "lodash"}],
    ["transform-runtime", {}]
    ]
    }

编译顺序为首先 plugins 从左往右,然后 presets 从右往左

7.3 babel插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let babel = require('babel-core');
let types = require('babel-types');
//只会处理ImportDeclaration
let visitor = {
ImportDeclaration(path,ref={options:{}}){
let node = path.node;
let specifiers = node.specifiers;
if(options.library == node.local.name && !types.isImportDefaultSpecifier(specifiers[0])){
let newImports = specifiers.map(specifier=>(
types.importDeclaration([types.ImportDefaultSpecifier(specifier.local)],
types.stringLiteral(`${node.source.value}/${specifier.local.name}`))
));
path.replaceWithMultiple(newImports);
}
}
}
let code = "import {flatten,join} from 'lodash';";
let r = babel.transform(code,{
plugins:[
{visitor}
]
});
console.log(r.code);