Skip to content

开发 babel 插件

可能有用的链接

https://github.com/jamiebuilds/babel-handbook/blob/master/translations/en/plugin-handbook.md

babel 知识点:

1. plugins、preset 调用优先级

plugins 会从上到下顺序执行。

preset 会从下到上逆序执行。

plugins 先于 preset 执行。

2. babel 转换过程

@babel/core 将源代码转为 ast 抽象语法树。

经过 plugins、preset 的操作生成新的 ast。

最后反向将新的 ast 转换为新的代码。

3. 在线查看并分析生成 ast

https://astexplorer.net/

插件基础入门

典型的一个 babel 插件结构:

js
export default function({ types: babelTypes }) {
  return {
    visitor: {
      Identifier(path, state) {

      }
    }
  };
};

解释:

  • babelTypes:类似于 loadash 那样的工具函数库。主要用来操作 AST 节点,比如创建、校验、转变等。举例:判断某个节点是不是标识符(identifier)。

  • path:AST 中有很多节点,每个节点可能有不同的属性,并且节点之间可能存在关联。path 是个对象,它代表了两个节点之间的关联。你可以在 path 上访问到节点的属性,也可以通过 path 来访问到关联的节点(比如父节点、兄弟节点等)。path.node 就是当前遍历到的节点对象。可以通过这个网址查看。

  • state:代表了插件的状态,你可以通过 state 来访问插件的配置项。

  • visitor:Babel 采取递归的方式访问 AST 的每个节点,之所以叫做 visitor,只是因为有个类似的设计模式叫做访问者模式,不用在意背后的细节。

  • Identifier、ASTNodeTypeHere:AST 的每个节点,都有对应的节点类型,比如标识符(Identifier)、函数声明(FunctionDeclaration)等,可以在 visitor 上声明同名的属性,当 Babel 遍历到相应类型的节点,属性对应的方法就会被调用,传入的参数就是 path、state。

实现插件 bad-to-good

写插件:

js
module.exports = (params) => {
  // undefined
  console.log(params)
  return {
    name: 'bad-to-good',
    // babel 用了访问者设计模式来遍历节点。
    // 访问到一个节点时,visitor 下的属性方法钩子就会视情况被调用。
    visitor: {
      // 如果节点类型是标识符,就会调用这个钩子
      Identifier(path, state) {
        // path 是个对象。可以通过 path 访问到当前节点的信息,也可以获取相关联的节点信息。
        // state 代表插件状态。可以通过 state 访问到传入插件的配置参数。
        if (path.node.name === 'bad') {
          path.node.name = 'good'
        }
      }
    }
  }
}

引入插件(三种引入方式)

js
const badToGoodPlugin = require('./my-plugin')

module.exports = {
  plugins: [
    // badToGoodPlugin() // 这种方式拿不到参数
    // badToGoodPlugin
    './my-plugin'
  ]
}

实现插件传参 bad-to-good-2

插件函数:

js
module.exports = (params) => {
  // undefined
  // console.log(params)
  return {
    name: 'bad-to-good-2',
    visitor: {
      Identifier(path, state) {
        // 通过 state.opts 拿到配置参数
        const name = path.node.name
        if (state.opts[name]) {
          path.node.name = state.opts[name]
        }
      }
    }
  }
}

实现辅助插件:替换 process.env.NODE_ENV

插件函数:

js
// path.get('xx.xxx') // 返回当前节点下的 xx.xx 属性。等同于 path.node.xx.xxx
// path.node // 当前访问的节点对象
// path.toComputedKey // 获取成员表达式的 key。比如 process.env.NODE_ENV 的是 NODE_ENV。
// process.env 是 env。
// matchesPattern // 传入正则或字符串。如果匹配则返回 true。
module.exports = function({ types: babelTypes }) {
  return {
    name: "node-env-replacer",
    visitor: {
	    // 成员表达式
      // 表示一个对象的成员访问
      MemberExpression(path, state) {
        // 如果为 true,说明当前节点为 `process.env.NODE_ENV`
        if (path.get("object").matchesPattern("process.env")) {
		      // 这里返回结果为字符串字面量类型的节点
          const keyPath = path.toComputedKey();
          // 如果是字符串字面量
          if (babelTypes.isStringLiteral(keyPath)) {
            // path.replaceWith(newNode) 用来替换当前节点
            // babelTypes.valueToNode(value) 用来创建节点,
            // 如果 value 是字符串,则返回字符串字面量类型的节点
            path.replaceWith(babelTypes.valueToNode(process.env[keyPath.value]));
          }
        }
      }
    }
  };
};

生产模式下去除 DEBUG

比如下面这段代码,在生产模式下是会被清除的。

js
if (DEBUG) {
  console.log(123)
}

插件写法:

js
const t = require("@babel/types");

module.exports = () => {
  return {
    visitor: {
      // 标识符(变量名)
      Identifier: (path, state) => {
        const nodeName = path.node.name;
        const isDebug = nodeName === "DEBUG";
        const parentPath = path.parentPath; 
        const parentIsIf = t.isIfStatement(parentPath);
        if (isDebug && parentIsIf) {
          const isPro = process.env.NODE_ENV === 'production'
          if (isPro) {
            path.parentPath.remove();
          }
        }
      }
    },
  };
};

生成 ast 树

执行下面一段 js 代码:

js
import parser from "@babel/parser";

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

const res = parser.parse(code);
console.log(res)

