掌握聚合最新动态了解行业最新趋势
API接口,开发服务,免费咨询服务

剖析 Babel 之 Babel 总览

名词解释

AST:Abstract Syntax Tree, 抽象语法树

DI: Dependency Injection, 依赖注入

===============================================================

Babel的解析引擎

Babel使用的引擎是babylon,babylon并非由babel团队自己开发的,而是fork的acorn项目,acorn的项目本人在很早之前在兴趣部落1.0在构建中使用,为了是做一些代码的转换,是很不错的一款引擎,不过acorn引擎只提供基本的解析ast的能力,遍历还需要配套的acorn-travesal, 替换节点需要使用acorn-,而这些开发,在Babel的插件体系开发下,变得一体化了

Babel的工作过程

Babel会将源码转换AST之后,通过便利AST树,对树做一些修改,然后再将AST转成code,即成源码。

上面提到Babel是fork acon项目,我们先来看一个来自兴趣部落项目的,简单的ACON示例

一个简单的ACON转换示例

解决的问题

1
2
3
4

Model.task('getData', function($scope, dbService){
 
});
 

转换成

1
2
3
4

Model.task('getData', ['$scope', 'dbService', function($scope, dbService){
 
}]);
 

熟悉angular的同学都能看到这段代码做的是对DI的自动提取功能,使用ACON手动撸代码

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
71
72
73
74

var code = 'let a = 1; // ....';
 
var acorn = require("acorn");
var traverse = require("ast-traverse");
var alter = require("alter");
 
var ast = acorn.parse(code);
var ctx = [];
 
traverse(ast, {
    pre: function(node, parent, prop, idx){
        if(node.type === "MemberExpression") {
 
            var object = node.object;
 
            var objectName = object.name;
 
            var property = node.property;
            var propertyName = property.name;
 
            // 这里要进行替换
            if (objectName === "Model" && (propertyName === "service" || propertyName === "task")) {
                // 第一个就为serviceName 第二个是function
                var arg = parent.arguments;
                var serviceName = arg[0];
                var serviceFunc = arg[1];
                for (var i = 0; i < arg.length; i++) {
                    if (arg[i].type === "FunctionExpression") {
                        serviceFunc = arg[i];
 
                        break;
                    }
 
                }
 
                if (serviceFunc.type === "FunctionExpression") {
                    var params = serviceFunc.params;
                    var body = serviceFunc.body;
 
                    var start = serviceFunc.start;
                    var end = serviceFunc.end;
 
                    var funcStr = source.substring(start, end);
 
                    //params里是注入的代码
 
                    var injectArr = [];
                    for (var j = 0; j < params.length; j++) {
                        injectArr.push(params[j].name);
                    }
 
                    var injectStr = injectArr.join('","')
 
                    var replaceString = '["' + injectStr + '", ' + funcStr + ']';
                    if(params.length){
                        ctx.push({
                            start: start,
                            end: end,
                            str: replaceString
                        })
                    }
 
                }
            }
        }
 
 
 
    }
});
var distStr = alter(code, ctx);
console.log(distStr);
 
 

具体的流程如下

可以从上面的过程看到acorn的特点

  • 1.acorn做为一款优秀的源码解析器
  • 2.acorn并不提供对AST树的修改能力
  • 3.acorn并不提供AST树的还原能力
  • 4.修改源码仍然靠源码修改字符串的方式

Babel正是扩展了acorn的能力,使得转换变得更一体化

Babel的前序工作——Babylon、babel-types:code转换为AST

Babel转AST树的过程涉及到语法的问题,转AST树一定有对就的语法,如果在解析过程中,出现了不符合Babel语法的代码,就会报错,Babel转AST的解析过程在Babylon中完成

解析成AST树使用babylon.parse方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14

import babylon from 'babylon';
 
let code = `
     let a = 1, b = 2;
     function sum(a, b){
          return a + b;
     }
 
    sum(a, b);
`;
 
let ast = babylon.parse(code);
console.log(ast);
 

结果如下

AST如下

关于AST树的详细定义Babel有文档

https://github.com/babel/babylon/blob/master/ast/spec.md

关于AST树的定义

1
2
3
4
5

interface Node {
  type: string;
    loc: SourceLocation | null;
}
 

ast中的节点都是继承自Node节点,Node节点有type和loc两个属性,分别代表类型和位置,

其中位置定义如下

1
2
3
4
5
6

interface SourceLocation {
  source: string | null;
  start: Position;
  end: Position;
}
 

位置节点又是由source(源码string), 开始位置,结束位置组成,start,end又是Position类型

1
2
3
4
5

interface Position {
  line: number; // >= 1
  column: number; // >= 0
}
 

节点又包含行号和列号

再看Program的定义

1
2
3
4
5
6
7

interface Program <: Node {
  type: "Program";
  sourceType: "script" | "module";
  body: [ Statement | ModuleDeclaration ];
  directives: [ Directive ];
}
 

Program是继承自Node节点,类型是Program, sourceType有两种,一种是script,一种是module,程序体是一个声明体Statement或者模块声明体ModuleDeclaration节点数组

Babylon支持的语法

