乐趣区

关于babel:转剖析-BabelBabel-总览

名词解释

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 转换示例
解决的问题

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

转换成

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

相熟 angular 的同学都能看到这段代码做的是对 DI 的主动提取性能,应用 ACON 手动撸代码

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 树的还原能力

  1. 批改源码依然靠源码批改字符串的形式

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

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

Babel 转 AST 树的过程波及到语法的问题,转 AST 树肯定有对就的语法,如果在解析过程中,呈现了不合乎 Babel 语法的代码,就会报错,Babel 转 AST 的解析过程在 Babylon 中实现

解析成 AST 树应用 babylon.parse 办法

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/baby…

对于 AST 树的定义

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

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

其中地位定义如下

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

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

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

节点又蕴含行号和列号

再看 Program 的定义

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/baby…

Babel-types,扩大的 AST 树

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

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

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

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 节点

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

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

同样验证是否一个 JSXElement 节点,也能够应用 babel-types.isTYPE 办法

比方

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

所以用 JSXElement 语法定义能够间接看该文件,简略做个梳理如下

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

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

  • 遍历的办法
    一旦依照 AST 中的定义,解析成一颗 AST 树之后,接下来的工作就是遍历树,并且在遍历的过程中进行转换
    Babel 负责便当工作的是 Babel-traverse 包,应用办法
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

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;}`);
    }
 
  }
});

后果如下

- 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 插件的门路

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

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

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

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;

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

如果应用这样创立一个 ast 节点,必定要累死了

  • 应用 replaceWithSourceString 办法创立替换
  • 应用 template 办法来创立 AST 结点
    template 办法其实也是 babel 体系中的一部分,它容许应用一些模板来创立 ast 节点
    比方下面的 var a = 1 能够应用

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

    当然也能够简略写

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

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

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

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

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

至此,代码转换就算实现了

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

通常咱们都是应用 webpack 编译后代码再执行代码的,应用 Babel-register 容许咱们不提前编译代码就能够运行代码,这在 node 端是十分便当的

在 node 端,babel-regiser 的外围实现是上面这两个代码

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 端的适配工作等等。

参考资料

babel 官网:https://babeljs.io

babel-github: https://github.com/babel

babylon: https://github.com/babel/babylon

acorn: https://github.com/marijnh/acorn

babel-ast 文档:https://github.com/babel/baby…

babel 插件 cookbook: https://github.com/thejamesky…

babel-packages: https://github.com/babel/babe…

babel-types-definitions: https://github.com/babel/babe…

转载

转载自 AlloyTeam:http://www.alloyteam.com/2017…

退出移动版