babel插件开发
一. babel 原理
1. 什么是 babel
Babel 是一个 JavaScript 编译器, 可以进行 语法转换、源码转换 等,主要用于将 ECMAScript 2015+ 语法编写的代码转换为向后兼容的 JavaScript 语法,以实现浏览器兼容。
2. 转换原理与 ast
转换原理:
原代码 ==解析==》 ast抽象语法树 ==修改==》ast抽象语法树 ==生成==》新代码
ast 简单来说就是由许多节点(node)组成的树状结构,node 之间的关系用 path 标示,详细记录了每个节点的属性与内容。
我们要做的就是使用 babel 将代码解析后,修改 ast ,再生成处理后的代码。
ast 结构可以通过 astexplorer.net 查看。
3. babel 的架构
babel 是通过插件(plugins)构成,简单来说就是每个插件负责只处理代码中的某一类问题。
例如:
| 插件 | 功能 |
|---|---|
| @babel/plugin-transform-arrow-functions | 将箭头函数转换为function |
| @babel/plugin-transform-spread | 除理展开运算符 ... |
将所有需要的功能的插件配置入 babel.config.js 中,即可完成语法的转换。
但是一般需要许多插件,配置繁琐,所以可以使用配置好的功能集合——也就是预设(presets)的方式引入
例如:
| 预设 | 功能 |
|---|---|
| @babel/preset-env | 包含了常用插件,并可根据配置(需兼容的浏览器等)进一步控制其中导出的插件 |
| @babel/preset-react | 包含了react相关插件 |
二. 准备工作
1. 相关工具
首选了解一下 babel 相关的包
| 预设 | 功能 |
|---|---|
| @babel/cli | 可以通过命令行执行babel |
| @babel/core | 可以在js中调用执行babel |
| @babel/types | 提供了开发babel插件时的常用方法(类似于lodash) |
2.目录结构
|-- dist // 转换后代码目录
|-- src
|-- index.js // 插件源码
|-- test.code.js // 需要转换的测试代码
|-- test.js // 使用 @babel/core 转换代码
|-- babel.config.jschunk // babel配置
|-- package.json
babel.config.js 只需引入我们要开发的插件即可,配置如下
// ./babel.config.js
module.exports = {
plugins: [
'./src/index.js'
],
}
我们可以通过 @babel/core 直接转换代码,并输出 ast, 方便我们进行观察 ast 结构以进行调整
// test.js
const babel = require("@babel/core");
const path = require('path');
(async () => {
// babel.transformFileAsync(file) 直接转换file文件中的代码 (默认使用 babel.config.js 配置)
const result = await babel.transformFileAsync(path.join(__dirname, './test.code.js'),{
ast: true, // 生成ast,默认不生成
});
// debug
console.log(result)
})();
设置 NODE_ENV ,使 @babel/cli 可以区分环境
// package.json
{
...,
"scripts": {
"build": "cross-env NODE_ENV=production babel ./src/test.code.js --out-dir dist",
"build:dev": "cross-env NODE_ENV=development babel ./src/test.code.js --out-dir dist"
}
}
三. 插件开发
1. 插件1 -- 区分环境、替换字符串、删除代码段
1.预期插件功能
// ./src/test.code.js
if (DEBUG) {
console.log(111);
}
// 注释
const b = 10;
const square = (n) => {
return n * n;
}
console.log(square(b))
区分开发环境与生产环境
- 开发环境下,可执行 if 语句, 输出 111;
- 生产环境下,删除 if 语句
实现:
- 根据 process.env.NODE_ENV 区分环境
- 开发环境下,将 DEBUG 关键字替换为字符串 “DEBUG” ,使 if 语句可执行
- 生产环境下,将包含 DEBUG 的 if 语句节点删除
2. ast 分析
使用 astexplorer.net/ 解析测试代码,可以看到输出的 ast 大致如下 (去除了位置数据)
// 递归删除位置数据
const delByKeys = (obj, needDelKeys = ['start', 'end', 'loc']) => {
return Object.keys(obj).reduce((pv, cv) => {
if (needDelKeys.includes(cv)) {
return pv;
}
if (obj[cv] instanceof Array) {
return {
...pv,
[cv]: obj[cv].map(item => delByKeys(item, needDelKeys))
}
}
if (obj[cv] instanceof Object) {
return {
...pv,
[cv]: delByKeys(obj[cv], needDelKeys),
}
}
return {
...pv,
[cv]: obj[cv],
}
}, {});
};
{
"type":"File",
"errors":[
],
"program":{
"type":"Program",
"sourceType":"module",
"interpreter":null,
"body":[
{
"type":"IfStatement",
"test":{
"type":"Identifier",
"name":"DEBUG"
},
"consequent":{
"type":"BlockStatement",
"body":[
{
"type":"ExpressionStatement",
"expression":{
"type":"CallExpression",
"callee":{
"type":"MemberExpression",
"object":{
"type":"Identifier",
"name":"console"
},
"computed":false,
"property":{
"type":"Identifier",
"name":"log"
}
},
"arguments":[
{
"type":"NumericLiteral",
"extra":{
"rawValue":111,
"raw":"111"
},
"value":111
}
]
}
}
],
"directives":[
]
},
"alternate":null,
"trailingComments":[
{
"type":"CommentLine",
"value":" 注释"
}
]
},
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"b"
},
"init":{
"type":"NumericLiteral",
"extra":{
"rawValue":10,
"raw":"10"
},
"value":10
}
}
],
"kind":"const",
"leadingComments":[
{
"type":"CommentLine",
"value":" 注释"
}
]
},
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"square"
},
"init":{
"type":"ArrowFunctionExpression",
"id":null,
"generator":false,
"async":false,
"params":[
{
"type":"Identifier",
"name":"n"
}
],
"body":{
"type":"BlockStatement",
"body":[
{
"type":"ReturnStatement",
"argument":{
"type":"BinaryExpression",
"left":{
"type":"Identifier",
"name":"n"
},
"operator":"*",
"right":{
"type":"Identifier",
"name":"n"
}
}
}
],
"directives":[
]
}
}
}
],
"kind":"const"
},
{
"type":"ExpressionStatement",
"expression":{
"type":"CallExpression",
"callee":{
"type":"MemberExpression",
"object":{
"type":"Identifier",
"name":"console"
},
"computed":false,
"property":{
"type":"Identifier",
"name":"log"
}
},
"arguments":[
{
"type":"CallExpression",
"callee":{
"type":"Identifier",
"name":"square"
},
"arguments":[
{
"type":"Identifier",
"name":"b"
}
]
}
]
}
}
],
"directives":[
]
},
"comments":[
{
"type":"CommentLine",
"value":" 注释"
}
]
}
我们只需要找到 if(DEBUG) 部分进行处理即可
3. Visitors(访问者)
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。
module.exports = () => {
return {
visitor: {
// 遍历过程中,每遇到一个 Identifier 节点就会触发
Identifier: {
enter(path) {
}
},
// 遍历过程中,每遇到一个 FunctionDeclaration 节点就会触发
// FunctionDeclaration() { ... } 是 FunctionDeclaration: { enter() { ... } } 的简写形式。.
FunctionDeclaration() {},
}
}
}
4. Paths(路径)
Path 是表示两个节点之间连接的对象, 描述了节点的属性与节点之间的关系, 包含了一些属性和方法
| api | 功能 |
|---|---|
| node | 当前节点的数据 |
| parent | 父节点 |
| replaceWith(node) | 将当前节点替换为node |
| replaceWithMultiple([node]) | 将当前节点替换为多个node |
| remove() | 移除当前节点(包括其下的所有节点) |
| traverse(vistor) | 在当前path下再嵌套visitor,避免处理打预期外的节点 |
5. 实现
通过执行 node ./src/test.js ,可以输出代码的 ast,然后进行调整
module.exports = ({types: t}) => {
return {
visitor: {
// 遍历过程中,每遇到一个Identifier节点就会触发
Identifier: {
enter(path) {
// 判断该节点是 DEBUG,并且在 if 里
if (path.node.name === 'DEBUG' && t.isIfStatement(path.parent)) {
// 开发环境
if (process.env.NODE_ENV === 'development') {
// 替换为 新创建的 string节点
path.replaceWith(t.stringLiteral('MY_DEBUG'));
} else {
// 移除
path.parentPath.remove();
}
}
}
}
}
}
}
2. 插件2 -- 使用 require(xx) 时加上 default,已经有 default 的不处理
1.预期插件功能
// ./src/test.code.js
const m = require('./test.require.js'); // 转换为=> const m = require('./test.require.js').default;
const n = require('./test.require2.js').default; // 已有default,不作处理
2. ast 分析
{
"type":"File",
"errors":[
],
"program":{
"type":"Program",
"sourceType":"module",
"interpreter":null,
"body":[
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"m"
},
"init":{
"type":"CallExpression",
"callee":{
"type":"Identifier",
"name":"require"
},
"arguments":[
{
"type":"StringLiteral",
"extra":{
"rawValue":"./test.require.js",
"raw":"'./test.require.js'"
},
"value":"./test.require.js"
}
]
}
}
],
"kind":"const"
},
{
"type":"VariableDeclaration",
"declarations":[
{
"type":"VariableDeclarator",
"id":{
"type":"Identifier",
"name":"n"
},
"init":{
"type":"MemberExpression",
"object":{
"type":"CallExpression",
"callee":{
"type":"Identifier",
"name":"require"
},
"arguments":[
{
"type":"StringLiteral",
"extra":{
"rawValue":"./test.require.js",
"raw":"'./test.require.js'"
},
"value":"./test.require.js"
}
]
},
"computed":false,
"property":{
"type":"Identifier",
"name":"default"
}
}
}
],
"kind":"const"
}
],
"directives":[
]
},
"comments":[
]
}
可以看到,
- require 关键字类型是 CallExpression
- 有 default 的 require 的父节点类型为 MemberExpression
思路
- 仿照 require('./test.require2.js').default 的 ast 创建 MemberExpression 节点,替换掉 require('./test.require.js') 的 ast
3. 实现
module.exports = ({types: t}) => {
return {
visitor: {
CallExpression: {
enter(path) {
// 遍历 CallExpression ,并找到 所有require
if (path.node.callee.name === 'require') {
// 已经有 .default的,不改动
if (t.isIdentifier(path.parent.property) && path.parent.property.name === 'default'){
return;
}else {
// 没有default, 新建 MemberExpression 节点;其中包含 require callExpression节点(子节点为 identifier)、require中的参数(stringLiteral节点)、 default属性(identifier)
const newNode = t.memberExpression(t.callExpression(t.identifier('require'), path.node.arguments.map(item => t.stringLiteral(item.value))), t.identifier('default'));
// 替换
path.replaceWith(newNode);
}
}
}
}
}
}
}
四. 参考
插件编写教材: github.com/jamiebuilds…
@babel/types api: babeljs.io/docs/en/bab…