打印的 res 如下:

与通过网站生成的 ast 对象结构是一样的。

path 的方法

  • traverse(visitor, state) 遍历当前节点的子节点,传入 visitor 和 state(state 是不同节点间传递数据的方式)

使用 api

shell
# 将代码解析为 ast
pnpm install --save @babel/parser
# 操作 ast
pnpm install --save @babel/traverse
# 工具库
pnpm install --save @babel/types
# 将 ast 转为源代码
pnpm install --save @babel/generator

将变量 n 替换为 x

js
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";

// 源代码
const code = `function square(n) {
  return n * n;
}`

// 将源代码解析为 ast
const ast = parser.parse(code)

// 访问并操作 ast 的节点
traverse.default(ast, {
  enter(path, state) {
    if (
      // 如果节点类型是变量名
      path.node.type === "Identifier" &&
      // 并且节点名字是 n
      path.node.name === "n"
    ) {
      // 名字替换为 x
      path.node.name = "x";
    }
  },
  // 节点类型作为函数属性名
})

// 将处理后的 ast 方向生成代码。
const outputCode = generate.default(ast, {
  // 保留行数
  retainLines: false,
  // 压紧
  compact: "auto",
  // 引号
  quotes: "double",
  // concise,简明的
  concise: false,
}, code)

console.log(outputCode)

替换全等符号两边的变量

源码:

js
foo === bar;

生成的 ast:

js
{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}

插件:

js
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";

// 源代码
const code = `foo === bar;`

// 将源代码解析为 ast
const ast = parser.parse(code);

// 访问并操作 ast 的节点
traverse.default(ast, {
  // 节点类型作为函数属性名
  // 注意不要写 visitor!!
  // visitor: {},
  BinaryExpression(path) {
    if (path.node.operator !== "===") {
      return
    }
    path.node.left = t.identifier("leftValue");
    path.node.right = t.identifier("rightValue");
  }
})

// 将处理后的 ast 方向生成代码。
const outputCode = generate.default(ast, {}, code)

console.log(outputCode)

获取子节点的 path

当匹配到节点类型函数时,我们能获取到节点的 path 对象。

path.node 就是当前匹配的 ast 节点对象。

如果想获取子节点的 path 对象,通过 path.get() 函数获取。

还是以这段代码为例:

js
const code = `foo === bar;`

生成的 ast 如下:

js
{
  type: "BinaryExpression",
  operator: "===",
  left: {
    type: "Identifier",
    name: "foo"
  },
  right: {
    type: "Identifier",
    name: "bar"
  }
}
js
// 名字为 foo 的节点 path 对象。
path.get('left')
// 名字为 bar 的节点 path 对象。
path.get('right')
js
import parser from "@babel/parser"
import traverse from "@babel/traverse"
import generate from "@babel/generator"
import * as t from "@babel/types";

const code = `foo === bar;`

const ast = parser.parse(code);

traverse.default(ast, {
  BinaryExpression(path) {
    // 获取子节点的 path
    const leftPath = path.get('left')
    if(leftPath) {
      console.log(`isFoo: ${leftPath.node.name === 'foo'}`)
    }
  }
})

const outputCode = generate.default(ast, {}, code)

检查一个节点的确切的节点类型

1、传入 t.isIdentifier 一个参数

js
BinaryExpression(path) {
  // 检查当前节点的 left 节点类型,是否是标识符
  if (t.isIdentifier(path.node.left)) {
    // ...
  }
}

2、传入 t.isIdentifier 两个参数

除了比较节点类型,也可以多比较一些属性:

js
BinaryExpression(path) {
  if (t.isIdentifier(path.node.left, { name: "n" })) {
    // ...
  }
}

这相当于:

js
BinaryExpression(path) {
  if (
    path.node.left != null &&
    path.node.left.type === "Identifier" &&
    path.node.left.name === "n"
  ) {
    // ...
  }
}

当前节点路径对象也能检查当前节点的类型

一个节点的路径对象也能判断当前节点的类型。

js
BinaryExpression(path) {
  if (path.get('left').isIdentifier({ name: "n" })) {
    // ...
  }
}

等效于:

js
BinaryExpression(path) {
  if (t.isIdentifier(path.node.left, { name: "n" })) {
    // ...
  }
}

如果是变量节点,检查在上下文中是否有引用关系

api

js
Identifier(path) {
  if (path.isReferencedIdentifier()) {
    // ...
  }
}

或者

js
Identifier(path) {
  if (t.isReferenced(path.node, path.parent)) {
    // ...
  }
}

什么情况下一个变量会被认为与代码中的其它部分有引用关系呢?

  1. 使用一个变量
  2. 从变量中取值

比如下面这个例子:

js
import babel from '@babel/core'
import * as t from "@babel/types";

const soureCode = 
`const a = 123 // 1
function fn() { // 2
  console.log(a) // 3
  const a = 123 // 4
} // 5
[b][0] // 6
fn() // 7`

const visitor = {
  Identifier(path) {
    // 当前节点是否引用了其它变量。
    // 注意,即使其它变量在代码中没有被显示声明,也会进入该 if 判断。
    if(path.isReferencedIdentifier()) {
      console.log(path.node.loc.start.line, path.node.name)
    }
  },
}

const {code} = babel.transform(soureCode, {
  plugins: [{ visitor }],
})

// console.log('res', code)

Released under the MIT License.