Day12 Babel 插件、预设开发与实战

如何使用 babel 插件

在 babel 要使用一个插件,步骤实际上非常简单,就分为两步:

  • 安装插件
  • 在配置文件或者 CLI 中指定插件

举个例子,例如有一个专门将箭头函数转为普通函数的插件:

bash 复制代码
pnpm add @babel/plugin-transform-arrow-functions -D

之后在配置文件中进行插件配置即可

js 复制代码
{
  "plugins": ["@babel/plugin-transform-arrow-functions"]
}

插件使用的细节

  1. 插件的运行顺序

plugins 对应的值为一个数组,说明是可以指定多个插件的

js 复制代码
{
  "plugins": ["transform-decorators-legacy", "transform-class-properties"]
}

在上面的配置中,插件的运行会从左往右运行,也就是说,会先运行 transform-decorators-legacy 这个插件,然后运行 transform-class-properties 这个插件。

如果配置文件中既配置了插件,又配置了预设,那么 babel 会先运行插件,然后在运行预设里面的插件,也就是说,插件运行的时机是要早于预设的。

  1. 插件选项

在使用插件的时候,是可以传递插件选项的,例如有三种写法:

js 复制代码
{
  "plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}

上面的三种写法目前来讲是等价的,而第三种写法,数组第二项的对象实际上就是用来传递插件配置项

js 复制代码
{
  "plugins": [
    [
      "transform-async-to-module-method",
      {
        "module": "bluebird",
        "method": "coroutine"
      }
    ]
  ]
}

关于插件究竟有哪些配置项,这个需要去参阅插件相关的文档。

插件列表

你可以在 babeljs.io/docs/plugin... 看到 babel 中支持的大多数插件。

一般来讲,每个插件点击进去会包含该插件对应的说明信息,一般包含这些内容:

  • 该插件的说明
  • 插件编译前后代码的区别
  • 该插件的使用方法
  • 该插件的配置选项

使用预设

预设的基本使用

首先第一步仍然是先要安装对应的预设

bash 复制代码
pnpm add --save-dev @babel/preset-env

安装完成后,在配置文件中进行配置:

js 复制代码
{
  "presets": ["@babel/preset-env"]
}

预设对应的值是一个数组,说明也是能够配置多个预设的,但是一定要注意顺序

js 复制代码
{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

上面的配置中,会先运行 @babel/preset-react 预设里面的插件,然后运行 @babel/preset-env 这套预设里面的插件。

运行的顺序刚好和插件是相反的,从后往前运行。

官方预设

官方提供了 4 套预设:

  • @babel/preset-env 用于编译 ES2015 及以上版本的语法
  • @babel/preset-typescript 用于 TypeScript
  • @babel/preset-react 用于 React
  • @babel/preset-flow 用于 Flow

stage-x 预设

在 babel v7.0.0 之前,支持一种叫做 state-x 的预设特性。

JavaScript 的新特性是由 TC39 的小组提出并且通过一系列的阶段来推动的。一般来讲,这个阶段分为从 0 到 4,每个阶段对应了新特性的不同状态:

  • Stage 0 - Strawman:只是一个想法或者提案,还没有任何实现。
  • Stage 1 - Proposal :这是一个正式的提案,包含 API 的描述,但可能还没有完全实现。
  • Stage 2 - Draft:初步版本,已经有了初步的规范文本,并且大部分细节都已经确定。
  • Stage 3 - Candidate:候选阶段,规范已经完成,并且已经完成了浏览器的初步实现,这个阶段主要是为了获取反馈和评估。
  • Stage 4 - Finished :完成阶段,已经在多个浏览器中实现并通过了实际使用的测试,可以被添加到 ECMAScript 标准中。

在早期的时候(babel v7.x.x 之前),可以安装对应阶段的预设

bash 复制代码
npm install --save-dev @babel/preset-stage-2

这个预设对应了 stage2 阶段的新特性的编译

js 复制代码
{
  "presets": ["@babel/preset-stage-2"]
}

之后你在做开发的时候,就可以只用 stage 2 阶段的新语法了。

但是上面的 stage-x 的预设从 v7.0.0 版本开始就已经废弃了。

As of Babel 7, we've decided to deprecate the Stage-X presets and stop publishing them. Because these proposals are inherently subject to change, it seems better to ask users to specify individual proposals as plugins vs. a catch all preset that you would need to check up on anyway.

目前官方推荐的做法是要使用哪个新特性,直接安装对应的插件即可。

@babel/preset-env

这里我们主要看一下这一套插件对应的 options

js 复制代码
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "useBuiltIns": "entry",
        "corejs": "3.22",
        "modules": false
      }
    ]
  ]
}

