说到 babel 你确定会先想到 babel 能够将还未被浏览器实现的 ES6 规范转换成可以运行 ES5 规范,或者能够将 JSX 转换为浏览器能识别的 HTML 结构,那么 babel 是如何进行这个转换的步骤呢,下面我将经过开发一个简单的 babel 插件来解释这整个过程,但愿你对 Babel 插件原理与 AST 有新的认知。node
从上面的分析,咱们大概能猜出 Babel 的运行过程是:原始代码 -> 修改代码,那么在这个转换的过程当中,咱们须要知道如下三个重要的步骤。git
首先须要将 JavaScript 字符串通过词法分析、语法分析后,转换为计算机更易处理的表现形式,称之为“抽象语法树(AST)”,这个步骤咱们使用了 Babylon 解析器。github
当 JavaScript 从字符串转换为 AST 后,咱们就能更方便地对其进行浏览、分析和有规律的修改,根据咱们的需求,将其转换为新的 AST,babel-traverse 是一个很好的转换工具,使得咱们可以很便利的操做 AST 。npm
最后,咱们将修改完的 AST 进行反向处理,生成 JavaScript 字符串,整个转换过程也就完成了,这一步当中,咱们使用到了 babel-generator 模块。编程
以前听过一句话:“若是你能熟练地操做 AST ,那么你真的能够随心所欲。”,当时并不理解其含义,直到真正了解 AST 后,才发现 AST 对编程语言的重要性是不可估量的。设计模式
在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每一个节点都表示源代码中的一种结构。浏览器
之因此说语法是「抽象」的,是由于这里的语法并不会表示出真实语法中出现的每一个细节。bash
JavaScript 程序通常是由一系列字符组成的,咱们可使用匹配的字符([], {}, ()),成对的字符('', "")和缩进让程序解析起来更加简单,可是对计算机来讲,这些字符在内存中仅仅是个数值,并不能处理这些高级问题,因此咱们须要找到一种方式,将其转换成计算机能理解的结构。babel
咱们简单看下面的代码:编程语言
let a = 2;
a * 8
复制代码
将其转换为 AST 会是怎样的呢,咱们使用 astexplorer 在线 AST 转换工具,能够获得如下树结构:
为了更形象表述,咱们将其转换为更直观的结构图形:
AST 的根节点都是 Program ,这个例子中包含了两部分:
一个变量申明(VariableDeclarator),将标识符(Identifier) a 赋值为数值(NumericLiteral) 3。
一个二元表达式语句(BinaryExpression),描述为标志符(Identifier)为 a,操做符(operator) + 和数值(NumericLiteral) 5。
这只是一个简单的例子,在实际开发中,AST 将会是一个巨型节点树,将字符串形式的源代码转换成树状的结构,计算机便能更方便地处理,咱们使用的 Babel 插件,也就是对 AST 进行插入/移动/替换/删除节点,建立成新的 AST ,再将 AST 转换为字符串源代码,这即是 Babel 插件的原理,之因此可以“随心所欲”,其缘由就是能够将原始代码按照指定逻辑转换为你想要的代码。
一个典型的 Babel 插件结构,以下代码所示:
export default function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression(path, state) {
path.replaceWith(
t.callExpression(
t.memberExpression(t.identifier('mori'), t.identifier('vector')),
path.node.elements
)
);
},
ASTNodeTypeHere(path, state) {}
}
};
};
复制代码
咱们要关注的几个点为:
babel.types
: 用来操做 AST 节点,如建立、转换、校验等。vistor
: Babel 采用递归的方式访问 AST 的每一个节点,之因此叫作visitor,只是由于有个相似的设计模式叫作访问者模式,如上述代码中的 ArrayExpression
,当遍历到 ArrayExpression
节点时,即触发对应函数。path
: path 是指 AST 节点的对象,能够用来获取节点的属性、节点之间的关联。state
: 指插件的状态,能够用过 state 来获取插件中的配置项。ArrayExpression、ASTNodeTypeHere
: 指 AST 中的节点类型。由于是 Demo ,咱们需求很简单,咱们开发的 Bable 插件名称叫 vincePlugin
,在使用的时候,能配置插件的参数,使得插件能按照咱们配置的参数进行转换。
// babel 参数配置
plugins: [
[vincePlugin, {
name: 'vince'
}]
]
复制代码
转换效果:
var fool = [1,2,3];
// translate to =>
var fool = vince.init(1,2,3)
复制代码
为了你们更方便的阅读代码,源码已经上传到GitHub: babel-plugin-demo
了解了以上概念与需求后,咱们就能够开始进行 Babel 插件开发,开始以前先建立一个项目目录,初始化 npm ,并安装 babel-core :
mkdir babel-plugin-demo && cd babel-plugin-demo
npm init -y
npm install --save-dev babel-core
复制代码
建立 plugin.js
babel 插件文件,咱们将会在这里写转换的逻辑代码:
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
// ...
}
};
};
复制代码
建立原始代码 index.js
var fool = [1,2,3];
复制代码
建立 test.js
测试函数,这里咱们进行对插件的测试:
// test.js
var fs = require('fs');
var babel = require('babel-core');
var vincePlugin = require('./plugin');
// read the code from this file
fs.readFile('index.js', function(err, data) {
if(err) throw err;
// convert from a buffer to a string
var src = data.toString();
// use our plugin to transform the source
var out = babel.transform(src, {
plugins: [
[vincePlugin, {
name: 'vince'
}]
]
});
// print the generated code to screen
console.log(out.code);
});
复制代码
咱们经过 node test.js
,来测试 babel 插件的转换输出。
var fool = [1,2,3];
经过 AST 分析出来的节点如图:var bar = vince.init(1, 2, 3);
,经过 AST 分析出来的节点如图:咱们经过用红色标注来区分原始与转换后的 AST 结构图,如今咱们能够很清晰的看到咱们须要替换的节点,将 ArrayExpression 替换为 CallExpression ,在 CallExpression 节点中中增长一个 MemberExpression,而且保留原始的三个 NumericLiteral。
首先,咱们须要替换的是 ArrayExpression ,因此给 vistor 添加 ArrayExpression 方法。
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression: function(path, state) {
// ...
}
}
};
};
复制代码
当 Babel 遍历 AST 时,当发现含有 visitor 上有对呀节点方法时,即会触发这个方法,而且将上下文传入(path, state),在函数里面咱们进行节点的分析和替换操做:
// plugin.js
module.exports = function(babel) {
var t = babel.types;
return {
visitor: {
ArrayExpression: function(path, state) {
// 替换该节点
path.replaceWith(
// 建立一个 callExpression
t.callExpression(
t.memberExpression(t.identifier(state.opts.name), t.identifier('init')),
path.node.elements
)
);
}
}
};
};
复制代码
咱们须要将 ArrayExpression 替换为 CallExpression,能够经过 t.callExpression(callee, arguments) 来生成 CallExpression,第一个参数是 MemberExpression,经过t.memberExpression(object, property) 来生成,而后再将原有的三个 NumericLiteral 设置为第二个参数,因而就完成了咱们的需求。
这里咱们要注意 state.opts.name
中指的是配置 plugin 时,设置的 config 参数。
更多的转换方式和节点属性,能够查阅 babel-types 的文档
咱们回到test.js
,运行node test.js
,便会得出:
node test.js
=> var bar = vince.init(1, 2, 3);
复制代码
到这里,咱们简易的 Babel 插件便完成好了,实际上的开发需求要复杂的多,可是主要的逻辑仍是离不开上面的几个概念。
仍是回到开始那句话“若是你能熟练地操做 AST ,那么你真的能够随心所欲。”,咱们可以经过 AST 将原始代码转换成咱们所须要的任何代码,甚至你能建立一个私人的 ESXXX
,添加你创造的新规范。AST 并非一个很复杂的技术活,很大一部分能够视为“苦力活”,由于遇到复杂的转换需求可能须要编写写不少逻辑代码。
经过阅读这篇文章,咱们了解了 Babel 插件的实现原理,而且实践了一个 Plugin,除此以外,咱们也理解了 AST 的概念,认识到了其强大之处。
引用: