Babel 示例插件:处理 AST 替换变量、箭头函数

Babel 前段时间听闻后感觉神秘,无从下手?非常理解🧐

编写一个示例后,你会发现它只是一个将 ES6 代码转换为 ES5 的 JavaScript 工具,使我们能在不支持 ES6 的环境中运行代码。Babel 的工作是先将代码解析为抽象语法树(AST),然后遍历这棵树,对其节点进行操作,最终生成新的代码

下面我写了两个简单的例子方便快速开始,源码在文末,可以直接运行

示例:将变量 foo 替换为 bar

让我们看一个简单的例子,从原始代码开始,通过 Babel 插件处理后的输出,到转换过程的详细解释

原始代码

文件 test.js 包含以下 JavaScript 代码:

javascript 复制代码
const foo = 42;
console.log(foo);

通过 Babel 转换后的输出

转换后的代码将所有的 foo 替换为 bar

javascript 复制代码
const bar = 42;
console.log(bar);

转换过程的核心代码

转换代码如下,我们使用了 Babel 的同步转换方法和一个自定义插件 replaceFooWithBar

javascript 复制代码
const babel = require("@babel/core");
const replaceFooWithBar = require("./replaceFooWithBar.js");
const fs = require("fs");
const path = require("path");

const code = fs.readFileSync(path.resolve(__dirname, "./test.js"), "utf-8");

const output = babel.transformSync(code, {
  plugins: [replaceFooWithBar],
});

console.log(output.code);

插件 replaceFooWithBar.js 的代码

这个 Babel 插件简单但有效,它将所有的 foo 标识符替换为 bar

javascript 复制代码
module.exports = function ({ types: t }) {
  return {
    visitor: {
      Identifier(path) {
        if (path.node.name === "foo") {
          path.node.name = "bar";
        }
      },
    },
  };
};

解析代码

在 Babel 中,visitor 对象包含一组方法,每个方法对应一种或多种节点类型。当 Babel 遍历 AST 时,如果遇到某个节点类型,它会调用对应的访问者方法

babel.transformSync

javascript 复制代码
const output = babel.transformSync(code, {
  plugins: [replaceFooWithBar],
});
  1. code

    • 这是一个包含你想要转换的 JavaScript 代码的字符串。例如

      javascript 复制代码
      const code = 'const foo = 1';
  2. 配置对象(第二个参数)

    • 这是一个包含 Babel 配置的对象,指定要使用的插件、预设及其他配置选项。在这个例子中,我们只指定了一个插件 replaceFooWithBar

visitor 对象

visitor 对象的键是节点类型的名称,值是对应的处理函数。当 AST 中遇到该类型的节点时,Babel 会调用这个处理函数。处理函数会接收一个路径(path)对象,表示当前节点在 AST 中的位置

  1. 导出函数

    javascript 复制代码
    module.exports = function ({ types: t }) {
      // 插件返回一个包含 visitor 对象的对象
      return {
        visitor: {
          // 访问 Identifier 节点的方法
          Identifier(path) {
            if (path.node.name === "foo") {
              path.node.name = "bar";
            }
          },
        },
      };
    };
  2. visitor 对象

    • visitor 对象定义了一个 Identifier 方法
    • 当 Babel 遇到 Identifier 节点(表示标识符,例如变量名、函数名等)时,会调用这个方法
  3. Identifier 方法

    javascript 复制代码
    Identifier(path) {
      if (path.node.name === "foo") {
        path.node.name = "bar";
      }
    }
    • path 对象表示当前节点及其上下文
    • path.node 是当前节点的具体表示
    • 检查节点的名称是否为 foo,如果是,则将其改为 bar

看完上面的例子会觉得,直接正则表达式匹配替换不是更简单吗?一句话解决:

javascript 复制代码
const code = 'const foo = 1';
code.replace(/foo/g, 'bar')

如果有疑惑那么我们再写一个例子,将箭头函数转换为普通函数

示例:将箭头函数转换为普通函数

这个操作常在一些老的浏览器(微软都放弃了ie,到底是谁还在用)中,因为箭头函数是ES6的语法,老的浏览器不支持,所以我们需要将箭头函数转换为普通函数

原始代码

文件 test.js 包含以下 JavaScript 代码:

javascript 复制代码
const add = (a, b) => a + b;
console.log(add(2, 3));

通过 Babel 转换后的输出

转换后的代码将所有的箭头函数转换为普通函数:

javascript 复制代码
const add = function (a, b) {
  return a + b;
};
console.log(add(2, 3));

转换过程的核心代码

转换代码如下,我们使用了 Babel 的同步转换方法和一个自定义插件 transformArrowFunctions

javascript 复制代码
const babel = require("@babel/core");
const transformArrowFunctions = require("./transformArrowFunctions.js");
const fs = require("fs");
const path = require("path");

const code = fs.readFileSync(path.resolve(__dirname, "./test.js"), "utf-8");

const output = babel.transformSync(code, {
  plugins: [transformArrowFunctions],
});

console.log(output.code);

console.log("------------函数执行如下-----------");

const func1 = new Function("parm1", "parm2", output.code);
func1(1, 2);

插件 transformArrowFunctions.js 的代码

这个 Babel 插件简单但有效,它将所有的 foo 标识符替换为 bar

javascript 复制代码
module.exports = function ({ types: t }) {
  return {
    visitor: {
      ArrowFunctionExpression(path) {
        const { params, body, async } = path.node;
        // 如果主体不是 BlockStatement,则将其转换为 BlockStatement
        const functionBody = t.isBlockStatement(body)
          ? body
          : t.blockStatement([t.returnStatement(body)]);

        const functionExpression = t.functionExpression(
          null, // 函数名称,箭头函数是匿名的
          params, // 函数参数
          functionBody, // 函数体
          false, // generator
          async // 是否为异步函数
        );

        // 替换箭头函数为普通函数
        path.replaceWith(functionExpression);
      },
    },
  };
};

解析代码

ArrowFunctionExpression 对应的是箭头函数节点,遇到箭头函数就会执行这个方法

箭头函数和普通函数的区别有下面明显的两点:

  1. 匿名函数
  2. 没有自己的this,箭头函数的this是继承外层的this

那么在转换之后,那个执行的普通函数也模拟箭头函数的特性,保持执行结果一致

看看上面的代码,首先是判断箭头函数的主体是否是一个代码块,如果不是,就将其转换为代码块

这是因为箭头函数支持极致的简洁,如果主体是一个表达式,那么箭头函数会自动返回这个表达式的值,所以我们需要将其转换为代码块(花括号包裹📦),这样就构造好了函数体

然后就是创建一个普通函数,将箭头函数的参数和主体传入,最后将箭头函数替换为普通函数

FunctionExpression的源码 感兴趣的可以看看,反正我当前是没看明白

总结

babel.transformSync 是 Babel 提供的一个同步方法,用于将输入的代码字符串通过指定的插件和预设进行转换。我们通过这个方法可以很方便地使用自定义的 Babel 插件来处理和转换 JavaScript 代码。希望这个示例能帮助你更好地理解 Babel 插件的使用方式

一个 hello world 级别示例,希望对有所帮助,大家一起进步~~

示例源码地址:github.com/Aoyia/nqbi

相关推荐
长风清留扬24 分钟前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
m0_7482478038 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
ZJ_.1 小时前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
joan_852 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特2 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
一个处女座的程序猿O(∩_∩)O5 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.11 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖12 小时前
[react]searchParams转普通对象
开发语言·前端·javascript