本文主要是介绍Babel基础知识及实现埋点插件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
前言
AST
遍历
Visitors
Paths(路径)
Paths in Visitors(存在于访问者中的路径)
State(状态)
Scopes(作用域)
Bindings(绑定)
API
babylon
babel-traverse
babel-types
Definitions(定义)
Builders(构建器)
Validators(验证器)
Converters(变换器)
babel-generator
babel-template
编写第一个 Babel 插件
转换操作
访问
获取子节点的 Path
检查节点的类型
检查路径(Path)类型
检查标识符(Identifier)是否被引用
找到特定的父路径
获取同级路径
停止遍历
处理
替换一个节点
用多节点替换单节点
用字符串源码替换节点
插入兄弟节点
插入到容器(container)中
删除一个节点
替换父节点
删除父节点
Scope(作用域)
检查本地变量是否被绑定
创建一个 UID
提升变量声明至父级作用域
重命名绑定及其引用
案例1 代码实现
模块引入
函数插桩
插件使用
小结
案例2 代码实现
安装依赖
编写入口文件
编写插件
小结
案例3 注释埋点
效果展示
插件使用
源代码
插件编写
案例4 埋点传参
参数放在注释中
源代码
插件使用
编写插件
参数放在局部作用域中
源代码
编写插件
小结
在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.operator !== "===") {return;}path.node.left = t.identifier("sebmck");path.node.right = t.identifier("dork");
}
// 这就是我们的最终结果了:
sebmck === dork;
// 完美!我们的第一个 Babel 插件。
转换操作
访问
获取子节点的 Path
为了得到一个 AST 节点的属性值,我们一般先访问到该节点,然后利用 path.node.property 方法即可。
// the BinaryExpression AST node has properties: `left`, `right`, `operator`
BinaryExpression(path) {path.node.left;path.node.right;path.node.operator;
}// 如果你想访问到该属性内部的 path,使用 path 对象的 get 方法,传递该属性的字符串形式作为参数。
BinaryExpression(path) {path.get('left');
}
Program(path) {path.get('body.0');
}
检查节点的类型
// 如果你想检查节点的类型,最好的方式是:
BinaryExpression(path) {if (t.isIdentifier(path.node.left)) {// ...}
}// 你同样可以对节点的属性们做浅层检查
BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n" })) {// ...}
}
检查路径(Path)类型
// 一个路径具有相同的方法检查节点的类型
BinaryExpression(path) {if (path.get('left').isIdentifier({ name: "n" })) {// ...}
}// 就相当于:
BinaryExpression(path) {if (t.isIdentifier(path.node.left, { name: "n" })) {// ...}
}
检查标识符(Identifier)是否被引用
Identifier(path) {if (path.isReferencedIdentifier()) {// ...}
}// 或者:
Identifier(path) {if (t.isReferenced(path.node, path.parent)) {// ...}
}
找到特定的父路径
有时你需要从一个路径向上遍历语法树,直到满足相应的条件。
对于每一个父路径调用callback
并将其NodePath
当作参数,当callback
返回真值时,则将其NodePath
返回。
path.findParent((path) => path.isObjectExpression());
如果也需要遍历当前节点:
path.find((path) => path.isObjectExpression());
查找最接近的父函数或程序:
path.getFunctionParent();
向上遍历语法树,直到找到在列表中的父节点路径
path.getStatementParent();
获取同级路径
如果一个路径是在一个 Function
/Program
中的列表里面,它就有同级节点。
- 使用
path.inList
来判断路径是否有同级节点, - 使用
path.getSibling(index)
来获得同级路径, - 使用
path.key
获取路径所在容器的索引, - 使用
path.container
获取路径的容器(包含所有同级节点的数组) - 使用
path.listKey
获取容器的key
var a = 1;
// pathA, path.key = 0 var b = 2;
// pathB, path.key = 1 var c = 3;
// pathC, path.key = 2```js
export default function({ types: t }) {return {visitor: {VariableDeclaration(path) {// if the current path is pathApath.inList // truepath.listKey // "body"path.key // 0path.getSibling(0) // pathApath.getSibling(path.key + 1) // pathBpath.container // [pathA, pathB, pathC]}}};
}
停止遍历
如果你的插件需要在某种情况下不运行,最简单的做法是尽早写回。
BinaryExpression(path) {if (path.node.operator !== '**') return;
}
如果您在顶级路径中进行子遍历,则可以使用2个提供的API方法:
path.skip()
skips traversing the children of the current path.
path.stop()
stops traversal entirely.
outerPath.traverse({Function(innerPath) {innerPath.skip(); // if checking the children is irrelevant},ReferencedIdentifier(innerPath, state) {state.iife = true;innerPath.stop(); // if you want to save some state and then stop traversal, or deopt}
});
处理
替换一个节点
BinaryExpression(path) {path.replaceWith(t.binaryExpression('**', path.node.left, t.numberLiteral(2)))
}function square(n) {return n * n; -return n ** 2; +
}
用多节点替换单节点
ReturnStatement(path) {path.replaceWithMultiple([t.expressionStatement(t.stringLiteral("Is this the real life?")),t.expressionStatement(t.stringLiteral("Is this just fantasy?")),t.expressionStatement(t.stringLiteral("(Enjoy singing the rest of the song in your head)")),])
}function square(n) {
- return n * n
+ "Is this the real life?";
+ "Is this just fantasy?";
+ "(Enjoy singing the rest of the song in your head)";
}
用字符串源码替换节点
FunctionDeclaration(path) { path.replaceWithSourceString( function add(a, b) { return a + b; });
}```diff
- function square(n) {
- return n * n;
+ function add(a, b) {
+ return a + b;}
插入兄弟节点
FunctionDeclaration(path) {path.insertBefore(t.expressionStatement(t.stringLiteral("Because I'm easy come, easy go.")))path.insertAfter(t.expressionStatement(t.stringLiteral("A little high, little low.")))
}```diff
+ "Because I'm easy come, easy go.";function square(n) {return n * n;}
+ "A little high, little low.";
插入到容器(container)中
如果您想要在 AST 节点属性中插入一个像 body 那样的数组。它与 <code> insertBefore / insertAfter 类似,但您必须指定 listKey(通常是 正文)
ClassMethod(path) {path.get('body').unshiftContainer('body', t.expressionStatement(t.stringLiteral('before')));path.get('body').pushContainer('body', t.expressionStatement(t.stringLiteral('after')));
}```diff
class A {constructor() {
+ "Before"var a = 'middle';
+ "after"}
}
删除一个节点
FunctionDeclaration(path) {path.remove();
}- function square(n) {
- return n * n;
- }
替换父节点
// 只需要使用 parentPath: path.parentPath 调用 replace 即可BinaryExpression(path) {path.parentPath.replaceWith(t.expressionStatement( t.stringLiteral("Anywhere the wind blows, doesn't really matter to me, to me.")));
}function square(n) {
- return n * n;
+ "Anyway the wind blows, doesn't really matter to me, to me.";
}
删除父节点
BinaryExpression(path) {path.parentPath.remove();
}function square(n) {
- return n * n;
}
Scope(作用域)
检查本地变量是否被绑定
FunctionDeclaration(path) {if(path.scope.hasBinding("n")) {// ...}
}
// 这将遍历范围树并检查特定的绑定。
// 您也可以检查一个作用域是否有 自己的绑定
FunctionDeclaration(path) {if (path.scope.hasOwnBinding("n")) {// ...}
}
创建一个 UID
// 这将生成一个标识符,不会与任何本地定义的变量相冲突
FunctionDeclaration(path) {path.scope.generateUidIdentifier("uid");// Node { type: "Identifier", name: "_uid" }path.scope.generateUidIdentifier("uid");// Node { type: "Identifier", name: "_uid2" }
}
提升变量声明至父级作用域
// 有时你可能想要推送一个 VariableDeclaration,这样你就可以分配给它。
FunctionDeclaration(path) {const id = path.scope.generateUidIdentifierBasedOnNode(path.node.id);path.remove();path.scope.parent.push({ id, init: path.node });
}- function square(n) {
+ var _square = function square(n) {return n * n;
- }
+ };
重命名绑定及其引用
FunctionDeclaration(path) {path.scope.rename("n", "x");
}- function square(n) {
- return n * n;
+ function square(x) {
+ return x * x;}// 或者,您可以将绑定重命名为生成的唯一标识符
FunctionDeclaration(path) {path.scope.rename("n");
}- function square(n) {
- return n * n;
+ function square(_n) {
+ return _n * _n;}
案例1 代码实现
模块引入
引入模块这种功能的公共函数会放在 helper 中,这里我们使用 @babel/helper-module-imports。
const importModule = require('@babel/helper-module-imports');// 省略一些代码
importModule.addDefault(path, 'tracker',{nameHint: path.scope.generateUid('tracker')
})
首先要判断是否被引入过:在 Program 根节点里通过 path.traverse 来遍历 ImportDeclaration,如果引入了 tracker 模块,就记录 id 到 state 中,并用 path.stop 来终止后续遍历;没有引入的话就引入 tracker 模块,用 generateUid 生成唯一 id,然后放到 state 中。
当然 default import 和 namespace import 取 id 的方式不一样,需要分别处理下。
Program: {enter (path, state) {path.traverse({ImportDeclaration (curPath) {const requirePath = curPath.get('source').node.value;// 如果已经引入了// 我们把 tracker 模块名作为参数传入,通过 options.trackerPath 来取。if (requirePath === options.trackerPath) {const specifierPath = curPath.get('specifiers.0');if (specifierPath.isImportSpecifier()) { state.trackerImportId = specifierPath.toString();} else if(specifierPath.isImportNamespaceSpecifier()) {// tracker 模块的 id
state.trackerImportId = specifierPath.get('local').toString();}// 找到了就终止遍历path.stop();}}});if (!state.trackerImportId) {// tracker 模块的 idstate.trackerImportId = importModule.addDefault(path, 'tracker',{nameHint: path.scope.generateUid('tracker')}).name;// 在记录 tracker 模块 id 的时候,也生成调用 tracker 模块的 AST,使用 template.statement.// 埋点代码的 ASTstate.trackerAST = api.template.statement(`${state.trackerImportId}()`)();}}
}
函数插桩
函数插桩要找到对应的函数,这里要处理的有:ClassMethod、ArrowFunctionExpression、FunctionExpression、FunctionDeclaration 这些节点。
'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {const bodyPath = path.get('body');// 有函数体就在开始插入埋点代码if (bodyPath.isBlockStatement()) { bodyPath.node.body.unshift(state.trackerAST);} else { // 没有函数体要包裹一下,处理下返回值const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({PREV_BODY: bodyPath.node});bodyPath.replaceWith(ast);}
}
插件使用
const { transformFromAstSync } = require('@babel/core');
const parser = require('@babel/parser');
const autoTrackPlugin = require('./plugin/auto-track-plugin');
const fs = require('fs');
const path = require('path');const sourceCode = fs.readFileSync(path.join(__dirname, './sourceCode.js'), {encoding: 'utf-8'
});const ast = parser.parse(sourceCode, {sourceType: 'unambiguous'
});const { code } = transformFromAstSync(ast, sourceCode, {plugins: [[autoTrackPlugin, {trackerPath: 'tracker'}]]
});console.log(code);
小结
// 插件整体代码
const { declare } = require('@babel/helper-plugin-utils');
const importModule = require('@babel/helper-module-imports');const autoTrackPlugin = declare((api, options, dirname) => {api.assertVersion(7);return {visitor: {Program: {enter (path, state) {path.traverse({ImportDeclaration (curPath) {const requirePath = curPath.get('source').node.value;if (requirePath === options.trackerPath) {const specifierPath = curPath.get('specifiers.0');if (specifierPath.isImportSpecifier()) {state.trackerImportId = specifierPath.toString();} else if(specifierPath.isImportNamespaceSpecifier()) {state.trackerImportId = specifierPath.get('local').toString();}path.stop();}}});if (!state.trackerImportId) {state.trackerImportId = importModule.addDefault(path, 'tracker',{nameHint: path.scope.generateUid('tracker')}).name;state.trackerAST = api.template.statement(`${state.trackerImportId}()`)();}}},'ClassMethod|ArrowFunctionExpression|FunctionExpression|FunctionDeclaration'(path, state) {const bodyPath = path.get('body');if (bodyPath.isBlockStatement()) {bodyPath.node.body.unshift(state.trackerAST);} else {const ast = api.template.statement(`{${state.trackerImportId}();return PREV_BODY;}`)({PREV_BODY: bodyPath.node});bodyPath.replaceWith(ast);}}}}
});
module.exports = autoTrackPlugin;
函数插桩是在函数中插入一段逻辑但不影响函数原本逻辑,埋点就是一种常见的函数插桩,我们完全可以用 babel 来自动做。
实现思路分为“引入 tracker 模块”和“函数插桩”两部分。
1. 引入 tracker 模块需要判断 ImportDeclaration 是否包含了 tracker 模块,没有的话就用 @babel/helper-module-import 来引入。
2. 函数插桩就是在函数体开始位置插入一段代码,如果没有函数体,需要包装一层,并且处理下返回值。
案例2 代码实现
安装依赖
mrdir babel-tracker
cd ./babel-tracker
npm init -y
npm i -D @babel/core @babel/helper-plugin-utils
编写入口文件
const { transformFileSync } = require("@babel/core");
const path = require("path");const tracker = require("./babel-plugin-tracker");
const pathFile = path.resolve(__dirname, "./sourceCode.js");// transform ast and generate code
// 先将代码转成AST语法树,然后用插件对AST对象树做一系列的处理,最后将处理好的AST转回js代码。
const { code } = transformFileSync(pathFile, {plugins: [[tracker, {trackerPath: 'tracker'}]],
});console.log(code);
编写插件
一般在 bable 中处理 import,是在 Program 的 AST 节点中处理的,所以需要在插件中处理 Program 节点。
基本思路:判断文件中是否有 _tracker 的 import,如果没有,就添加一个导入。
添加了一个 Program 的处理函数。在逻辑中,遍历的了整个文件的 import 语句,并且一一比较了 import 的 source
,如果其中的 source.value
有 _tracker
,说明文件已经导入了_tracker。
一个 import 语句,如:
import a from 'a.js'
那么可以通过node.source.value
,获取这个 AST 节点中的a.js
在判断 _tracker
的导入路径的时候,代码中是从 options.trackerPath
中获取的,而 options 的配置是在插件引用的地方。 并没有 hard code。
如果没有发现 tracker 的导入,就需要手动添加了。代码中借用的是 addDefault 的依赖帮忙添加的。其中 { nameHint: "_tracker" }
用来设置 _tracker 作为埋点函数的变量名。
在代码中,做了一个对函数节点 body 属性值类型的判断,如果是 isBlockStatement,那就可以执行 unshift,如果不是,说明函数单纯返回了一个值,这时候就需要将函数体变成 blockStatement,并且函数的返回值依然是原来的值。形如 const test_5 = ()=>0
变成const test_5 = ()=>{ return 0; }
。这样就可以添加埋点函数了。
处理埋点函数变量名思路:
使用了 path.scope.generateUid("tracker")
来生成当前作用域内唯一的变量。
借助state,来传递生成的变量,或者是已经定义的变量。
在插入埋点函数的时候,就可以读取state中的变量了。
// 导入 `@babel/helper-module-imports` 包的 `addDefault` 函数
// 它可以向程序中添加默认导入
const { addDefault } = require("@babel/helper-module-imports");// 导出一个 Babel 插件的函数。它接受两个参数:
// `api` 是一个 Babel 插件 API 对象,提供了一些可以在插件中使用的方法。
// `options` 是用户在 Babel 配置文件中给该插件指定的选项。
module.exports = (api, options) => {// 返回一个插件对象return {// visitor 对象定义了我们要访问的 AST 节点类型以及对应的处理方法visitor: {// 对于四种类型的节点"ArrowFunctionExpression|FunctionDeclaration|ClassMethod|FunctionExpression": {// 当我们进入一个节点时enter: (path, state) => {// path 是当前节点的路径对象,它提供了一些操作当前节点的方法const types = api.types;// 获取函数的函数体的路径const bodyPath = path.get("body");const ast = state.trackerAst;if (types.isBlockStatement(bodyPath.node)) {// 将新生成的 ‘_tracker()’ 调用语句插入到函数的函数体的开头bodyPath.node.body.unshift(ast);} else {// 使用 Babel 插件 API 的 template.statement 方法创建一个新的 AST 节点// 这个节点表示 ‘_tracker()’ 这个语句。注意我们需要调用返回的函数 (()) 以生成 ASTconst ast2 = api.template.statement(`{${state.importTrackerId}();return BODY;}`)({ BODY: bodyPath.node });bodyPath.replaceWith(ast2);}},},// 对于 Program 类型的节点(整个程序):Program: {// 当我们进入一个节点时:enter: (path, state) => {// 从插件选项中获取 ‘_tracker’ 函数的导入路径const trackerPath = options.trackerPath;// 遍历当前节点(整个程序)的所有子节点path.traverse({// 对于 ImportDeclaration 类型的节点(导入声明)ImportDeclaration(path) {// 如果当前导入声明的来源与 ‘_tracker’ 函数的导入路径相同if (path.node.source.value === trackerPath) {const specifiers = path.get("specifiers.0");state.importTrackerId = specifiers.get("local").toString();// 停止遍历path.stop();}},});if (!state.importTrackerId) {// 使用 addDefault 函数向程序中添加 ‘_tracker’ 函数的默认导入// options.trackerPath 是 _tracker 函数的导入路径// { nameHint: …… } 是一个选项对象,用于指定导入的变量名state.importTrackerId = addDefault(path, options.trackerPath, {nameHint: path.scope.generateUid("tracker"),}).name;}state.trackerAst = api.template.statement(`${state.importTrackerId}();`)();},},},}
}
小结
讲了如何在函数中添加埋点函数,以及如何处理埋点函数的import。在埋点的时候,需要注意以下几个问题:
函数形态的多样性
埋点函数的变量是否已经定义,如果已经定义,插入埋点的时候,就要使用已经定义的变量名;如果没有定义,插入import的时候,就要保证插全局变量名的唯一性
案例3 注释埋点
主要讲如何根据注释,通过 babel 插件自动地,给相应函数插入埋点代码,在实现埋点逻辑和业务逻辑分离的基础上,配置更加灵活。
效果展示
源代码:
//##箭头函数
//_tracker
const test1 = () => {};const test1_2 = () => {};
转译之后:
import _tracker from "tracker";
//##箭头函数
//_tracker
const test1 = () => {_tracker();
};const test1_2 = () => {};
代码中有两个函数,其中一个 //_tracker
的注释,另一个没有。转译之后只给有注释的函数添加埋点函数。
要达到这个效果就需要读取函数上面的注释,如果注释中有 //_tracker
,我们就给函数添加埋点。这样做避免了僵硬的给每个函数都添加埋点的情况,让埋点更加灵活。
插件使用
const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");const pathFile = path.resolve(__dirname, "./sourceCode.js");//transform ast and generate code
const { code } = transformFileSync(pathFile, {plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker" }]],
});console.log(code);
这里我们使用 transformFileSync
API 转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入 plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]
。除此之外,还有插件的参数:
trackerPath
表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。
commentsTrack 埋点
标识,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活。
源代码
import "./index.css";//##箭头函数
//_tracker
const test1 = () => {};const test1_2 = () => {};//函数表达式
//_tracker
const test2 = function () {};const test2_1 = function () {};// 函数声明
//_tracker
function test3() {}function test3_1() {}
插件编写
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");//get comments path from leadingComments
const hasTrackerComments = (leadingComments, comments) => {if (!leadingComments) {return false;}if (Array.isArray(leadingComments)) {const res = leadingComments.filter((item) => {return item.node.value.includes(comments);});return res[0] || null;}return null;
};//insert path
const insertTracker = (path, param, state) => {const bodyPath = path.get("body");if (bodyPath.isBlockStatement()) {const ast = template.statement(`${state.importTackerId}(${param});`)();bodyPath.node.body.unshift(ast);} else {const ast = template.statement(`{${state.importTackerId}(${param});return BODY;}`)({ BODY: bodyPath.node });bodyPath.replaceWith(ast);}
};//check if tacker func was imported
const checkImport = (programPath, trackPath) => {let importTrackerId = "";programPath.traverse({ImportDeclaration(path) {const sourceValue = path.get("source").node.value;if (sourceValue === trackPath) {const specifiers = path.get("specifiers.0");importTrackerId = specifiers.get("local").toString();path.stop();}},});if (!importTrackerId) {importTrackerId = addDefault(programPath, trackPath, {nameHint: programPath.scope.generateUid("tracker"),}).name;}return importTrackerId;
};module.exports = declare((api, options) => {console.log("babel-plugin-tracker-comment");return {visitor: {"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression": {enter(path, state) {let nodeComments = path;if (path.isExpression()) {nodeComments = path.parentPath.parentPath;}// 获取leadingCommentsconst leadingComments = nodeComments.get("leadingComments");const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);//查看作用域中是否有——trackerParam// 如果有注释,就插入函数if (paramCommentPath) {//add Importconst programPath = path.hub.file.path;const importId = checkImport(programPath, options.trackerPath);state.importTackerId = importId;insertTracker(path, state);}},},},};
});
代码难点讲解:
AST 对象中,用 leadingComments
表示前面的注释,用 trailingComments
表示后面的注释。用 CommentBlock 表示块注释( /**/ ),用 CommentLine 表示行注释( // )。
拿到 import 语句
需要 program 节点。checkImport
函数的实现就是在当前文件中,找出埋点函数的引入。寻找的过程中,用到了引入插件时传入的参数 trackerPath
。还用到了 traverse
API,用来遍历 import 语句
。
如果找到了引入,就获取引入的变量。这个变量在之后埋点的时候需要。即如果引入的变量命名为 tracker2
,那么埋点的时候埋点函数就是 tracker2
了。
如果没有引入,就插入引入。
addDefault
就是引入 path 的函数,并且会返回插入引用的变量。
在生成埋点函数的时候,就用到了之前获取到的埋点函数的变量 importTackerId
。还有在实际插入的时候,要区分函数体是一个 Block
,还是直接返回的值--()=>''。
在获取注释的时候,代码中并不是直接获取到 path
的 leadingComments
,这是为什么?
比如这串代码:
// _tracker
const test1 = () => {};
我们在函数中遍历得到的 path 是 ()=>{}
ast 的 path,这个 path 的 leadingComments
其实是 null
,而想要获取 //_tracker
,我们真正需要拿到的 path,是注释下面的变量声明语句
。所以在代码中有判断是否为表达式,如果是,那就需要先获取 parentPath
,得到赋值表达式
的 path
,然后再获取 parentPath
,才能拿到变量声明语句。
案例4 埋点传参
那想要给插入的埋点函数传入参数应该怎么做呢?
传入参数可以有两个思路,
- 一个是将参数也放在注释里面,在babel插入代码的时候读取下注释里的内容就好了;
- 另一个是将参数以局部变量的形式放在当前作用域中,在babel插入代码时读取下当前作用域的变量就好;
下面我们来实现这两个思路,大家挑个自己喜欢的方法就好
参数放在注释中
源代码
import "./index.css";//##箭头函数
//_tracker,_trackerParam={name:'gongfu', age:18}
const test1 = () => {};//_tracker
const test1_2 = () => {};
插件使用
const { transformFileSync } = require("@babel/core");
const path = require("path");
const tracker = require("./babel-plugin-tracker-comment.js");const pathFile = path.resolve(__dirname, "./sourceCode.js");//transform ast and generate code
const { code } = transformFileSync(pathFile, {plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker",commentParam: "_trackerParam" }]],
});console.log(code);
使用了 transformFileSync
API 转译源代码,并将转译之后的代码打印出来。过程中,将手写的插件作为参数传入 plugins: [[tracker, { trackerPath: "tracker", commentsTrack: "_tracker"}]]
。除此之外,还有插件的参数
trackerPath
表示埋点函数的路径,插件在插入埋点函数之前会检查是否已经引入了该函数,如果没有引入就需要额外引入。commentsTrack
标识埋点,如果函数前的注释有这个,就说明函数需要埋点。判断的标识是动态传入的,这样比较灵活commentParam
标识埋点函数的参数,如果注释中有这个字符串,那后面跟着的就是参数了。就像上面源代码所写的那样。这个标识不是固定的,是可以配置化的,所以放在插件参数的位置上传进去
编写插件
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {if (!leadingComments) {return false;}if (Array.isArray(leadingComments)) {const res = leadingComments.filter((item) => {return item.node.value.includes(comments);});return res[0] || null;}return null;
};const getParamsFromComment = (commentNode, options) => {const commentStr = commentNode.node.value;if (commentStr.indexOf(options.commentParam) === -1) {return null;}try {return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);} catch {return null;}
};const insertTrackerBeforeReturn = (path, param, state) => {//blockStatementconst bodyPath = path.get("body");let ast = template.statement(`${state.importTackerId}(${param});`)();if (param === null) {ast = template.statement(`${state.importTackerId}();`)();}if (bodyPath.isBlockStatement()) {//get returnStatement, by body of blockStatementconst returnPath = bodyPath.get("body").slice(-1)[0];if (returnPath && returnPath.isReturnStatement()) {returnPath.insertBefore(ast);} else {bodyPath.node.body.push(ast);}} else {ast = template.statement(`{ ${state.importTackerId}(${param}); return BODY; }`)({BODY: bodyPath.node });bodyPath.replaceWith(ast);}
};const checkImport = (programPath, trackPath) => {let importTrackerId = "";programPath.traverse({ImportDeclaration(path) {const sourceValue = path.get("source").node.value;if (sourceValue === trackPath) {const specifiers = path.get("specifiers.0");importTrackerId = specifiers.get("local").toString();path.stop();}},});if (!importTrackerId) {importTrackerId = addDefault(programPath, trackPath, {nameHint: programPath.scope.generateUid("tracker"),}).name;}return importTrackerId;
};module.exports = declare((api, options) => {return {visitor: {"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {enter(path, state) {let nodeComments = path;if (path.isExpression()) {nodeComments = path.parentPath.parentPath;}// 获取leadingCommentsconst leadingComments = nodeComments.get("leadingComments");const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);// 如果有注释,就插入函数if (paramCommentPath) {//add Importconst programPath = path.hub.file.path;const importId = checkImport(programPath, options.trackerPath);state.importTackerId = importId;const param = getParamsFromComment(paramCommentPath, options);insertTrackerBeforeReturn(path, param, state);}},},},};
});
代码难点讲解:
getParamsFromComment 函数的逻辑是检查代码是否含有注释 _tracker
,如果有的话,再检查这一行注释中是否含有参数,最后再将埋点插入函数。
在检查是否含有参数的过程中,用到了插件参数 commentParam
。它表示如果注释中含有该字符串,那后面的内容就是参数了。获取参数的方法也是简单的字符串切割。如果没有获取到参数,就一律返回 null。
获取参数的复杂程度取决于,先前是否就有一个规范。并且编写代码时严格按照规范执行。 像我这里的规范是埋点参数
commentParam
和埋点标识符_tracker
必须放在一行,并且参数需要是对象的形式。即然是对象的形式,那这一行注释中就不允许就其他的大括号符号"{}"
。 遵从了这个规范,获取参数的过程就变的很简单了。当然你也可以有自己的规范。
import "./index.css";//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {const name = "gongfu2";
};const test1_2 = () => {};
当传递变量时,插入的参数确实用了变量,但是引用变量却在变量声明之前,这肯定不行,得改改。需要将埋点函数插入到函数体的后面,并且是 returnStatement
的前面,这样就不会有问题了。
这里将 insertTracker
改成了 insertTrackerBeforeReturn
。
其中关键的逻辑是判断是否是一个函数体,
- 如果是一个函数体,就判断有没有
return
语句,- 如果有
return
,就放在return
前面 - 如果没有
return
,就放在整个函数体的后面
- 如果有
- 如果不是一个函数体,就直接生成一个函数体,然后将埋点函数放在
return
的前面
参数放在局部作用域中
这个功能的关键就是读取当前作用域中的变量。
在写代码之前,来定一个前提:当前作用域的变量名也和注释中参数标识符一致,也是_trackerParam。
源代码
import "./index.css";//##箭头函数
//_tracker,_trackerParam={name, age:18}
const test1 = () => {const name = "gongfu2";
};const test1_2 = () => {};//函数表达式
//_tracker
const test2 = function () {const age = 1;_trackerParam = {name: "gongfu3",age,};
};const test2_1 = function () {const age = 2;_trackerParam = {name: "gongfu4",age,};
};
编写插件
const { declare } = require("@babel/helper-plugin-utils");
const { addDefault } = require("@babel/helper-module-imports");
const { template } = require("@babel/core");
//judge if there are trackComments in leadingComments
const hasTrackerComments = (leadingComments, comments) => {if (!leadingComments) {return false;}if (Array.isArray(leadingComments)) {const res = leadingComments.filter((item) => {return item.node.value.includes(comments);});return res[0] || null;}return null;
};const getParamsFromComment = (commentNode, options) => {const commentStr = commentNode.node.value;if (commentStr.indexOf(options.commentParam) === -1) {return null;}try {return commentStr.slice(commentStr.indexOf("{"), commentStr.indexOf("}") + 1);} catch {return null;}
};const insertTrackerBeforeReturn = (path, param, state) => {//blockStatementconst bodyPath = path.get("body");let ast = template.statement(`${state.importTackerId}(${param});`)();if (param === null) {ast = template.statement(`${state.importTackerId}();`)();}if (bodyPath.isBlockStatement()) {//get returnStatement, by body of blockStatementconst returnPath = bodyPath.get("body").slice(-1)[0];if (returnPath && returnPath.isReturnStatement()) {returnPath.insertBefore(ast);} else {bodyPath.node.body.push(ast);}} else {ast = template.statement(`{${state.importTackerId}(${param}); return BODY;}`)({BODY: bodyPath.node });bodyPath.replaceWith(ast);}
};const checkImport = (programPath, trackPath) => {let importTrackerId = "";programPath.traverse({ImportDeclaration(path) {const sourceValue = path.get("source").node.value;if (sourceValue === trackPath) {const specifiers = path.get("specifiers.0");importTrackerId = specifiers.get("local").toString();path.stop();}},});if (!importTrackerId) {importTrackerId = addDefault(programPath, trackPath, {nameHint: programPath.scope.generateUid("tracker"),}).name;}return importTrackerId;
};module.exports = declare((api, options) => {return {visitor: {"ArrowFunctionExpression|FunctionDeclaration|FunctionExpression|ClassMethod": {enter(path, state) {let nodeComments = path;if (path.isExpression()) {nodeComments = path.parentPath.parentPath;}// 获取leadingCommentsconst leadingComments = nodeComments.get("leadingComments");const paramCommentPath = hasTrackerComments(leadingComments,options.commentsTrack);// 如果有注释,就插入函数if (paramCommentPath) {//add Importconst programPath = path.hub.file.path;const importId = checkImport(programPath, options.trackerPath);state.importTackerId = importId;//check if have tackerParamconst hasTrackParam = path.scope.hasBinding(options.commentParam);if (hasTrackParam) {insertTrackerBeforeReturn(path, options.commentParam, state);return;}const param = getParamsFromComment(paramCommentPath, options);insertTrackerBeforeReturn(path, param, state);}},},},};
});
代码难点讲解:
path.scope.hasBinding() 这个函数的逻辑是先判断当前作用域中是否有变量 _trackerParam
,有的话,就获取该声明变量的初始值。然后将该变量名作为 insertTrackerBeforeReturn
的参数传入其中。
小结
讲了如何给埋点的函数添加参数,参数可以写在注释里,也可以写在布局作用域中。支持动态传递,非常灵活。
在create-reate-app中使用我们手写的babel插件
要在create-react-app项目中使用自定义Babel插件,你需要执行以下步骤:
- 在项目根目录下创建一个config-overrides.js文件(如果还没有的话)。
- 在config-overrides.js文件中,导出一个函数来重写Create React App的Webpack配置。
- 使用require加载你的自定义Babel插件。
- 应用插件到Babel的配置中。
以下是一个config-overrides.js的示例,它演示了如何加载和使用自定义Babel插件:
// config-overrides.js
const path = require('path');module.exports = function override(webpackConfig, env) {// 确保Babel插件路径是正确的const babelPlugin = path.resolve(__dirname, 'path-to-your-babel-plugin.js');// 修改webpack配置webpackConfig.module.rules.forEach(rule => {// 针对.js文件的规则if (rule.test.toString() === '/\\.js$/') {// 确保Babel配置是一个数组const babelLoader = rule.use[0];if (babelLoader.options) {babelLoader.options.plugins = [...(babelLoader.options.plugins || []),require(babelPlugin), // 加载自定义插件];}}});return webpackConfig;
};
请注意,自定义Babel插件应该是一个符合Babel插件API的标准插件,并且需要确保它与Create React App的Babel版本兼容。
最后,重新启动开发服务器以使更改生效。
这篇关于Babel基础知识及实现埋点插件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!