前言
文本babel
插件开发将用到babel-cli
脚手架环境,并结合一个AST
抽象语法树 查询网站AST Explorer,让你开发插件时能够快速定位对应js
代码的AST
节点, 并通过几个demo
让你快速了解如何开发一个babel
插件。
关键词: 速成babel、babel插件、AST/抽象语法树。
babel简介
Babel 是一个 JavaScript 编译器
Babel 是一个工具链,主要用于将采用 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以便能够运行在当前和旧版本的浏览器或其他环境中。下面列出的是 Babel 能为你做的事情:
- 语法转换
- 通过 Polyfill 方式在目标环境中添加缺失的功能(通过引入第三方 polyfill 模块,例如 core-js)
- 源码转换(codemods)
- ......
babel-cli环境搭建
新建一个babel项目目录hello-babel
,并在该目录下运行以下命令安装@babel/cli
脚手架:
cmd
npm install --save-dev @babel/cli
在根目录下添加src
文件目录,用来编写我们需要进行编译的js
文件,并在该文件下添加一个index.js
文件,下文我们将就在这个文件下进行编写需要进行代码转换的相关代码。
index.js
js
const fn = () => {
console.log('hello babel')
}
有了以上基础后,这时我们就可以通过在根目录的cmd
命令行中输入./node_modules/.bin/babel src --out-dir lib
来对编写的js
代码进行编译了,如果你使用的是npm@5.2.0
以上版本,可以通过npx babel src --out-dir lib
来执行编译。
下面解释以上命令代表的含义:
src
:代表编译该目录下的所有js
文件
src/index.js
:只编译该目录下的index.js
文件
--out-dir lib
:将编译后的js
文件输出到根目录下的lib
文件夹下
运行以上命令后,发现把src/index.js
文件原样输出到了lib/index.js
文件夹下:
这是由于我们还未进行插件编写,在编写插件之前有必要简单了解一下: 一个插件其实就是一个函数,该函数会返回一个对象,对象里具有一个visitor属性,该属性也是一个对象,用来观察对应节点的变化。
第一个babel插件
下面来写一个简单的修改节点名称的插件说明如何观察节点对对其进行编辑。
这里给大家推荐一个查看AST抽象语法树结构
的网站:AST Explorer
如下图所示,把src/index.js
的代码copy
到该网站下,左边是你的代码,右边可查看对应的AST
树或者JSON
结构。
我们需要修改fn
的方法名,鼠标点击fn
名称,右边会自动定位到对应的节点。
上图可得出,fn
方法得节点类型为Identifier
,我们可以编写以下代码,并观察Identifier
节点。
js
module.exports = function () {
return {
visitor: {
Identifier(path) {
// ...
},
},
};
}
Identifier
方法可接收到一个参数,记录Identifier
类型节点的相关信息,我们可通过path.node
获取当前节点信息。
js
Identifier(path) {
let name = path.node.name;
if (name === 'fn') {
console.log(path.node)
}
},
// path.node打印信息如下
Node {
type: 'Identifier',
start: 6,
end: 8,
loc: SourceLocation {
start: Position { line: 1, column: 6, index: 6 },
end: Position { line: 1, column: 8, index: 8 },
filename: undefined,
identifierName: 'fn'
},
name: 'fn',
leadingComments: undefined,
innerComments: undefined,
trailingComments: undefined
}
我们要修改Identifier
类型的节点名称,只需需求path.node.name
的值即可:
plugins/change-name.js
js
module.exports = function () {
return {
visitor: {
// 编辑节点名称
Identifier(path) {
let name = path.node.name;
if (name === 'fn') {
console.log(path.node)
path.node.name = 'fn2'
}
},
},
};
}
以上我们就完成了一个把Identifier
节点名称为fn
的修改为fn2
的babel
插件,想要使用这个插件还需要创建一个配置文件进行引用。
创建babel配置文件
babel
配置可在根目录下的这4
种文件里面进行配置:
babel.config.json
.babelrc.json
package.json
babel.config.js
这里我们选择在根目录下创建babel.config.js
文件进行配置:
js
module.exports = {
// 引入当前目录下的plugins/change-name.js插件
plugins: ['./plugins/change-name.js']
}
执行babel
编译
cmd
npx babel src src --out-dir lib
可在lib/index.js
文件下看到名称修改成功了!
进阶:向console.log()方法下追加参数插件
还是老样子,copy
代码先查看一下该观察哪个类型的节点:
上图可看出,我们可以通过观察CallExpression
节点来获取console.log
相关的信息,并且AST
中有一个arguments
数组存放着console.log
的参数节点,所有我们可以向arguments
数组添加我们想要打印的参数:
js
// const t = require('@babel/types');
// 编辑进阶版:向console.log添加参数
module.exports = function({ types: t }) {
return {
visitor: {
CallExpression({node}) {
// 由上图得出的console.log所在节点类型条件,来查找出console.log方法
if (t.isMemberExpression(node.callee)) {
if (node.callee.object.name === "console") {
// 找到console对象
if (["log"].includes(node.callee.property.name)) {
// 找到对应的方法和所在行数
const { line } = node.loc.start; // 找到所处位置的行
// 向arguments添加打印所在行信息
node.arguments.push(t.stringLiteral(`line:${line}`));
}
}
}
},
},
};
}
以上我们从插件方法的参数当中解构出types
参数,该参数和顶部引入的@babel/types
包作用一样。我们可通过该对象提供的相关类型工具判断类型,以及向arguments
数组添加一个stringLiteral
节点,该节点的内容为line:${line}
。
配置文件中引入插件:
js
module.exports = {
// 插件列表,编译顺序从前到后
plugins: [
'./plugins/change-name.js',
'./plugins/add-args.js'
]
}
执行后输出代码如下:
向代码块{}中添加方法插件
我们先修改一下src/index.js
文件
js
const fn = () => {
var name = 'ikun'
console.log(name)
}
目的是在以上代码的{}
代码块最前面插入console.log('start')
代码。
首先找出{}
代码块对应的AST
类型:
我们可看到{}
的类型为BlockStatement
,并且该节点下具有一个body
属性存放在两个元素,分别为var name = 'ikun'
和console.log(name)
节点,所有我们只需参考后者的节点信息,把console.log('start')
节点创建出来,并放到body
数组的最前面即可:
plugins/add-console.js
js
module.exports = function markFnPlugin({ types: t }) {
return {
// 在babel把js源文件编译为AST抽象语法树后,我们可以根据抽象语法树的规则,对源文件进行增加/修改/删除代码的目的
visitor: {
// 添加代码:在代码块{}最前面插入一段console.log('start')代码
BlockStatement(path) {
// 在数组前面插入元素
path.node.body.unshift(
// 创建expressionStatement节点
t.expressionStatement(
t.callExpression(
t.memberExpression(
t.identifier('console'),
t.identifier('log')
),
[t.stringLiteral('start')]
)
)
)
}
},
};
}
babel.config.js
js
module.exports = {
plugins: [
'./plugins/add-console.js'
]
}
执行后可得到以下结果:
总结
以上通过修改对应节点名称 、向console.log
方法追加参数 、向{}
代码块追加代码 三个插件快速了解了如何编写babel
插件,并结合AST Explorer网站,帮我们快速定位AST
结构,通过编辑AST
抽象语法树的节点信息,可让我们对源代码进行转换,这也是前端工程化的一大利器。