在上面的配置中,我们就使用了 @babel/preset-env 预设,并且对这套预设做了一些配置。

  • targets:指定浏览器需要支持的版本范围
js 复制代码
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead"
    }]
  ]
}
  • useBuiltIns:让你决定如何使用 polyfills

    • entry:该选项值会根据项目中 browserslist 对应的浏览器版本范围来添加 polyfills,这个选项不会管你源码中是否用到缺失的特性,只要对应的浏览器版本是缺失的,那么就会添加对应的特性。而且在使用这个选项值的时候,还需要在源码的入口文件中手动引入 core-js
    • usage:根据你的源码中是否使用了缺失的特性,如果使用到了缺失的特性,那么才添加对应的 polyfills
    • false:这个是默认值,关闭自动引入 polyfills。
  • corejs:指定你的 corejs 版本,polyfills 实际上就是通过 corejs 来实现的。该配置项一般就和 useBuiltIns 一起使用

js 复制代码
{
  "presets": [
    ["@babel/preset-env", {
      "useBuiltIns": "usage",
      "corejs": 3
    }]
  ]
}

corejs 支持的配置项有 2、3 还有 false:

  • "2 ": 使用 core-js 的版本 2 。这是旧版本的 core-js ,它包含 ES5、ES6ES7 的特性。在 Babel 7.4.0 之前,这是默认值。

  • "3 ": 使用 core-js 的版本 3 。这是新版本的 core-js ,它包含 ES5、ES6、ES7、ES8 和更高版本的特性。在 Babel 7.4.0 及更高版本,这是推荐的值。

  • false : 不使用 core-js 。如果你不想让 Babel 添加任何 polyfill ,你可以将 corejs 设置为 false

  • modules:设置模块的类型

    • amd
    • umd
    • systemjs
    • commonjs
    • cjs
    • auto
    • false

默认值为 auto,根据你的环境和代码自动来决定使用的模块版本。

  • include:允许你显式的指定要包含的插件(这个插件是本身在预设里面,但是因为 targets 的设置,可能会被排除掉)
js 复制代码
{
  "presets": [
    ["@babel/preset-env", {
      "targets": "> 0.25%, not dead",
      "include": ["@babel/plugin-proposal-optional-chaining"]
    }]
  ]
}

假设 preset-env 里面有 pluginA、pluginB、pluginC,假设我现在指定了浏览器范围,所指定的这些浏览器范围已经实现了特性 A 和 特性B,那么这里就只会用到 pluginC。那么 include 配置项就可以强行指定要包含的插件

APIs

关于 babel 里面的 APIs 主要位于 @babel/core 这个依赖里面,你可以在官网左下角的 Tooling Packages 分类下找到这个依赖包。