Babel或者说Babylon支持的语法现阶段是不可以第三方扩展的,也就是说我们不可以使用babylon做一些奇奇怪的语法,换句话说

不要希望通过babel的插件体系来转换自己定义的语法规则

那么babylon支持的语法有哪些呢,除了常规的js语法之外,babel暂时只支持如下的语法

Plugins

  • estree
  • jsx
  • flow
  • doExpressions
  • objectRestSpread
  • decorators (Based on an outdated version of the Decorators proposal. Will be removed in a future version of Babylon)
  • classProperties
  • exportExtensions
  • asyncGenerators
  • functionBind
  • functionSent
  • dynamicImport

如果要真要自定义语法,可以在babylon的plugins目录下自定义语法

https://github.com/babel/babylon/tree/master/src/plugins

Babel-types,扩展的AST树

上面提到的babel的AST文档中,并没有提到JSX的语法树,那么JSX的语法树在哪里定义呢,同样jsx的AST树也应该在这个文档中指名,然而babel团队还没精力准备出来

实际上,babel-types有扩展AST树,babel-types的definitions就是天然的文档,具体的源码定义在这里

举例一个AST节点如查是JSXElement,那么它的定义可以在jsx.js中找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

defineType("JSXElement", {
  builder: ["openingElement", "closingElement", "children", "selfClosing"],
  visitor: ["openingElement", "children", "closingElement"],
  aliases: ["JSX", "Immutable", "Expression"],
  fields: {
    openingElement: {
      validate: assertNodeType("JSXOpeningElement"),
    },
    closingElement: {
      optional: true,
      validate: assertNodeType("JSXClosingElement"),
    },
    children: {
      validate: chain(
        assertValueType("array"),
        assertEach(assertNodeType("JSXText", "JSXExpressionContainer", "JSXSpreadChild", "JSXElement"))
      ),
    },
  },
});
 

JSXElement的builder字段指明要构造一个这样的节点需要4个参数,这四个参数分别对应在fields字段中,四个参数的定义如下

openingElement: 必须是一个JSXOpeningElement节点

closingElement: 必须是一个JSXClosingElement节点

children: 必须是一个数组,数组元素必须是JSXText、JSXExpressionContainer、JSXSpreadChild中的一种类型

selfClosing: 未指明验证

使用 babel-types.[TYPE]方法就可以构造这样的一个AST节点

1
2
3
4
5
6
7
8
9

var types = require('babel-types');
 
var jsxElement = types.JSXElement(
            types.OpeningElement(...),
            types.JSXClosingElement(...),
            [...],
            true
);
 

构造了一个jsxElement类型的节点,这在Babel插件开发中是很重要的

同样验证是否一个JSXElement节点,也可以使用babel-types.isTYPE方法

比如

1
2
3
4

var types = require('babel-types');
 
types.isJSXElement(astNode);
 

所以用JSXElement语法定义可以直接看该文件,简单做个梳理如下

其中,斜体代表非终结符,粗体为终结符

Babel的中序工作——Babel-traverse、遍历AST树,插件体系

  • 遍历的方法
    一旦按照AST中的定义,解析成一颗AST树之后,接下来的工作就是遍历树,并且在遍历的过程中进行转换

Babel负责便利工作的是Babel-traverse包,使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13

import traverse from "babel-traverse";
 
traverse(ast, {
  enter(path) {
    if (
      path.node.type === "Identifier" &&
      path.node.name === "n"
    ) {
      path.node.name = "x";
    }
  }
});
 

遍历结点让我们可以获取到我们想要操作的结点的可能,在遍历一个节点时,存在enter和exit两个时刻,一个是进入结点时,这个时候节点的子节点还没触达,遍历子节点完成的时刻,会离开该节点,所以会有exit方法触发

访问节点,可以使用的参数是path参数,path这个参数并不直接等同于节点,path的属性有几个重要的组成,如下

举个栗子,如下的代码会将所有function变成另外的function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

import traverse from "babel-traverse";
 
import types from "babel-types";
 
traverse(ast, {
  enter(path) {
    let node = path.node;
    if(types.isFunctionDeclaration(node)){
        path.replaceWithSourceString(`function add(a, b) {
            return a + b;
         }`);
    }
 
  }
});
 

结果如下

1
2
3
4
5
6

