前言
社区里面有很多关于
Babel
的文章,有些写的很好,我自己也受这些文章启发很大。但我发现一个问题就是,这类文章一进来就讲了很多babel底层的概念,说实话对基础不深的一些童鞋来说,看完之后理解起来还是有一定难度的,最重要的是看完了之后,自己并不知道如何去写一个
Babel
插件,因而这促使了
如何从0到1完成一个babel插件
这篇文章的编写,学习完本篇文章,期望是大家能对
Babel
有一个整体的认识,知道
Babel
是什么?
Babel
是如何运作的?并且自己能实现一个简单的
Babel
插件。
什么是Babel
Babel
是一个JavaScript编译器,意思就是说你为
Babel
提供一些代码,
Babel
做一些转换,给你返回一些新的代码。比如,我们常见的将ES5+的代码转换成ES5+之前的一些代码。
Babel的处理步骤
如图,Babel经过3个处理步骤,分别为
解析(parse)
,
转换(transform)
,
生成(generate)
。
解析
解析又经过
词法分析
,
语法分析
两个步骤,将输入的代码生成
抽象语法数(AST)
,AST可以理解为就是描述一段代码的节点树,看如下这个例子:
我们输入
const a = 1
复制代码
经过
解析(parse)
,生成如下结构的节点树(为了方便观看,去掉了一些表明节点位置信息的属性),详细的可以通过这个
工具
查看
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": {
"type": "Literal",
"value": 1,
"rawValue": 1,
"raw": "1"
}
}
],
"kind": "const"
}
复制代码
每一个{“type”:””}包裹的内容都可以视为一个节点(Node)
转换
得到了AST抽象语法树,本质就是一个用来描述代码的节点树(Node),我们就可以通过
树形遍历
来遍历它,从而进行代码转换(对节点添加、更新及移除等操作),也就是
Babel插件
真正处理的地方
生成
经过转换之后的AST还是AST,所以我们还需要将AST生成字符串形式的代码
实战
Babel的基础知识还有很多,我觉得一开始了解这么多就够了,我们现在开始开发一个简单的Babel转换。
如前面所说Babel的3个步骤,
解析
,
转换
,
生成
,Babel都提供了对应的方法,分别如下:
-
@babel/parser 提供解析
parse
-
@babel/traverse 提供转换
traverse
-
@babel/generator 提供生成
generate
我们要实现一个插件,将整个引入组件的代码
import { Select as MySelect, Pagination } from 'UI';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
复制代码
处理为如下按需处理的形式
import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
复制代码
第一:搭建一个开发环境
这里我使用了
codesandbox
在线编写的方式,
访问这里
,将需要的依赖包引进来。
const parse = require('@babel/parser').parse;
const traverse = require('@babel/traverse').default;
const generate = require('@babel/generator').default;
const t = require('@babel/types');
复制代码
其中,
@babel/types
是用于 AST 节点的 Lodash 式工具库, 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。
第二:解析代码
const code = `import { Select as MySelect, Pagination } from '';
// import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
`;
const ast = parse(code);
复制代码
第三:转换代码
这步很关键,我们的转换处理都在这一步
traverse(ast, {
ImportDeclaration(path) {
// 获取原本组件名
const source = path.node.source.value;
// 获取Select as MySelect , Pagination两个节点
const specifiers = path.node.specifiers;
// import specifiers有3种形式,ImportSpecifier ,ImportNamespaceSpecifier,ImportDefaultSpecifier
// 获取specifiers类型是否是 命名空间类型,类似 import * as UI from 'xxx-ui' 这种
const isImportNamespaceSpecifier = t.isImportNamespaceSpecifier(
specifiers[0]
);
// 获取specifiers类型是否是 默认导出类型,类似 import UI2 from 'xxx-ui' 这种
const isImportDefaultSpecifier = t.isImportDefaultSpecifier(specifiers[0]);
if (!isImportNamespaceSpecifier && !isImportDefaultSpecifier) {
const declarations = specifiers.map(specifier => {
// 缓存单个组件名
let localName = specifier.local.name;
// 拼接引入路径
let newSource = `${source}/${localName}/${localName}.js`;
// 构造新的ImportDeclaration节点
return t.importDeclaration(
[t.importDefaultSpecifier(specifier.local)],
t.stringLiteral(newSource)
);
});
// 将构造好的新AST替换原来的AST
path.replaceWithMultiple(declarations);
}
}
});
复制代码
traverse
方法第二个参数传入的就是我们对具体节点遍历的处理方法,这里有个概念需要明确的是,当我们以访问者身份遍历节点的时候,我们其实访问的是路径path,而非具体某个节点,所以示例中我们我们有2个
ImportDeclaration
节点,但我们只写了一个处理方法,因为这里这个方法会被执行2次。
第四:生成
最后,我们需要将转换后的AST重新生成代码
let newCode = generate(ast).code;
console.log(newCode);
复制代码
第五:运行
终端输入
node ./src/index.js
复制代码
可以看到最终我们生成的代码
import MySelect from "/MySelect/MySelect.js";
import Pagination from "/Pagination/Pagination.js"; // import UI2 from 'xxx-ui';
import * as UI from 'xxx-ui';
复制代码
实际项目引用
我们完成了一个babel插件,那在项目中如何引入呢?其实,上述所述的步骤代码只是从内部剖析了下Babel插件的处理原理,真正我们在项目中只需要对外暴露一个方法,里面返回一个包含
visitor
属性的对象。
visitor访问者是一个对象,定义了一系列访问树形结构中节点的方法
// myPlugin.js
const babel = require(@babel/core');
const t = require('@babel/types');
export default function() {
return {
visitor: {
ImportDeclaration(path, state) {
//转换逻辑
},
}
};
};
复制代码
然后在
babel-loader
的plugin引入
options:{
plugins:[
["myPlugin"]
]
}
复制代码
原理是啥呢?是因为通过
babel-loader
引入,Babel里面core模块提供了
transform
方法,具体APi可以查看
这里
,只需要传入
visitor
对象,该方法据此默认会去做
解析
,
转换
,
生成
工作,内部处理逻辑如下:
const visitor = require('visitor.js');
const babel = require('@babel/core');
const result = babel.transform(code, {
plugins: [visitor],
});
复制代码
进阶例子
这里提供一个简化版的
vue
转
react
的示例,有兴趣的可以学习下,
地址
。
最后,如果你对多端开发有兴趣,我们微店有个小组,从事多端统一开发研究,有兴趣的童鞋也可以加入进来看看,里面有很多Babel相关实例和文章,
访问地址