Skip to content

Go back

从零实现一个Babel插件

Published:  at 

文章目录

Babel插件的作用

Babel 是一款通过插件转换代码的 JavaScript 编译器。掌握如何编写 Babel 插件,可以构建自定义代码转换、工具链与自动化重构。

  1. 解析(Parse):源码 → AST(抽象语法树)
  2. 转换(Transform):AST → 修改后的 AST(通过插件/预设)
  3. 生成(Generate):修改后的 AST → 输出代码

Babel插件结构

Babel 插件是一个函数:接收 babel 对象,返回包含 visitor 的对象:

// 基本插件结构
module.exports = function (babel) {
  // babel 内含:types、template、traverse、generate 等
  const { types: t } = babel;

  return {
    // visitor:键为 AST 节点类型名
    visitor: {
      // 遍历到 Identifier 节点时调用
      Identifier(path, state) {
        // 在此编写转换逻辑
      },
    },
  };
};

Babel核心API

types(t)

const { types: t } = babel;

// 类型检查
t.isIdentifier(node); // 判断是否为该节点类型
t.isIdentifier(node, { name: "x" }); // 带属性约束的检查
t.isNumericLiteral(node); // 是否为数字字面量

// 构造不同类型的节点
t.identifier("x"); // x
t.numericLiteral(42); // 42
t.stringLiteral("hello"); // 'hello'
t.binaryExpression("+", left, right); // left + right
t.callExpression(callee, args); // callee(args)
t.memberExpression(obj, prop); // obj.prop
t.arrowFunctionExpression(params, body); // (params) => body
t.functionDeclaration(id, params, body); // function id(params) {}
t.variableDeclaration("const", [declarator]); // const x = y
t.objectExpression([property]); // { key: value }
t.arrayExpression([elements]); // [elements]
t.templateLiteral(quasis, exprs); // `string ${expr}`

// 节点操作
t.cloneNode(node); // 深拷贝节点
t.removeProperties(node); // 移除特殊属性
t.validate(node, key, value); // 校验节点属性

traverse

const traverse = require("@babel/traverse").default;

// 手动遍历 AST
traverse(ast, {
  Identifier(path) {
    console.log(path.node.name);
    // 默认会继续进入子节点
    // path.skip(); // 跳过子节点
    // path.stop(); // 停止整次遍历
  },
  FunctionDeclaration(path) {
    // 访问父节点
    const parent = path.parent;
    // 访问兄弟节点所在容器
    const siblings = path.parentPath.container;
    // 获取作用域
    const scope = path.scope;
  },
});

template

const template = require("@babel/template").default;

// 简单模板
const buildRequire = template(`
  require(SOURCE)
`);
// 用法:buildRequire({ SOURCE: t.stringLiteral('./module') })

// 带占位符的模板
const buildWrapper = template(`
  (function() {
    BODY;
  })()
`);
// 用法:buildWrapper({ BODY: statementNode })

// 需要额外语法插件的导出模板
const buildExport = template(`
  module.exports = EXPRESSION
`, { plugins: ["proposal-export-default-from"] });

generate

const generate = require("@babel/generate").default;
const output = generate(
  ast,
  {
    sourceMaps: true, // 生成 source map
    comments: true, // 保留注释
    compact: false, // 是否压缩输出
  },
  code
);

// output = { code: '...', map: sourceMap }

path对象

Identifier(path) {
  // 节点相关
  path.node;           // 当前 AST 节点
  path.parent;         // 父节点
  path.parentPath;     // 父 path

  // 控制遍历
  path.skip();         // 不再遍历子节点
  path.stop();         // 停止整次遍历

  // 替换节点
  path.replaceWith(node);           // 替换为单个节点
  path.replaceWithMultiple(nodes);  // 替换为多个节点
  path.replaceWithSourceString(code); // 用源码字符串替换

  // 删除节点
  path.remove();       // 移除当前节点

  // 作用域
  path.scope;          // 当前作用域

  // 绑定
  path.getBinding('name'); // 获取变量绑定
  path.bindings;       // 当前作用域内全部绑定
}

state对象

// 插件可接收配置项
module.exports = function (babel) {
  return {
    visitor: {
      Program(path, state) {
        // state.opts = { optionName: value },来自插件配置
        // state.filename = 当前文件名
        // state.file = { ast, code, opts }
      },
    },
  };
};

// 用法:{ plugins: [ ['plugin-name', { optionName: value }] ] }

实现一个依赖注入的插件

// transform-deps.js —— 按使用自动注入依赖

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

  // import 语句模板
  const importTemplate = template(`
    import IDENTIFIER from 'MODULE';
  `);

  return {
    visitor: {
      // 从 Program 进入
      Program: {
        enter(path, state) {
          // 收集需要注入的标识符
          const depsToInject = new Set();
          const options = state.opts || {};
          const importMap = options.imports || {};

          // 扫描全文件使用的标识符
          path.traverse({
            Identifier(idPath) {
              const name = idPath.node.name;
              // 若是声明或局部变量则跳过
              if (idPath.scope.hasBinding(name)) return;
              // 判断是否需要按配置注入
              if (importMap[name]) {
                depsToInject.add(name);
              }
            },
          });

          // 已有 import,避免重复
          const existingImports = new Set();
          path.traverse({
            ImportDeclaration(importPath) {
              importPath.node.specifiers.forEach(spec => {
                if (t.isImportDefaultSpecifier(spec)) {
                  existingImports.add(spec.local.name);
                }
              });
            },
          });

          // 在文件顶部插入缺失的 import
          const body = path.node.body;
          let insertIndex = 0;

          // 若有 #!/usr/bin/env node,跳过第一行
          if (t.isStringLiteral(body[0]) && body[0].value.startsWith("#!")) {
            insertIndex = 1;
          }

          // 过滤已有的import模块
          const depsArray = Array.from(depsToInject).filter(
            dep => !existingImports.has(dep)
          );

          // 基于导入模板将代码
          depsArray.forEach(depName => {
            const importNode = importTemplate({
              IDENTIFIER: t.identifier(depName),
              MODULE: t.stringLiteral(importMap[depName]),
            });
            body.splice(insertIndex, 0, importNode);
          });
        },
      },

      // 转换:console.log → logger.log
      CallExpression(path, state) {
        const callee = path.node.callee;
        const options = state.opts || {};
        const transforms = options.transforms || {};

        // 转换 console.log
        if (
          t.isMemberExpression(callee) &&
          t.isIdentifier(callee.object, { name: "console" }) &&
          t.isIdentifier(callee.property) &&
          transforms.console
        ) {
          // 替换为 logger.log
          path.node.callee = t.memberExpression(
            t.identifier("logger"),
            callee.property
          );
        }
      },
    },
  };
};

如何在babel配置文件中使用编写好的插件

// .babelrc 或 babel.config.js
{
  "plugins": [
    [
      "./transform-deps.js",
      {
        "imports": {
          "React": "react",
          "lodash": "lodash",
          "axios": "axios"
        },
        "functionDeps": {
          "fetchData": [{ "name": "fetch", "source": "./utils/fetch" }]
        },
        "transforms": {
          "console": true,
          "promisify": true
        }
      }
    ]
  ]
}


Previous Post
基于 Web Vitals 的指标进行性能调优
Next Post
从零实现Webpack的异步导入功能