这里顺便介绍一下每一种依赖包的作用:

  • @babel/parser : 是 Babel 的解析器,用于将源代码转换为 AST
  • @babel/core : Babel 的核心包,它提供了 Babel 的核心编译功能。这个包是使用 Babel 必须安装的。
  • @babel/generator : 是 Babel 的代码生成器,它接收一个 AST 并将其转换为代码和源码映射(sourcemap)。
  • @babel/code-frame : 提供了一种用于生成 Babel 错误消息的方法,可以在代码帧中高亮显示错误。
  • @babel/runtime : 提供了 Babel 运行时所需要的辅助函数和 polyfills,以避免在每个文件中都重复这些代码。
  • @babel/template : 提供了一种编写带有占位符的 Babel AST 模板的方法。
  • @babel/traverse : 是 BabelAST 遍历器,它包含了一些用于处理 AST 的工具。
  • @babel/types : 提供了一种用于 AST 节点的 Lodash-esque 实用程序库。

在第一节课的时候,我们安装了三个依赖:core、cli、preset,但是我们使用 babel 进行编译的时候发现最终是生成了编译后的代码的,而从 AST 生成编译后代码是 generator 的工作,实际上当你安装 core 的时候,就会间接的安装 generator、traverse 等需要用到的依赖包。

通过对官方 API 的观察,我们发现 babel/core 的 API 主要分为三大类:

  • transformXXX
  • parseXXX
  • loadXXX

transformXXX

这一组方法一看就是做和编译相关的操作,之所以有这么多,其实就是同步或者异步、编译代码或者文件的区别,每个方法的具体含义如下:

  • transform(code: string, options: Object) : 这是一个异步函数,用于将源代码字符串转换为 Babel 的结果对象。结果对象包含了转换后的代码,源码映射,以及 AST
  • transformSync(code: string, options: Object) : 这个函数和 transform 函数功能相同,但它是同步执行的。
  • transformAsync(code: string, options: Object) : 这个函数和 transform 函数功能相同,它返回一个 Promise ,这个 Promise 会在转换完成后解析为结果对象。
  • transformFile(filename: string, options: Object, callback: Function): 这个函数会读取并转换指定的文件。转换完成后,会调用提供的回调函数,并将结果对象传递给回调函数。
  • transformFileSync(filename: string, options: Object) : 这个函数和 transformFile 函数功能相同,但它是同步执行的。
  • transformFileAsync(filename: string, options: Object) : 这个函数和 transformFile 函数功能相同,它返回一个 Promise ,这个 Promise 会在转换完成后解析为结果对象。
  • transformFromAst(ast: Object, code: string, options: Object) : 这个函数接受一个 AST 对象,然后将这个 AST 转换为 Babel 的结果对象。这个函数可以用于在已经有 AST 的情况下避免重新解析代码。
  • transformFromAstSync(ast: Object, code: string, options: Object) : 这个函数和 transformFromAst 函数功能相同,但它是同步执行的。
  • transformFromAstAsync(ast: Object, code: string, options: Object) : 这个函数和 transformFromAst 函数功能相同,它返回一个 Promise ,这个 Promise 会在转换完成后解析为结果对象。

上面这些方法中,只要搞懂一个,其他的也就搞懂了。

parseXXX

该系列方法主要负责将源码转为抽象语法树(AST),之后就不管了。

  • parse(code: string, options: Object) : 这是一个异步函数,用于解析源代码字符串并返回一个 AST 。你可以通过选项对象来配置解析过程,例如是否包含注释,是否包含 location 信息等。

  • parseSync(code: string, options: Object) : 这个函数和 parse 函数功能相同,但它是同步执行的。

  • parseAsync(code: string, options: Object) : 这个函数和 parse 函数功能相同,它返回一个 Promise ,这个 Promise 会在解析完成后解析为 AST

loadXXX

