自己动手撸一个 Babel 插件

本文介绍了 Babel 的原理以及如何动手写一个 Babel 插件,如果对 JS 语法树不了解的童鞋可以参考我的上一篇文章 《JS 语法树学习(全)》

Babel 原理简介

Babel 工作原理分为下面三个阶段:

  1. Parse: 将代码解析成 AST
  2. Transform: 遍历 AST,通过插件对节点进行添加、更新及移除等操作
  3. Generate: 将 AST 转化成代码

案例讲解

下面我用一个简单的案例讲述如何构建一个 Babel 插件

// 转换前
import config from './config';
// 转换后
const config = require('./config');

转换前的 AST

Babel 的解析器 babylon fork 至 acorn,现在划到 babel 仓库下的 @babel/parser。我们可以通过 https://astexplorer.net/ 查看 acorn 解析后的语法树。

// import config from './config';
[
  ImportDeclaration {
    specifiers: [
      ImportDefaultSpecifier {
        local: Identifier { name: "config" }
      }
    ],
    source: StringLiteral { value: "./config" }
  }
]

转换后的 AST

// const config = require('./config');
[
  VariableDeclaration {
    kind: "const"
    declarations: [
      VariableDeclarator {
        id: Identifier { name: "config" },
        init: CallExpression {
          callee: Identifier { name: "require" },
          arguments: StringLiteral { value: "./config" }
        }
      }
    ]
  }
]

编写插件

const babel = require('@babel/core');
const types = require('@babel/types');

const myPlugin = {
  visitor: {
    ImportDeclaration(path) {
      const specifier = path.node.specifiers[0];
      // 判断说明符是 ImportDefaultSpecifier
      if (types.isImportDefaultSpecifier(specifier)) {
        const specName = specifier.local.name;
        const srcPath = path.node.source.value;
        // 构造新节点
        const node = types.variableDeclaration('const', [
          types.variableDeclarator(
            types.identifier(specName),
            types.callExpression(
              types.identifier('require'),
              [types.stringLiteral(srcPath)]
            )
          )
        ])
        // 替换 ImportDeclaration 节点
        path.replaceWith(node)
      }
    }
  }
}

// test
const source = `import a from 'b'`;
const { code } = babel.transform(source, { plugins: [myPlugin] })
console.log(code);

这里 visitor 是访问者模式里的概念,有点类似于迭代器,可以理解为遍历 AST 时的 iterator,我们定义了 ImportDeclaration 属性,所以当遍历到 ImportDeclaration 节点的时候就会进入到我们的回调函数里。

@babel/types 提供了判断和构造 AST 节点的能力。

ImportDeclaration(path: NodePath<ImportDeclaration>)path,为我们提供了操作 AST 节点的方法,但是它不等同于节点,它的定义在 @babel/traverse 中,其中一些重要属性可以参考下面这张图:

小结

根据上面用例,我们基本了解了如何通过编写 Babel 插件来修改 AST,但是 Babel 插件的功能也仅限于在 Transform 阶段,例如我想创造一个这样的语法:

// 转换前
const arr = [1,2,3,4,5][1:3];
// 转换后
const arr = [1,2,3,4,5].slice(1,3);

Babel 插件就无能为力了,因为语法解析器无法解析在 ArrayExpression 后面出现冒号的情况,如果想要在 Parse 阶段扩展词法和语法解析的能力,可以 fork Babel 源码进行修改,且听下回分解。