共计 11506 个字符,预计需要花费 29 分钟才能阅读完成。
学习的背景(为啥 要写 一个 Babel 插件呢?)
- es6 是如何转换为 es5 的?
- 什么是 AST 语法树呢,怎么对一个 AST 树 的节点 进行增删改查呢?
- 为啥 之前 jsx 须要 手动导入 react,当初不须要了?
- 国际化内容 须要写 t 函数的 中央太多,懒得写了。(业务方面)
- 任何你能够想到的骚操作。
1. babel 罕用包的介绍(写插件必备常识)
代码 转 语法树的 官网:https://astexplorer.net/
1. Babylon 是 Babel 的解析器,代码转为 AST 语法树
npm init -y
进行我的项目的初始化 搭建- Babylon 是 Babel 的解析器,是 将 代码 转换为 AST 语法树的 工具,当初来装置它
npm install --save babylon
(PS:
新版本 的 babel 改名为 @babel/parser,仅仅是名字的更改,上面局部包的名字也有所更改然而 API 的用法大抵不变) - 新增
babylon-demo.mjs
(留神是 mjs 结尾的,方便使用 ESmodule 语法),写入 如下内容。调用 babylon.parse生成 ast 语法树
import * as babylon from "babylon";
const code = `function square(n) {return n * n;}`;
const ast = babylon.parse(code);
console.log(ast);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }
2. Babel-traverse 来操作 AST 语法树
npm install --save babel-traverse
装置 依赖。- 利用 语法树 将 code 中的 n 替换为 x。(别急 下一步 就是 依据新的 语法树 生成代码)
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {return n * n;}`;
const ast = babylon.parse(code);
// 对 形象语法树 一层层的 遍历
traverse.default(ast, {
// 树 的节点 会 作为 参数 传入 enter 函数
enter(path) {
// 如果以后节点 是 Identifier 并且 name 是 n。就替换为 x
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {path.node.name = "x";}
}
});
3. babel-generator
依据批改的语法树 生成代码 和源码映射(source map)
- 装置 依赖
npm install --save babel-generator
- 将 AST 语法树 生成代码
import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";
// 原始代码
const code = `function square(n) {return n * n;}`;
// ast 是对象 属于援用型
const ast = babylon.parse(code);
// 对 形象语法树 一层层的 遍历
traverse.default(ast, {
// 树 的节点 会 作为 参数 传入 enter 函数
enter(path) {
// 如果以后节点 是 Identifier 并且 name 是 n。就替换为 x
// 因为 ast 是对象,所以 此处做的变更会 间接影响到 ast
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {path.node.name = "x";}
},
});
// 对节点操作过当前的代码
const targetCode = generate.default(ast).code
console.log('targetCode', targetCode)
// targetCode function square(x) {
// return x * x;
// }
4. 发现对节点的判断 须要写的代码很多,抽离出公共的包来进行节点的判断。babel-types
(AST 节点里的 Lodash 式工具库)
- 装置:
npm install --save babel-types
- 优化下面代码的 AST 节点的 if 判断。
import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";
// 留神 node_modules 模块里 导出的是 default
import {default as t} from "babel-types";
// 原始代码
const code = `function square(n) {return n * n;}`;
// ast 是对象 属于援用型
const ast = babylon.parse(code);
// 对 形象语法树 一层层的 遍历
traverse.default(ast, {
// 树 的节点 会 作为 参数 传入 enter 函数
enter(path) {
// 如果以后节点 是 Identifier 并且 name 是 n。就替换为 x
// 因为 ast 是对象,所以 此处做的变更会 间接影响到 ast
// if (
// path.node.type === "Identifier" &&
// path.node.name === "n"
// ) {
// path.node.name = "x";
// }
if (t.isIdentifier(path.node, {name: "n"})) {path.node.name = "x"}
},
});
// 对节点操作过当前的代码
const targetCode = generate.default(ast).code
console.log('targetCode', targetCode)
// targetCode function square(x) {
// return x * x;
// }
5. 通过 AST 来生成 CODE 可读性 太差。应用 babel-template
来实现占位符的来生成代码。
- 装置依赖:
npm install --save babel-template
- 以后的需要是:我不想手动导入 文件 a 依赖。即:const a = require(“a”);这句话 我不想写。
- 首先构建 ast 的模板:判断哪些是变量,哪些是 语法。
// 构建模板
const buildRequire = template(`
const IMPORT_NAME = require(SOURCE);
`);
- 应用 变量 进行 填充
// 创立 ast
const astImport = buildRequire({IMPORT_NAME: t.identifier("a"),
SOURCE: t.stringLiteral("a")
});
- 剖析 何时塞入 这段 ast。应用 https://astexplorer.net/ 剖析 得悉。代码和 图片如下
import * as babylon from "babylon";
import traverse from "babel-traverse";
import generate from "babel-generator";
import {default as template} from "babel-template";
// 留神 node_modules 模块里 导出的是 default
import {default as t} from "babel-types";
// 构建模板
const buildRequire = template(`
const IMPORT_NAME = require(SOURCE);
`);
// 创立 ast
const astImport = buildRequire({IMPORT_NAME: t.identifier("a"),
SOURCE: t.stringLiteral("a")
});
// 原始代码
const code = `
function square(n) {return n * n;}`;
// ast 是对象 属于援用型
const ast = babylon.parse(code);
// 对 形象语法树 一层层的 遍历
traverse.default(ast, {
// 树 的节点 会 作为 参数 传入 enter 函数
enter(path) {
// 如果以后节点 是 Identifier 并且 name 是 n。就替换为 x
// 因为 ast 是对象,所以 此处做的变更会 间接影响到 ast
// if (
// path.node.type === "Identifier" &&
// path.node.name === "n"
// ) {
// path.node.name = "x";
// }
if (t.isIdentifier(path.node, {name: "n"})) {path.node.name = "x"}
// 在程序的结尾 塞进去 我的 ast
if (t.isProgram(path.node)) {console.log('塞入我写的 ast')
path.node.body.unshift(astImport)
}
},
});
// 对节点操作过当前的代码
const targetCode = generate.default(ast).code
console.log('targetCode', targetCode)
// 塞入我写的 ast
// targetCode const a = require("a");
// function square(x) {
// return x * x;
// }
2. 开始 撸 Babel 的插件
1. 开始撸插件代码 之前 必须要有一个 不便调试的 babel 的环境
- 装置 babel 外围包 @babel/core(文档:https://www.babeljs.cn/docs/u…)。
npm install --save-dev @babel/core
- 新建 demo 代码
index.js
// index.js
let bad = true;
const square = n => n * n;
-
新建插件
plugin2.js
// 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';} } } }; };
- 新建
core-demo.js
应用 babel-core 来编译 代码
const babel = require("@babel/core"); const path = require("path"); const fs = require("fs"); // 导入 index.js 的代码 并应用 插件 plugin2 转换 babel.transformFileAsync('./index.js', {plugins: [path.join(__dirname,'./plugin2.js')], }).then(res => {console.log(res.code); // 转换后的代码 写入 dist.js 文件 fs.writeFileSync(path.join(__dirname,'./dist.js'), res.code, {encoding: 'utf8'}); })
- 测试 断点是否失效(不便前期调试)
vscode 中 新建 debug 终端
- 新建
2. 应用 nodemon 包优化环境,进步调试的效率(nodemon + debug 提高效率)
- 装置依赖:
npm i nodemon
- 配置 package.json 的 script 命令为:(监听文件变更时候疏忽 dist.js,因为 dist 的变更会引起 脚本的从新执行,脚本的从新执行又 产生新的 dist.js)
"babylon": "nodemon core-demo.js --ignore dist.js"
- 开启 debug 终端,运行
npm run babylon
即可看到文件变更 会主动走到断点里
3. 开始进行 babel 插件的实战
本文并未具体介绍所有的 babel path 节点的相干 api,具体的 对于 path 节点的相干文档 请见 官网举荐文档(中文 有点老旧)或者 依据官网原版 英文文档 翻译的 中文文档(曾经向 官网 提了 PR 然而暂未合并),举荐的 是 先看 此文档,发现其中 局部 api 不相熟 的时候 再去查 api 文档,印象粗浅。
1. babel 插件的 API 标准
- Babel 插件 实质上是一个函数,该函数 承受 babel 作为参数,通过 会 应用
babel
参数里的types
函数
export default function(babel) {// plugin contents}
// or
export default function({types}) {// plugin contents}
- 返回的 是一个 对象。对象的
visitor
属性是这个插件的次要访问者。visitor
的 每个函数中 都会承受 2 个 参数:path
和state
export default function({types: t}) {
return {
visitor: {
// 此处的函数 名 是从 ast 里 取的
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {}}
};
};
2. 来个 demo 实现 ast 层面的 代码替换
目标: 将 foo === bar;
转为 replaceFoo !== myBar;
- 首先 通过 https://astexplorer.net/ 来剖析 ast 构造。
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}
- 从
BinaryExpression
增加 访问者 进行 ast 节点解决,能够 看到 当operator
为 === 的时候 须要进行解决。代码如下
// plugin.js
module.exports = function({types}) {console.log('t')
return {
visitor: {BinaryExpression(path, state) {console.log('path1', path);
// 不是 !== 语法的 间接返回
if (path.node.operator !== '===') {return;}
},
}
};
};
-
进行 ast 节点的 更改,因为 ast 是一个对象,能够 对 path 字段 间接更改其属性值即可。比方 将 left 和 right 节点 的 name 进行批改。
// plugin.js module.exports = function({types}) {console.log('t') return { visitor: {BinaryExpression(path, state) {console.log('path1', path); if (path.node.operator !== '===') {return;} if (path.node.operator === '===') {path.node.operator = '!=='} if (path.node.left.name === 'foo') {path.node.left.name = 'replaceFoo'} if (path.node.right.name === 'bar') {path.node.right.name = 'myBar';} }, } }; };
-
从 index.js 通过 上述 babel 插件解决当前得出 dist.js 内容为:
// index.js foo === bar a = 123 // babel 插件解决后 replaceFoo !== myBar; a = 123;
-
3. 上一大节 把握了 ast 节点 根底的 批改 和 拜访,加深一下 ast 节点的操作
1. 获取 ast 节点的 属性值:path.node.property
BinaryExpression(path) {
path.node.left;
path.node.right;
path.node.operator;
}
2. 获取 该属性 外部的 path (节点信息):path.get(xxx)
BinaryExpression(path) {path.get('left'); // 返回的是一个 path 性的
}
Program(path) {path.get('body.0');
}
3. 查看节点的类型,通过 babel 参数自带的 types 函数进行查看。
- 简略判断节点的类型
// plugin.js
module.exports = function({types: t}) {console.log('t')
return {
visitor: {BinaryExpression(path, state) {console.log('path1', path.get('left'));
if (path.node.operator !== '===') {return;}
if (path.node.operator === '===') {path.node.operator = '!=='}
// 等同于 path.node.left.type === "Identifier"
if (t.isIdentifier(path.node.left)) {path.node.left.name = 'replaceFoo'}
},
}
};
};
- 判断节点的类型,外加 浅层属性的校验
BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n"})) {// ...}
}
性能上等同于:
BinaryExpression(path) {
if (
path.node.left != null &&
path.node.left.type === "Identifier" &&
path.node.left.name === "n"
) {// ...}
}
4. 再来一道对于 ast 操作节点的题小试本领(要害还是学会看 ast 语法树和 尝试一些 ast 节点相干的 api)
以后程序代码为:
function square(n) {return n * n;}
const a = 2;
console.log(square(a));
指标程序代码是:
function newSquare(n, left) {return left ** n;}
const a = 2;
console.log(newSquare(a, 222));
整体操作 ast 语法树的剖析逻辑:(结尾会放残缺代码)
- 将
square
函数命名 进行 更名,改为newSquare
- 将
newSquare
(因为square
参数 节点的 ast 名称 曾经改为了newSquare
)的入参减少 一个left
参数 - 将
n * n
进行 替换,换成left ** n;
- 在调用
square
处 进行批改,首先将函数名 改为newSquare
,而后在,对该函数的入参减少 一个222
1. 首先剖析 原代码的 ast 语法树
能够看到以后程序 代码 被解析为 3 段 ast 语法树 节点
2. 接下来剖析 函数定义 的这个节点
鼠标滑选 1-3 行,发现右侧 主动开展了。
3. 进行第一步:将 square
函数命名 进行 更名,改为 newSquare
由图看出,如何确定 以后的节点是 square 函数的命名 节点呢?(1 分钟 思考一下)。
- 节点的类型首先是:Identifier 类型,并且 以后节点 的
name
字段是 square - 节点的 父级 节点的 类型 是 FunctionDeclaration 的。
伪代码如下:
// 新建 变量,记录 新函数的函数名
const newName = 'newSquare';
// 获取以后 函数的 父级。查找最靠近的父函数或程序:const parentFunc = path.getFunctionParent();
if (parentFunc) {
// 以后父节点 是 square 函数 并且以后的节点的 key 是 id(此处是为了确认 square 的函数命名节点)。// 而后对此函数进行重命名 从 square 改为 newName
if (
parentFunc.node.id.name === "square" &&
path.key === "id"
) {console.log("对 square 进行重命名:", newName);
path.node.name = newName;
}
}
4. 接下来 将 newSquare
的入参减少 一个 left
参数。
- 以后节点 的 类型 是
Identifier
类型,并且是 在 名为params
的 列表里 (列表,就意味着 能够 进行 增删改查了) - 以后节点的 父级 节点类型 是 FunctionDeclaration 的,并且 父级节点下的 id 的 name 属性 曾经变更为了
newSquare
伪代码如下:
// 以后父节点 是 square 函数 并且以后的节点的 listKey 是 params(此处是为了排除 square 的函数命名节点)。// 此处是在重命名后才会走的 逻辑 所以 该节点 父级的 名称判断用的是 newName 而不是 square
if (
parentFunc.type === "FunctionDeclaration" &&
parentFunc.node.id.name === newName &&
path.listKey === "params"
) {console.log("新增函数参数 left");
path.container.push(t.identifier("left"));
}
5. 将 n * n
进行 替换,换成 left ** n;
- 发现 如果单纯的 去 操作 Identifier类型的 n 状况有些多,并且 当前情况 还要 判断 操作符(operator)是不是
*
, 换个思路,去操作 BinaryExpression 类型的数据 -
在 BinaryExpression类型 中,仅仅 须要 判断 以后
operator
的 属性 是不是 咱们须要的*
伪代码如下:
BinaryExpression(path, state) {if (path.node.operator !== "*") return; console.log("BinaryExpression"); // 替换一个节点 path.replaceWith(// t.binaryExpression("**", path.node.left, t.NumericLiteral(2)) t.binaryExpression("**", t.identifier("left"), t.identifier("n")) ); },
6. 最初一步:在调用 square
处 进行批改,首先将函数名 改为 newSquare
,而后在,对该函数的入参减少 一个 222
- 指标 是将 name 字段的
square
字段 改为newSquare
。
办法一:其 父级节点 是一个 CallExpression
,间接在其 父级节点 操作 它。
伪代码 如下:
CallExpression(path, state) {console.log("CallExpression");
// 以后被调用函数的 名称 是 square
if (path.node.callee.name === 'square') {console.log("在 CallExpression 中,更改 被调用 函数 square 的名字 为", newName);
path.node.callee.name = newName;
}
},
办法二:通过 节点 Identifier
进行操作
- 判断以后 节点的属性是
callee
示意是被调用的,并且 以后 节点的 名字 为square
伪代码如下:
// 判断是不是 square 的函数调用
if (path.key === 'callee' && path.isIdentifier({name: 'square'})) {console.log("对 square 函数调用进行重命名", newName);
path.node.name = newName;
}
7. 总结 以及 全副代码
到当初,你会发现其实 对 ast 语法树的操作,次要还是 操作一个 ast 语法树的对象,只有 对 ast 语法树 对象 进行 合乎 ast 语法树 相干规定的 属性的 更改,babel 就会 主动 解决 ast 语法树对象 并生成 新的 代码。
残缺代码地址
外围代码
// square-plugin.js
// 新建 变量,记录 新函数的函数名
const newName = 'newSquare';
module.exports = function ({types: t}) {
return {
visitor: {Identifier(path, state) {console.log("走进 Identifier");
if (path.parentPath && path.listKey === 'arguments') {console.log("减少参数");
path.container.push(t.NumericLiteral(222));
return;
}
// 获取以后 函数的 父级。查找最靠近的父函数或程序:const parentFunc = path.getFunctionParent();
if (parentFunc) {
// 以后父节点 是 square 函数 并且以后的节点的 listKey 是 params(此处是为了排除 square 的函数命名节点)。// 此处是在重命名后才会走的 逻辑 所以 该节点 父级的 名称判断用的是 newName 而不是 square
if (
parentFunc.type === "FunctionDeclaration" &&
parentFunc.node.id.name === newName &&
path.listKey === "params"
) {console.log("新增函数参数 left");
path.container.push(t.identifier("left"));
}
// 以后父节点 是 square 函数 并且以后的节点的 key 是 id(此处是为了确认 square 的函数命名节点)。// 而后对此函数进行重命名 从 square 改为 newName
if (
parentFunc.node.id.name === "square" &&
path.key === "id"
) {console.log("对 square 进行重命名:", newName);
path.node.name = newName;
}
}
// 办法二:判断是不是 square 的函数调用
// if (path.key === 'callee' && path.isIdentifier({name: 'square'})) {// console.log("对 square 函数调用进行重命名", newName);
// path.node.name = newName;
// }
},
BinaryExpression(path, state) {if (path.node.operator !== "*") return;
console.log("BinaryExpression");
// 替换一个节点
path.replaceWith(// t.binaryExpression("**", path.node.left, t.NumericLiteral(2))
t.binaryExpression("**", t.identifier("left"), t.identifier("n"))
);
},
CallExpression(path, state) {console.log("CallExpression");
// 办法 1:以后被调用函数的 名称 是 square
if (path.node.callee.name === 'square') {console.log("在 CallExpression 中,更改 被调用 函数 square 的名字 为", newName);
path.node.callee.name = newName;
}
},
FunctionDeclaration(path, state) {console.log("FunctionDeclaration");
// const params = path.get('params');
// const params = path.get('params');
// params.push(t.identifier('left'));
// console.log('FunctionDeclaration end', path);
// path.params = params;
// path.params.push(t.identifier('right'));
},
},
};
};
本文由 mdnice 多平台公布