这一系列方法主要是做配置文件的加载工作的

  • loadOptions(options: Object) : 这个函数接受一个选项对象,然后返回一个完整的、已解析的 Babel 配置对象。这个配置对象包括了所有的预设,插件,和其他配置选项。如果提供的选项对象中没有指定配置,那么这个函数会尝试从 .babelrc 文件或 babel.config.js 文件中加载配置。

    例如:

    js 复制代码
    const babel = require('@babel/core');
    
    const options = {
      filename: './src/myFile.js',
    };
    
    const config = babel.loadOptions(options);
    
    console.log(config);

    在这个例子中,我们首先导入了 @babel/core ,然后定义了一个选项对象。这个对象中,filename 属性指定了我们正在处理的文件的路径。然后我们使用 @babel/coreloadOptions 方法来加载 Babel 的配置。

    loadOptions 方法返回一个配置对象,这个对象包括了所有的预设,插件,和其他配置选项。在这个例子中,我们将这个配置对象打印到控制台。

  • loadPartialConfig(options: Object) : 这个函数和 loadOptions 函数类似,但是返回的配置对象可能是部分的,也就是说,它可能没有包括所有的预设和插件。这个函数主要用于在构建工具中,当你需要对 Babel 配置进行更精细的控制时。

自定义插件part1

关于 babel 中如何创建自定义插件,官方是有一个 handbook:github.com/jamiebuilds...

  • AST
  • Babel处理代码流程
  • 遍历

AST

开发者所书写的源码文件里面的代码,最终会被表现为一颗树结构

js 复制代码
function square(n) {
  return n * n;
}

最终上面的代码,就会被转为如下的树结构:

markdown 复制代码
- FunctionDeclaration:
  - id:
    - Identifier:
      - name: square
  - params [1]
    - Identifier
      - name: n
  - body:
    - BlockStatement
      - body [1]
        - ReturnStatement
          - argument
            - BinaryExpression
              - operator: *
              - left
                - Identifier
                  - name: n
              - right
                - Identifier
                  - name: n

上面的树结构如果使用 JS 来表示,结构如下:

js 复制代码
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}

你可以在 astexplorer.net/ 看到一段源码转换为的 AST

在上面的 JS 对象中,我们会发现每一层有一些相同的结构:

js 复制代码
{
  type: "FunctionDeclaration",
  id: {...},
  params: [...],
  body: {...}
}
js 复制代码
{
  type: "Identifier",
  name: ...
}
js 复制代码
{
  type: "BinaryExpression",
  operator: ...,
  left: {...},
  right: {...}
}

每一个拥有 type 属性的对象,我们可以将其称之为一个节点,那么一颗 AST 树实际上就是由成百上千个节点构成的。不同的节点有不同的类型,通过 type 来表示当前节点的类型。

除了 type 以外,还会有一些额外的属性,这些属性就提供了该节点额外的一些信息。

js 复制代码
{
  type: ...,
  start: 0,
  end: 38,
  loc: {
    start: {
      line: 1,
      column: 0
    },
    end: {
      line: 3,
      column: 1
    }
  },
  ...
}

Babel处理代码流程

Babel 对代码进行处理的时候,核心的流程就分为三步:

  • 解析(parse)
  • 转换(transform)
  • 生成(generate)

解析(parse)

将接收到的源代码转为抽象语法树,这个步骤又分为两个小阶段:

  • 词法分析
  • 语法分析

所谓词法分析,就是将源码转为 token

js 复制代码
let i = "Hello";
bash 复制代码
let、i、=、 "Hello"

转为 token 时,每一个 token 会包含一些额外的信息:

js 复制代码
n * n;

会形成如下的 token:

