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.oper
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chengbo_eva

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值