文章概览
主要包括:Babel如何进行转码、插件编写的入门基础、实例讲解如何编写插件。javascript
阅读本文前,须要读者对Babel插件如何使用、配置有必定了解,能够参考笔者以前的文章。html
本文全部例子能够在 笔者的github 找到,欢迎访问笔者博客获取更多相关文章。java
Babel运行阶段
首先来了解Babel转码的过程分三个阶段:分析(parse)、转换(transform)、生成(generate)。node
其中,分析、生成阶段由Babel核心完成,而转换阶段,则由Babel插件完成,这也是本文的重点。git
分析
Babel读入源代码,通过词法分析、语法分析后,生成抽象语法树(AST)。github
parse(sourceCode) => AST
转换
通过前一阶段的代码分析,Babel获得了AST。在原始AST的基础上,Babel经过插件,对其进行修改,好比新增、删除、修改后,获得新的AST。npm
transform(AST, BabelPlugins) => newAST
生成
经过前一阶段的转换,Babel获得了新的AST,而后就能够逆向操做,生成新的代码。json
generate(newAST) => newSourceCode
插件基础入门
典型的Babel插件结构,以下代码所示。设计模式
export default function({ types: babelTypes }) { return { visitor: { Identifier(path, state) {}, ASTNodeTypeHere(path, state) {} } }; };
须要关注的内容以下:bash
- babelType:相似lodash那样的工具集,主要用来操做AST节点,好比建立、校验、转变等。举例:判断某个节点是否是标识符(identifier)。
- path:AST中有不少节点,每一个节点可能有不一样的属性,而且节点之间可能存在关联。path是个对象,它表明了两个节点之间的关联。你能够在path上访问到节点的属性,也能够经过path来访问到关联的节点(好比父节点、兄弟节点等)
- state:表明了插件的状态,你能够经过state来访问插件的配置项。
- visitor:Babel采起递归的方式访问AST的每一个节点,之因此叫作visitor,只是由于有个相似的设计模式叫作访问者模式,不用在乎背后的细节。
- Identifier、ASTNodeTypeHere:AST的每一个节点,都有对应的节点类型,好比标识符(Identifier)、函数声明(FunctionDeclaration)等,能够在visitor上声明同名的属性,当Babel遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是path、state。
极简插件实例
在本例子中,咱们实现一个毫无心义的插件:将全部名称为bad的标识符,转成good。完整代码在这里。
首先,安装项目依赖。
npm init -f npm install --save-dev babel-cli
接着,建立插件。判断标识符的名称是不是bad,若是是则替换成good。
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "deadly-simple-plugin-example", visitor: { Identifier(path, state) { if (path.node.name === 'bad') { path.node.name = 'good'; } } } }; };
源码前的源代码:
// index.js let bad = true;
运行转码命令:
npx babel --plugins ./plugin.js index.js
输出转码结果:
// index.js let good = true;
插件配置
插件能够有本身的配置项。咱们修改前面的例子,看下在Babel插件中如何获取配置项。完整代码在这里
首先,咱们新建 .babelrc,传入配置项。
{ "plugins": [ ["./plugin", { "bad": "good", "dead": "alive" }] ] }
而后,修改插件代码。咱们从 state.opts 中获取到配置参数。
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "deadly-simple-plugin-example", visitor: { Identifier(path, state) { let name = path.node.name; if (state.opts[name]) { path.node.name = state.opts[name]; } } } }; };
修改须要转换的代码:
// index.js let bad = true; let dead = true;
运行转码命令 npx babel index.js
,转码结果以下:
// index.js let good = true; let alive = true;
复杂插件例子:替换process.env.NODE_ENV
下面,来看一个稍微复杂一点但比较实用的例子:替换 process.env.NODE_ENV。示例完整代码能够在 这里找到,参考了这个插件。
在不少开源项目中,咱们常常会看到相似下面的代码,对这些代码,须要在构建阶段进行处理,好比进行替换。
// index.js if ( process.env.NODE_ENV === 'development' ) { console.log('我是程序猿小卡'); }
下面,咱们建立一个叫作 node-env-replacer 的插件,代码以下,下面会对插件代码进行讲解。
// plugin.js module.exports = function({ types: babelTypes }) { return { name: "node-env-replacer", visitor: { // 成员表达式 MemberExpression(path, state) { // 若是 object 对应的节点匹配了模式 "process.env" if (path.get("object").matchesPattern("process.env")) { // 这里返回结果为字符串字面量类型的节点 const key = path.toComputedKey(); if ( babelTypes.isStringLiteral(key) ) { // path.replaceWith( newNode ) 用来替换当前节点 // babelTypes.valueToNode( value ) 用来建立节点,若是value是字符串,则返回字符串字面量类型的节点 path.replaceWith(babelTypes.valueToNode(process.env[key.value])); } } } } }; };
插件代码讲解
此次咱们处理的是成员表达方式(MemberExpression)。对于MemberExpression,BabelType的定义以下:
MemberExpression 主要是由 object、property、computed、optional 组成的。对于本例子来讲,object 是 process.env 对应的节点,property 为 NODE_ENV 对应的节点。
defineType("MemberExpression", { builder: ["object", "property", "computed", "optional"], visitor: ["object", "property"], // ... });
前面提到,path对应了节点的属性,以及节点的关联关系。path.get("object") 获取到的就是 object(process.env)对应的 path实例。
matchesPattern(pattern) 检查某个节点是否符合某种模式(pattern)。本例子中,path.get("object").matchesPattern("process.env") 检查 object 是否符合 "process.env" 这种模式。好比 成员表达式 process.env.NODE_ENV 为true,而成员表达式 process.hello.NODE_ENV 返回false。
if (path.get("object").matchesPattern("process.env")) { }
接着,经过 path.toComputedKey() 获取成员表达式的键(key),对于对于MemberExpression,返回的是类型为字符串字面量(stringLiteral)的节点。
const key = path.toComputedKey();
if ( babelTypes.isStringLiteral(key) ) 判断 key 是否为字符串字面量,若是是,则返回true。
path.replaceWith( node ) 方法用来替换节点。babelTypes.valueToNode( value ) 用来建立节点,若是value是字符串,则返回字符串字面量类型的节点。
path.replaceWith(babelTypes.valueToNode(process.env[key.value]));
运行插件
命令以下:
npx babel --plugins ./plugin.js index.js
转换结果:
// index.js if ('development' === 'development') { console.log('我是程序猿小卡'); }
小结
Babel的插件入门比较简单,照葫芦画瓢便可。在编写插件过程当中,可能会遇到的主要障碍,包括对ECMA规范不了解、对Babel的API不了解。
- 对ECMA规范不了解:MemberExpression、FunctionDeclaration、Identifier等都是规范里的术语,若是对规范没有必定的了解,转换代码的时候就不知道如何入手。建议读者稍微了解下ECMA规范。
- 对Babel的API不了解:Babel相关API的文档比较少,这会对插件编写形成不小的困难,目前比较好的解决办法,就是参考现有的插件进行修改。
总而言之,就是多看多写多查。
这里再留个小问题,前面插件替换了 process.env.NODE_ENV,若是是下面代码该怎么替换?
process.env['NODE_' + 'ENV'];