js 复制代码
[
  { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
  { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
  { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
]

每一个 token 里面专门有一个 type 属性来描述这个 token:

js 复制代码
{
  type: {
    label: 'name',
    keyword: undefined,
    beforeExpr: false,
    startsExpr: true,
    rightAssociative: false,
    isLoop: false,
    isAssign: false,
    prefix: false,
    postfix: false,
    binop: null,
    updateContext: null
  },
  ...
}

形成一个一个 token 之后,接下来就会进入到语法分析阶段,该阶段就是将所得到的 token 转为 AST 树结构,便于后续的操作。

转换(transform)

目前我们已经得到了一颗 AST 树结构,接下来对这棵树进行一个遍历操作,在遍历的时候,就可以对树里面的节点进行一些添加、删除、更新等操作,这个其实就是 babel 转换代码的核心。

例如我们的一些插件,就是在转换阶段介入并进行工作的。

生成(generate)

经历过转换之后,你现在得到的树结构已经和之前不一样,接下来我们要做的事情,就是将这颗 AST 重新转为代码(字符串)

遍历

在对 AST 进行遍历的时候,采用的是深度优先遍历,例如:

js 复制代码
{
  type: "FunctionDeclaration",
  id: {
    type: "Identifier",
    name: "square"
  },
  params: [{
    type: "Identifier",
    name: "n"
  }],
  body: {
    type: "BlockStatement",
    body: [{
      type: "ReturnStatement",
      argument: {
        type: "BinaryExpression",
        operator: "*",
        left: {
          type: "Identifier",
          name: "n"
        },
        right: {
          type: "Identifier",
          name: "n"
        }
      }
    }]
  }
}
  1. 于是我们从 FunctionDeclaration 开始并且我们知道它的内部属性(即:id,params,body),所以我们依次访问每一个属性及它们的子节点。
  2. 接着我们来到 id ,它是一个 IdentifierIdentifier 没有任何子节点属性,所以我们继续。
  3. 之后是 params ,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier 类型的单一节点,然后我们继续。
  4. 此时我们来到了 body ,这是一个 BlockStatement 并且也有一个 body 节点,而且也是一个数组节点,我们继续访问其中的每一个。
  5. 这里唯一的一个属性是 ReturnStatement 节点,它有一个 argument ,我们访问 argument 就找到了 BinaryExpression*(二元表达式)。
  6. BinaryExpression 有一个 operator ,一个 left ,和一个 rightOperator 不是一个节点,它只是一个值因此我们不用继续向内遍历,我们只需要访问 leftright

访问者

所谓访问者其实就是一个对象,该对象上面会有一些特殊的方法,这些特殊的方法会在你到达特定的节点的时候触发。

js 复制代码
const MyVisitor = {
  Identifier() {
    console.log("Called!");
  }
};

该访问者对象会在遍历这颗树的时候,当遇见 Identifier 节点的时候就会被调用。

例如上面的那颗 AST 树,我们只表示 type,表示出来的形式如下:

js 复制代码
- FunctionDeclaration
  - Identifier (id)
  - Identifier (params[0])
  - BlockStatement (body)
    - ReturnStatement (body)
      - BinaryExpression (argument)
        - Identifier (left)
        - Identifier (right)

因此在遍历上面这颗树的时候,Identifier 方法就会被调用四次。

有些时候我们可以针对特定的节点定义进入时要调用的方法,退出时要调用的方法

js 复制代码
const MyVisitor = {
  Identifier: {
    enter() {
      console.log("Entered!");
    },
    exit() {
      console.log("Exited!");
    }
  }
};

这里还是以上面的抽象语法树为例,整体的进入节点和退出节点的流程如下:

js 复制代码
进入 FunctionDeclaration
    进入 Identifier (id)
        走到尽头
    退出 Identifier (id)
    进入 Identifier (params[0])
        走到尽头
    退出 Identifier (params[0])
    进入 BlockStatement (body)
        进入 ReturnStatement (body)
            进入 BinaryExpression (argument)
                进入 Identifier (left)
                    走到尽头
                退出 Identifier (left)
                进入 Identifier (right)
                    走到尽头
                退出 Identifier (right)
            退出 BinaryExpression (argument)
        退出 ReturnStatement (body)
    退出 BlockStatement (body)
退出 FunctionDeclaration

现在你可能比较好奇的是访问者对象除了 Identifier 方法,还能够有哪些方法?

一般来讲,不同的节点类型就有节点 type 所对应的方法,例如:

  • Identifier(path, state): 这个方法在遍历到标识符节点时会被调用。
  • FunctionDeclaration(path, state): 这个方法在遍历到函数声明节点时会被调用。

至于节点究竟有哪些类型,可以参阅 estree:github.com/estree/estr...

路径

AST 是由一个一个的节点组成的,但是这些节点之间并非孤立的,而是彼此之间有一些联系的。因此有一个 path 对象,该对象主要就是记录节点和节点之间的一些关系。path 对象里面不仅仅包含了节点本身的信息,还包含了节点和父节点、子节点、兄弟节点之间的关系。

这样做的好处在于我们使用了一个相对简单的对象来表示节点之间复杂关系,不需要在每个节点里面来保存节点之间关系的信息。

在实际编写插件的时候,我们经常就会利用 path 对象来获取节点的相关信息:

js 复制代码
const babel = require("@babel/core");
const traverse = require("@babel/traverse").default;

const code = `function square(n) {
  return n * n;
}`;

const ast = babel.parse(code);

// traverse 接收两个参数
// 第一个参数就是抽象语法树
// 第二个参数就是访问者对象
traverse(ast, {
  enter(path) {
    console.log(path.node.type);
  },
});

状态

在遍历和修改抽象语法树的时候,应该尽量避免全局状态的问题

例如,现在我们有一个需求,重命名一个函数的参数。

js 复制代码
let paramName; // 存储函数参数名

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0]; // 同 path 对象拿到当前节点的参数
    paramName = param.name; // 将参数的名称存储到 paramName 里面(全局变量)
    param.name = "x";
  },

  Identifier(path) {
    // 之后,进入到每一个 Identifier 类型的节点的时候
    // 判断当前节点的名称是否等于 paramName(之前的函数参数名称)
    if (path.node.name === paramName) {
      // 进行修改
      path.node.name = "x";
    }
  }
};

