共计 3252 个字符,预计需要花费 9 分钟才能阅读完成。
Babel 简介
Babel 是 Javascript 编译器,是种代码到代码的编译器,通常也叫做『转换编译器』。
Babel 的工作过程
Babel 的处理主要过程:解析(parse)、转换(transform)、生成(generate)。
- 代码解析
词法分析和语法分析构造 AST。 - 代码转换
处理 AST,处理工具、插件等就是在这个阶段进行代码转换,返回新的 AST。 - 代码生成
遍历 AST,输出代码字符串。所以我们需要对 AST 有一定了解才能进行 Babel 插件开发。
AST
在这整个过程中,都是围绕着抽象语法树 (AST) 来进行的。在 Javascritp 中,AST,简单来说,就是一个记录着代码语法结构的 Object。感兴趣的同学可到 https://astexplorer.net/ 去深入体验
比如下面的代码:
import {Button} from 'antd';
import Card from 'antd/button/lib/index.js';
转换成 AST 后如下,
{
"type": "Program",
"start": 0,
"end": 253,
"body": [
{
"type": "ImportDeclaration",
"start": 179,
"end": 207,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 187,
"end": 193,
"imported": {
"type": "Identifier",
"start": 187,
"end": 193,
"name": "Button"
},
"local": {
"type": "Identifier",
"start": 187,
"end": 193,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 200,
"end": 206,
"value": "antd",
"raw": "'antd'"
}
},
{
"type": "ImportDeclaration",
"start": 209,
"end": 253,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 216,
"end": 220,
"local": {
"type": "Identifier",
"start": 216,
"end": 220,
"name": "Card"
}
}
],
"source": {
"type": "Literal",
"start": 226,
"end": 252,
"value": "antd/button/lib/index.js",
"raw": "'antd/button/lib/index.js'"
}
}
],
"sourceType": "module"
}
插件开发思路
- 确定我们需要处理的节点类型
- 处理节点
- 返回新的节点
简单插件结构
插件必须是一个函数,根据官方文档要求,形式如下:
module.exports = function ({types: t}) {
return {
visitor: {ImportDeclaration(path, source){//todo},
FunctionDeclaration(path, source){//todo},
}
}
}
types 来自 @babel/types 工具类,主要用途是在创建 AST 的过程中判断各种语法的类型和节点构造。
实现示例
很多同学用过 babel-plugin-import,它帮助我们在使用一些 JS 类库是达到按需加载。其实,该插件帮助我们做了如下代码转换:
//from
import {Button} from 'antd';
//to
import Button from 'antd/es/button';
import 'antd/es/button/style.css';
我们先看看两者的 AST 有何差别,以帮助我们对转换有个清晰的认识:
转换前:
[{
"type": "ImportDeclaration",
"start": 6,
"end": 45,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 14,
"end": 20,
"imported": {
"type": "Identifier",
"start": 14,
"end": 20,
"name": "Button"
},
"local": {
"type": "Identifier",
"start": 14,
"end": 20,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 28,
"end": 44,
"value": "antd/es/button",
"raw": "'antd/es/button'"
}
}]
转换后:
[{
"type": "ImportDeclaration",
"start": 5,
"end": 41,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 12,
"end": 18,
"local": {
"type": "Identifier",
"start": 12,
"end": 18,
"name": "Button"
}
}
],
"source": {
"type": "Literal",
"start": 24,
"end": 40,
"value": "antd/es/button",
"raw": "'antd/es/button'"
}
},
{
"type": "ImportDeclaration",
"start": 46,
"end": 76,
"specifiers": [],
"source": {
"type": "Literal",
"start": 53,
"end": 75,
"value": "antd/es/button/style",
"raw": "'antd/es/button/style'"
}
}]
对比两棵树,我们应该有个大致的思路。在转换过程中,我们还需要一些参数,这些参数在配置文件 (package.json 或者.babelrc) 中, 提供了一些自定义配置,比如 antd 的按需加载:
["import",{libraryName:"antd",libraryDireactory:"es","style":"css"}]
现在我们开始尝试实现这个插件吧:
module.exports = function ({types: t}) {
return {
visitor: {ImportDeclaration(path, source) {
// 取出参数
const {opts: { libraryName, libraryDirectory='lib', style="css"} } = source;
// 拿到老的 AST 节点
let node = path.node
if(node.source.value !== libraryName){return;}
// 创建一个数组存入新生成 AST
let newImports = [];
// 构造新节点
path.node.specifiers.forEach(item => {newImports.push(t.importDeclaration([t.importDefaultSpecifier(item.local)], t.stringLiteral(`${libraryName}/${libraryDirectory}/${item.local.name}`)));
newImports.push(t.importDeclaration([], t.stringLiteral(`${libraryName}/${libraryDirectory}/style.${style}`)))
});
// 替换原节点
path.replaceWithMultiple(newImports);
}
}
}
}
现在,简单版本的 @babel-plugin-import 的 babel 插件我们已经完成了。
若感兴趣了解更多内容,babel 插件中文开发文档提供了很多详细资料。
正文完
发表至: javascript
2019-08-07