热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

剖析Babel——Babel总览

名词解释AST:AbstractSyntaxTree,抽象语法树DI:DependencyInjection,依赖注入Babel的解析引擎Babel使用的引擎是b

名词解释

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 第二个是functionvar arg = parent.arguments;var serviceName = arg[0];var serviceFunc = arg[1];for (var i = 0; i


具体的流程如下

可以从上面的过程看到 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 方法

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 树的定义

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; // >= 1column: number; // >= 0}

节点又包含行号和列号

再看 Program 的定义

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


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

Babylon 支持的语法

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

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

那么 babylon 支持的语法有哪些呢&#xff0c;除了常规的 js 语法之外&#xff0c;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

如果要真要自定义语法&#xff0c;可以在 babylon 的 plugins 目录下自定义语法

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

Babel-types&#xff0c;扩展的 AST 树

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

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

举例一个 AST 节点如查是 JSXElement&#xff0c;那么它的定义可以在 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 个参数&#xff0c;这四个参数分别对应在 fields 字段中&#xff0c;四个参数的定义如下

openingElement: 必须是一个 JSXOpeningElement 节点

closingElement: 必须是一个 JSXClosingElement 节点

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

selfClosing: 未指明验证

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

var types &#61; require(&#39;babel-types&#39;);var jsxElement &#61; types.JSXElement(types.OpeningElement(...),types.JSXClosingElement(...),[...],true);


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

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

比如

var types &#61; require(&#39;babel-types&#39;);types.isJSXElement(astNode);


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

其中&#xff0c;斜体代表非终结符&#xff0c;粗体为终结符

Babel 的中序工作——Babel-traverse、遍历 AST 树&#xff0c;插件体系


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

Babel 负责便利工作的是 Babel-traverse 包&#xff0c;使用方法

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

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

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

举个栗子&#xff0c;如下的代码会将所有 function 变成另外的 function

import traverse from "babel-traverse";import types from "babel-types";traverse(ast, {enter(path) {let node &#61; path.node;if(types.isFunctionDeclaration(node)){path.replaceWithSourceString(&#96;function add(a, b) {return a &#43; b;}&#96;);}}});

结果如下

- function square(n) {-   return n * n;&#43; function add(a, b) {&#43;   return a &#43; b;}


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

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

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

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

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

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

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

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

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

这里值得一提的是&#xff0c;preset 的顺序是倒着的&#xff0c;plugin 的顺序是正的&#xff0c;也就是说

preset: [&#39;es2015&#39;, &#39;react&#39;], 其实是先使用 react 插件再用 es2015

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

babel 插件编写

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

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


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

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

module.exports &#61;  function(babel) {var types &#61; babel.types;// plugin contentsreturn {visitor: {FunctionDeclaration: {enter: function(path){path.replaceWithSourceString(&#96;function add(a, b){ return a &#43; b}&#96;);}}}};};


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

一些 AST 树的创建方法

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

  • 使用 babel-types 定义的创建方法创建
    比如创建一个 var a &#61; 1;

types.VariableDeclaration(&#39;var&#39;,[types.VariableDeclarator(types.Identifier(&#39;a&#39;),types.NumericLiteral(1))])


如果使用这样创建一个 ast 节点&#xff0c;肯定要累死了

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

比如上面的 var a &#61; 1 可以使用

var gen &#61; babel.template(&#96;var NAME &#61; VALUE;&#96;);var ast &#61; gen({NAME: t.Identifier(&#39;a&#39;),VALUE: t.NumberLiteral(1)});


当然也可以简单写

var gen &#61; babel.template(&#96;var a &#61; 1;&#96;);var ast &#61; gen({});


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

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

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

import generator from "babel-generator";let code &#61; generator(ast);


至此&#xff0c;代码转换就算完成了

Babel 的外围工作——Babel-register&#xff0c;动态编译

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

在 node 端&#xff0c;babel-regiser 的核心实现是下面这两个代码

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

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

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

结语

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

参考资料

babel 官网&#xff1a; https://babeljs.io

babel-github: Babel · GitHub

babylon: GitHub - babel/babylon: PSA: moved into babel/babel as &#64;babel/parser -->

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

babel-ast 文档&#xff1a; babylon/spec.md at master · babel/babylon · GitHub

babel 插件 cookbook: https://github.com/thejameskyle/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md

babel-packages: https://github.com/babel/babel/tree/7.0/packages

babel-types-definitions: https://github.com/babel/babel/tree/7.0/packages/babel-types/src/definitions


推荐阅读
author-avatar
Apollo宫保鸡丁
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有