上面的代码看上去没有什么问题,但是上面的代码可能在某些情况下不能够正常的工作。

例如在我们要转换的源码文件中就存在 paramName 这个变量,那么这段代码就会出现问题

为了解决这样的问题,我们需要避免全局状态,我们可以在一个访问者对象里面再定义一个访问者对象专门拿来存储状态。

js 复制代码
const updateParamNameVisitor = {
  Identifier(path) {
    if (path.node.name === this.paramName) {
      path.node.name = "x";
    }
  }
};

const MyVisitor = {
  FunctionDeclaration(path) {
    const param = path.node.params[0];
    const paramName = param.name;
    param.name = "x";

    path.traverse(updateParamNameVisitor, { paramName });
  }
};

path.traverse(MyVisitor);

自定义插件part2

要自定义 babel 的插件,实际上有一个固定的格式:

js 复制代码
module.exports = function(babel){
  // 该函数会自动传入 babel 对象
  // types 也是一个对象,该对象上面有很多的方法,方便我们对 AST 的节点进行操作
  const { types } = babel;
  
  return {
    name: "插件的名字",
    visitor: {
      // ...
      // 这里书写不同类别的方法,不同的方法会被进入不同类别的节点触发
    }
  }
}

示例一

创建一个自定义插件,该插件能够把 ES6 里面的 ** 转换为 Math.pow

在编写自定义插件的时候,会使用到 types 对象的一些方法:

  • t.callExpression(callee, arguments):这个函数用于创建一个 表示函数调用的 AST 节点。callee 参数是一个表示被调用的函数的表达式节点,arguments 参数是一个数组,包含了所有的参数表达式节点。

  • t.memberExpression(object, property, computed = false) :这个函数用于创建一个 表示属性访问的 AST 节点object 参数是一个表示对象的表达式节点,property 参数是一个表示属性名的标识符或表达式节点。computed 参数是一个布尔值,表示属性名是否是动态计算的。

  • t.identifier( ) : 创建 AST 节点,只不过创建 的是 identifier 类型的 AST 节点

插件的核心,其实就是创建一些新的 AST 节点,去替换旧的 AST 节点。

插件的代码如下:

js 复制代码
// 该插件负责将 ** --> Math.pow
// 例如 2 ** 3 ---> Math.pow(2, 3)