- function square(n) {
-   return n * n;
+ function add(a, b) {
+   return a + b;
  }
 

注意这里我们使用babel-types来判别node的类型,使用path的replaceWithSourceString方法来替换节点

但这里在babel的文档中也有提示,尽量少用replaceWithSourceString方法,该方法一定会调用babylon.parse解析代码,在遍历中解析代码,不如将解析代码放到遍历外面去做

其实上面的过程只是定义了如何遍历节点的时候转换节点

babel将上面的便利操作对外开放出去了,这就构成了babel的插件体系

babel的插件体系——结点的转换定义

babel的插件就是定义如何转换当前结点,所以从这里可以看出babel的插件能做的事情,只能转换ast树,而不能在作用在前序阶段(语法分析)

这里不得不提下babel的插件体系是怎么样的,babel的插件分为两部分

  • babel-preset-xxx
  • babel-plugin-xxx

preset: 预设, preset和plugin其实是一个东西,preset定义了一堆plugin list

这里值得一提的是,preset的顺序是倒着的,plugin的顺序是正的,也就是说

preset: [‘es2015’, ‘react’], 其实是先使用react插件再用es2015

plugin: [‘transform-react’, ‘transfrom-async-function’] 的顺序是正的遍历节点的时候先用transform-react再用transfrom-async-function

babel插件编写

如果是自定义插件,还在开发阶段,要先在babel的配置文件指明babel插件的路径

1
2
3
4
5
6
7
8
9
10
11
12

{
    "extensions": [".jsx", ".js"],
    "presets": ["react", "es2015"],
    "plugins": [
         [
            path.resolve(SERVER_PATH, "pourout/babel-plugin-transform-xxx"),
            {}
         ],
 
     ]
}
 

babel的自定义插件写法是多样,上面只是一个例子,可以传入option,具体可以参考babel的配置文档

上面的代码写成babel的插件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

module.exports =  function(babel) {
 
  var types = babel.types;
  // plugin contents
  return {
    visitor: {
        FunctionDeclaration: {
            enter: function(path){
                 path.replaceWithSourceString(`function add(a, b){ return a + b}`);
            }
        }
    }
  };
};
 

Babel的插件包return一个function, 包含babel的参数,function运行后返回一个包含visitor的对象,对象的属性是遍历节点匹配到该类型的处理方法,该方法依然包含enter和exit方法

一些AST树的创建方法

在写插件的过程中,经常要创建一些AST树,常用的方法如下

  • 使用babel-types定义的创建方法创建
    比如创建一个var a = 1;
1
2
3
4
5
6
7
8
9
10

types.VariableDeclaration(
     'var',
     [
        types.VariableDeclarator(
                types.Identifier('a'),
                types.NumericLiteral(1)
        )
     ]
)
 

如果使用这样创建一个ast节点,肯定要累死了

  • 使用replaceWithSourceString方法创建替换
  • 使用template方法来创建AST结点
    template方法其实也是babel体系中的一部分,它允许使用一些模板来创建ast节点

比如上面的var a = 1可以使用

1
2
3
4
5
6
7

var gen = babel.template(`var NAME = VALUE;`);
 
var ast = gen({
       NAME: t.Identifier('a'),
       VALUE: t.NumberLiteral(1)
});
 

当然也可以简单写

1
2
3
4
5

var gen = babel.template(`var a = 1;`);
 
var ast = gen({
});
 

接下来就可以用path的增、删、改操作进行转换了

Babel的后序工作——Babel-generator、AST树转换成源码

Babel-generator的工作就是将一颗ast树转回来,具体操作如下

1
2
3
4

import generator from "babel-generator";
 
let code = generator(ast);
 

至此,代码转换就算完成了

Babel的外围工作——Babel-register,动态编译

通常我们都是使用webpack编译后代码再执行代码的,使用Babel-register允许我们不提前编译代码就可以运行代码,这在node端是非常便利的

在node端,babel-regiser的核心实现是下面这两个代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

function loader(m, filename) {
  m._compile(compile(filename), filename);
}
 
function registerExtension(ext) {
  var old = oldHandlers[ext] || oldHandlers[".js"] || require.extensions[".js"];
 
  require.extensions[ext] = function (m, filename) {
    if (shouldIgnore(filename)) {
      old(m, filename);
    } else {
      loader(m, filename, old);
    }
  };
}
 

通过定义require.extensions方法,可以覆盖require方法,这样调用require的时候,就可以走babel的编译,然后使用m._compile方法运行代码

但这个方法在node是不稳定的方法

结语

最后,就像babylon官网感觉acorn一样,babel为前端界做了一件awesome的工作,有了babel,不仅仅可以让我们的新技术的普及提前几年,我们可以通过写插件做更多的事情,比如做自定义规则的验证,做node的直出node端的适配工作等等。

参考资料

原文来自:AlloyTeam

声明:所有来源为“聚合数据”的内容信息,未经本网许可,不得转载!如对内容有异议或投诉,请与我们联系。邮箱:marketing@think-land.com

  • 营运车判定查询

    输入车牌号码或车架号,判定是否属于营运车辆。

    输入车牌号码或车架号,判定是否属于营运车辆。

  • 名下车辆数量查询

    根据身份证号码/统一社会信用代码查询名下车辆数量。

    根据身份证号码/统一社会信用代码查询名下车辆数量。

  • 车辆理赔情况查询

    根据身份证号码/社会统一信用代码/车架号/车牌号,查询车辆是否有理赔情况。

    根据身份证号码/社会统一信用代码/车架号/车牌号,查询车辆是否有理赔情况。

  • 车辆过户次数查询

    根据身份证号码/社会统一信用代码/车牌号/车架号,查询车辆的过户次数信息。

    根据身份证号码/社会统一信用代码/车牌号/车架号,查询车辆的过户次数信息。

  • 风险人员分值

    根据姓名和身份证查询风险人员分值。

    根据姓名和身份证查询风险人员分值。

0512-88869195
数 据 驱 动 未 来
Data Drives The Future