编写一个 Babel 插件

Babel转译流程

Babel 对源码进行转译时,主要有三个步骤

  1. 首先通过Babylon 将源码转化成 AST
  2. 然后再通过babel-traverse遍历 AST,找到需要更改的 AST 节点,对其进行修改
  3. 根据修改后的 AST,通过babel-generator将修改后的 AST重新生成源码 Babel插件主要是处理第二步。

Babylon

Babylon 是 Babel 的解析器,主要负责将源码转化成 AST。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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: [...]
// }

我们还能像下面这样传递选项给 parse()方法:

1
2
3
4
babylon.parse(code, {
  sourceType: "module", // default: "script"
  plugins: ["jsx"] // default: []
});

sourceType 可以是 “module” 或者 “script”,它表示 Babylon 应该用哪种模式来解析。 “module” 将会在严格模式下解析并且允许模块定义,“script” 则不会。

注意: sourceType 的默认值是 “script” 并且在发现 import 或 export 时产生错误。 使用 scourceType: “module” 来避免这些错误。

babel-traverse

Babel Traverse(遍历)模块维护了整棵AST树的状态,并且负责替换、移除和添加AST节点。

我们可以和 Babylon 一起使用来遍历和更新节点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 节点的工具库,它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。

可以运行以下命令来安装它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import traverse from "babel-traverse";
import * as t from "babel-types";

traverse(ast, {
  enter(path) {
    if (t.isIdentifier(path.node, { name: "n" })) {
      path.node.name = "x";
    }
  }
});

babel-generator

Babel Generator模块是 Babel 的代码生成器,它读取AST并将其转换为代码和源码映射(sourcemaps)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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: "..."
// }

你也可以给 generate() 方法传递选项。.

1
2
3
4
5
6
7
generate(ast, {
  retainLines: false,
  compact: "auto",
  concise: false,
  quotes: "double",
  // ...
}, code);

Babel 插件编写

Babel 插件是一个接收了当前babel对象作为参数的函数

1
2
3
export default function(babel) {
  // plugin contents
}

我们可以直接取解构出 babel.types

1
2
3
export default function({ types: t }) {
  // plugin contents
}

接着返回一个对象,其 visitor 属性是这个插件的主要访问者。

1
2
3
4
5
6
7
export default function({ types: t }) {
  return {
    visitor: {
      // visitor contents
    }
  };
};

Visitor 中的每个函数接收2个参数:path 和 state

1
2
3
4
5
6
7
8
export default function({ types: t }) {
  return {
    visitor: {
      Identifier(path, state) {},
      ASTNodeTypeHere(path, state) {}
    }
  };
};

Babel 插件编写思路

  1. Babel 插件主要是对 AST 进行转换
  2. 通过astexplorer.net来比较转换前和转换后的 AST 的差异。
  3. 通过插件visitor模式遍历需要修改的 AST 节点
  4. 通过babel-types来判断AST节点类型或者修改、构建新的AST节点
  5. 通过path.replaceWith()或者path.replaceWithMultiple()替换掉原来的节点

箭头函数插件

1
const square = n => n * n;

我们写一个插件将上面的箭头函数转化成下面的普通函数

1
2
3
const square = function (n) {
  return n * n;
}

Babel 插件主要是对 AST 进行转换 首先我们可以通过astexplorer.net来观察一下,转换前和转换后的 AST 的差异。 转换前: AST

转换后:

比较两个AST 后,我们知道要将ArrowFunctionExpression替换成FunctionExpression,BinaryExpression替换成BlockStatement,那我们就要构建一个FunctionExpression节点,如何构建FunctionExpression节点呢,我们可以在babel-types查找构建方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
let babel = require('@babel/core')
let t = require('babel-types')

let transformArrayFunctions = {
  visitor: {
    ArrowFunctionExpression: (path, state)=>{
      let node = path.node;
      let id = path.parent.id;
      let params = node.params;
      let body = t.blockStatement([t.returnStatement(node.body)])
      let functionExpression = t.functionExpression(id,params,body,false,false);
      path.replaceWith(functionExpression);
    }
  }
}
const result = babel.transform(code, {
    plugins: [transformArrowFunctions]
});
console.log(result.code);


comments powered by Disqus
The LatestT