module.exports = function (babel) {
  const { types: t } = babel;

  return {
    name: "transform-to-mathpow",
    visitor: {
      // 当你遍历 AST 节点的时候
      // 遍历到二元表达式的时候会自动执行该方法
      BinaryExpression(path) {
        // 二元表达式比较多
        // 5 + 3
        // 1 / 2
        // 检查当前的节点的运算符是否是 **
        // 如果不是,直接返回
        if (path.node.operator !== "**") {
          return;
        }
        // 说明当前是 ** 我们要做一个替换操作
        // 首先需要生成新的 AST 节点,因为替换使用新的 AST 节点来替换的旧的 AST 节点

        // t.identifier("Math") // ---> Math
        // t.identifier("pow") // ---> pow

        // pow 需要作为 Math 的一个属性
        // Math.pow
        // t.memberExpress(t.identifier("Math"), t.identifier("pow"));

        const mathpowAstNode = t.callExpression(
          t.memberExpression(t.identifier("Math"), t.identifier("pow")),
          [path.node.left, path.node.right]
        );

        // 用新的 AST 节点替换旧的 AST 节点
        path.replaceWith(mathpowAstNode);
      },
    },
  };
};

在上面的代码中,我们就创建了一个自定义的插件,该插件首先对外暴露一个函数,该函数需要返回一个对象,对象里面就有访问器对象,访问器对象里面会有一些特定的方法,这些方法会在进入到特定的节点的时候被调用。

插件内部做的核心的事情:创建新的 AST 节点,然后去替换旧的 AST 节点。

示例二

编写一个自定义插件,该插件能够将箭头函数转为普通的函数。

js 复制代码
// a => {...}
// function(a){...}
module.exports = function (babel) {
  const { types: t } = babel;

  return {
    name: "transform-arrow-to-function",
    visitor: {
      // 当你的节点类型为箭头函数表达式的时候
      // 执行特定的方法
      ArrowFunctionExpression(path) {
        let body; // 存储函数体

        if (path.node.body.type !== "BlockStatement") {
          // 进入此 if,说明箭头函数是一个表达式,需要将 body 部分转为返回语句
          // a => b
          // function(a){return b}
          body = t.blockStatement([t.returnStatement(path.node.body)]);
        } else {
          // 可以直接使用箭头函数的方法体
          body = path.node.body;
        }
        // 该方法创建一个普通函数表达式的 AST 节点(  function(){} )
        const functionExpression = t.functionExpression(
          null, // 函数名
          path.node.params, // 函数参数,和箭头函数的参数是一致的
          body, // 函数方法体
          false, // 不是一个生成器函数
          path.node.async // 是否是异步函数,和箭头函数是一致的
        );

        path.replaceWith(functionExpression);
      },
    },
  };
};
相关推荐
LY80915 分钟前
前端开发者的福音:用JavaScript实现Live2D虚拟人口型同步
前端·虚拟现实
林涧泣15 分钟前
【Uniapp-Vue3】uniapp创建组件
前端·javascript·uni-app
Sinyu101221 分钟前
Flutter 动画实战:绘制波浪动效详解
android·前端·flutter
pikachu冲冲冲24 分钟前
vue权限管理(动态路由)
前端·vue.js
那年星空30 分钟前
Flutter 3.x 版本升级实战:让老项目焕发新生
android·flutter·架构
一条不想当淡水鱼的咸鱼32 分钟前
taro转H5端踩坑
前端·taro
傻小胖1 小时前
React Context用法总结
前端·react.js·前端框架
xsh801442421 小时前
Java Spring Boot监听事件和处理事件
java·前端·数据库
JINGWHALE12 小时前
设计模式 行为型 状态模式(State Pattern)与 常见技术框架应用 解析
前端·人工智能·后端·设计模式·性能优化·系统架构·状态模式
Smile_zxx2 小时前
windows 下npm 使用 n 切换node版本
前端·windows·npm