目录
在create-reate-app中使用我们手写的babel插件
前言
需求:做性能埋点,每个函数都要处理。
想法:自动埋点。
解释:埋点只是在函数里插入了一段代码,这段代码不影响其他逻辑。
概念:这种函数中 插入不影响逻辑代码的手段 叫做函数插桩。
工具:babel。
// 想到的函数类型
import aa from 'aa';
import * as bb from 'bb';
import {cc} from 'cc';
import 'dd';
function a () {
console.log('aaa');
}
class B {
bb() {
return 'bbb';
}
}
const c = () => 'ccc';
const d = function () {
console.log('ddd');
}
// 不同类型函数达到的埋点效果
import _tracker2 from "tracker";
import aa from 'aa';
import * as bb from 'bb';
import { cc } from 'cc';
import 'dd';
function a() {
_tracker2();
console.log('aaa');
}
class B {
bb() {
_tracker2();
return 'bbb';
}
}
const c = () => {
_tracker2();
return 'ccc';
};
const d = function () {
_tracker2();
console.log('ddd');
};
实现思路:
·引入 tracker 模块。如果已经引入就不再引入,没有的话就引入,并且生成唯一 ID 作为标识符
·对所有函数在函数体内插入 tracker 代码
AST
// 留意到 AST 的每一层都拥有相同的结构
{
type: "FunctionDeclaration",
id: {...},
params: [...],
body: {...}
}
{
type: "Identifier",
name: ...
}
{
type: "BinaryExpression",
operator: ...,
left: {...},
right: {...}
}
// Babel 还为每个节点额外生成了一些属性,用于描述该节点在原始代码中的位置
{
type: ...,
start: 0,
end: 38,
loc: {
start: {
line: 1,
column: 0
},
end: {
line: 3,
column: 1
}
},
...
}
// 每一个节点都会有 start、end、loc 这几个属性。
Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。
转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。这是 Babel 或是其他编译器中最复杂的过程。同时也是插件将要介入工作的部分,这将是本手册的主要内容。
遍历
Visitors
当我们谈及“进入”一个节点,实际上是说我们在访问它们,之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。
访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。
const MyVisitor = {
Identifier() {
console.log("Called!");
}
};
// 你也可以先创建一个访问者对象,并在稍后给它添加方法。
let visitor = {};
visitor.MemberExpression = function() {};
visitor.FunctionDeclaration = function() {}
// 使用访问者
path.traverse(MyVisitor);
Paths(路径)
AST 通常会有许多节点,那么节点直接如何相互关联呢?可以用 Paths(路径)来简化这件事情。Path 是表示两个节点之间连接的对象。
// 例如,如果有下面这样一个节点及其子节点
{
type: "FunctionDeclaration",
id: {
type: "Identifier",
name: "square"
},
...
}
//将子节点 Identifier 表示为一个路径(Path)的话,看起来是这样的:
{
"parent": {
"type": "FunctionDeclaration",
"id": {...},
....
},
"node": {
"type": "Identifier",
"name": "square"
}
}
同时,还包含关于该路径的其他元数据。当然路径对象还包含添加、更新、移动和删除节点有关的其他很多方法。
{
"parent": {...},
"node": {...},
"hub": {...},
"contexts": [],
"data": {},
"shouldSkip": false,
"shouldStop": false,
"removed": false,
"state": null,
"opts": null,
"skipKeys": null,
"parentPath": null,
"context": null,
"container": null,
"listKey": null,
"inList": false,
"parentKey": null,
"key": null,
"scope": null,
"type": null,
"typeAnnotation": null
}
在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式 reactive 表示。当你调用一个修改树的方法后,路径信息也会被更新。Babel 帮你管理这一切,从而使得节点操作简单,尽可能做到无状态。
Paths in Visitors(存在于访问者中的路径)
当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。通过这种方式,你操作的就是节点的响应式表示(即路径)而非节点本身。
const MyVisitor = {
Identifier(path) {
console.log("Visiting: " + path.node.name);
}
};
a + b + c;
path.traverse(MyVisitor);
Visiting: a
Visiting: b
Visiting: c
State(状态)
状态是抽象语法树 AST 转换的敌人,状态管理会不断牵扯你的精力,而且几乎所有你对状态的假设,总是会有一些未考虑到的语法最终证明你的假设是错误的。
Scopes(作用域)
JavaScript 支持词法作用域,在树状嵌套结构中代码块创建出新的作用域。
当编写一个转换时,必须小心作用域。我们得确保在改变代码的各个部分时不会破坏已经存在的代码。
// 作用域可以被表示为如下形式:
{
path: path,
block: path.node,
parentBlock: path.parent,
parent: parentScope,
bindings: [...]
}
当你创建一个新的作用域时,需要给出它的路径和父作用域,之后在遍历过程中它会在该作用域内收集所有的引用(“绑定”)。
一旦引用收集完毕,你就可以在作用域(Scopes)上使用各种方法,稍后我们会了解这些方法。
Bindings(绑定)
所有引用属于特定的作用域,引用和作用域的这种关系被称作:绑定(binding)。
// 单个绑定看起来像这样
Text for Translation
{
identifier: node,
scope: scope,
path: path,
kind: 'var',
referenced: true,
references: 3,
referencePaths: [path, path, path],
constant: false,
constantViolations: [path]
}
有了这些信息你就可以查找一个绑定的所有引用,并且知道这是什么类型的绑定(参数,定义等等),查找它所属的作用域,或者拷贝它的标识符。 你甚至可以知道它是不是常量,如果不是,那么是哪个路径修改了它。
在很多情况下,知道一个绑定是否是常量非常有用,最有用的一种情形就是代码压缩时。
API
babylon
Babylon 是 Babel 的解析器。最初是从 Acorn 项目 fork 出来的。Acorn 非常快,易于使用,并且针对非标准特性(以及那些未来的标准特性)设计了一个基于插件的架构。
// 使用方式
import * as babylon from "babylon";
const code = `function square(n) {
return n * n;
}`;
babylon.parse(code);
// Node {
// type: "File",
// start: 0,
// end: 38,
// loc: SourceLocation {...},
// program: Node {...},
// comments: [],
// tokens: [...]
// }
babel-traverse
Babel Traverse(遍历)模块维护了整棵树的状态,并且负责替换、移除和添加节点。
// 使用方式:可以和 Babylon 一起使用来遍历和更新节点
import * as babylon from "babylon";
import traverse from "babel-traverse";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
traverse(ast, {
enter(path) {
if (
path.node.type === "Identifier" &&
path.node.name === "n"
) {
path.node.name = "x";
}
}
});
babel-types
Babel Types 模块是一个用于 AST 节点的 Lodash 式工具库(Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数),它包含了构造、验证以及变换 AST 节点的方法。该工具库包含考虑周到的工具方法,对编写处理 AST 逻辑非常有用。
Definitions(定义)
Babel Types 模块拥有每一个单一类型节点的定义,包括节点包含哪些属性,什么是合法值,如何构建节点、遍历节点,以及节点的别名等信息。
// 单一节点类型的定义形式如下
defineType("BinaryExpression", {
builder: ["operator", "left", "right"],
fields: {
operator: {
validate: assertValueType("string")
},
left: {
validate: assertNodeType("Expression")
},
right: {
validate: assertNodeType("Expression")
}
},
visitor: ["left", "right"],
aliases: ["Binary", "Expression"]
});
Builders(构建器)
你会注意到上面的 BinaryExpression 定义有一个 builder 字段。这是由于每一个节点类型都有构造器方法 builder,按类似下面的方式使用:
t.binaryExpression("*", t.identifier("a"), t.identifier("b"));
可以创建如下所示的 AST:
{
type: "BinaryExpression",
operator: "*",
left: {
type: "Identifier",
name: "a"
},
right: {
type: "Identifier",
name: "b"
}
}
当打印出来之后是这样的:
a * b
Validators(验证器)
BinaryExpression 的定义还包含了节点的字段 fields 信息,以及如何验证这些字段。
// 可以创建两种验证方法
// 第一种 isX
t.isBinaryExpression(maybeBinaryExpressionNode)
// 这个测试也可以传入第二个参数来确保节点包含特定的属性和值
t.isBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// 第二种 断言式。会抛出异常而不是返回 true 或 false
t.assertBinaryExpression(maybeBinaryExpressionNode);
t.assertBinaryExpression(maybeBinaryExpressionNode, { operator: "*" });
// Error: Expected type "BinaryExpression" with option { "operator": "*" }
Converters(变换器)
babel-generator
Babel Generator 模块是 Babel 的代码生成器,它读取 AST 并将其转换为代码和源码映射(sourcemaps)。
// 使用方式
import * as babylon from "babylon";
import generate from "babel-generator";
const code = `function square(n) {
return n * n;
}`;
const ast = babylon.parse(code);
generate(ast, {}, code);
// {
// code: "...",
// map: "..."
// }
babel-template
babel-template 是另一个虽然很小但却非常有用的模块。它能让你编写字符串形式且带有占位符的代码来代替手动编码,尤其是生成的大规模 AST 的时候。在计算机科学中,这种能力被称作准引用(quasiquotes)。
// 使用方式
import template from "babel-template";
import generate from "babel-generator";
import * as t from "babel-types";
const buildRequire = template(`
var IMPORT_NAME = require(SOURCE);
`);
const ast = buildRequire({
IMPORT_NAME: t.identifier("myModule"),
SOURCE: t.stringLiteral("my-module")
});
console.log(generate(ast).code);
var myModule = require("my-module");
编写第一个 Babel 插件
// 先从一个接收了当前 babel 对象作为参数的 function 开始。
export default function( babel ) {
// plugin contents
}
// 由于你将会经常这样使用,所以直接取出 babel.types 会更方便;
export default function( {types: t} ) {
// plugin contents
}
// 接着返回一个对象,其 visitor 属性是这个插件的主要访问者
export default function( {types: t} ) {
return {
visitor: {
// visitor contents
}
}
}
// Visitor 中的每个函数接收2个参数:path 和 state
export default function( {types: t} ) {
return {
visitor: {
Identifier(path, state) {},
ASTNodeTypeHere(path, state) {}
}
}
}
// 让我们快速编写一个可用的插件来展示一下它是如何工作的。下面是我们的源码:
foo === bar;
// 其 AST 形式如下:
{
type: "BinaryExpression",
operator: "===",
left: {
type: "Identifier",
name: "foo"
},
right: {
type: "Identifier",
name: "bar"
}
}
// 我们从添加 BinaryExpression 访问者方法开始:
export default function({ types: t }) {
return {
visitor: {
BinaryExpression(path) {
// ...
}
}
}
}
// 然后我们更确切一些,只关注哪些使用了 === 的 BinaryExpression
visitor: {
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
// ...
}
}
// 现在我们用新的标识符来替换 left 属性:
BinaryExpression(path) {
if (path.node.operator !== "===") {
return;
}
path.node.left = t.identifier("sebmck");
// ...
}
// 于是如果我们运行这个插件我们会得到:
sebmck === bar;
// 现在只需要替换 right 属性了。
BinaryExpression(path) {
if (path.node.oper