什么是宏?
在计算机编程中,宏(Macro)是一种被预处理器处理的代码块或指令,用来在编译时进行代码替换或扩展,以便简化代码编写或实现一些特定功能。宏允许程序员定义自己的简短、易于理解的代码片段,然后在代码中重用这些宏。
Babel 宏也提供一个类似的概念,可以在编译时对代码进行转换,通过 AST 进行代码操作,生成新的代码。
Babel 宏和 Babel 插件的区别
Babel 宏和 Babel 插件都是用来对代码进行转换的工具,唯一的区别就是 Babel 宏可以在编码阶段局部动态指定转换行为,而 Babel 插件是在编译阶段全局代码转换的。
比如要实现一个 console.scope 方法来输出特定格式的日志。
- 使用 Babel 插件写法
console.scope("hello world");
- 使用 Babel 宏写法
import scope from "console.scope.macro";scope("hello world");
使用 Babel 宏的好处就是
scope 方法只在需要的地方引入,不会全局污染,比如 eslint 会提示 scope 方法不存在。
一些工具,比如 create-react-app 已经默认支持宏,不需要额外配置。
babel 宏相对于 plugin 更方便编写,可以指定代码片段进行转换。
Babel 宏实现
Babel 宏的实现原理是通过 babel-plugin-macros
的 createMacro
来实现的,比如要实现一个对象转成 Proxy 的宏。
注意定义宏的文件一定要以 .macro 结尾。
createMacro 参数解释
reference
reference 对象包含了宏在代码中的引用信息,值时一个数组,每一个元素对应引用位置的 Babel 节点路径,通过 reference 可以知道哪里引用了,以及如何访问这些节点。
比如我们要实现一个宏,实现 uppercase('hello')
转换成 uppercase(String.prototype.toUpperCase.call('hello'))
,我们可以通过 reference 来实现:
const { createMacro } = require("babel-plugin-macros");function myMacro({ references, babel }) { const t = babel.types; // Babel types 工具 // 处理 uppercase 引用 if (references.uppercase) { references.uppercase.forEach((referencePath) => { const newNode = t.callExpression( t.memberExpression(t.identifier("String"), t.identifier("prototype.toUpperCase"), false), [referencePath.parentPath.node.arguments[0] || t.stringLiteral("")] ); referencePath.parentPath.replaceWith(newNode); }); }}module.exports = createMacro(myMacro);
state
state 对象包含了宏在代码中的状态信息,值是一个对象,包含了宏在代码中的状态信息,比如文件路径,babel 配置项,通过 state 可以获取到文件相关信息。
babel
babel 对象是 babel-core 的引用,可以通过 babel 对象获取到 babel-core 的所有 API,比如 types 工具,parse 解析器等。执行代码的转换工作。例如使用 babel.types.stringLiteral('some value') 创建一个字符串字面量节点。
实战
我们新建一个项目,目录结构如下:
|-- tests |-- index.test.js|--.babel.config.js|--package.json|--console.scope.macro.js
编写一个 Proxy 功能,新建一个 console.scope.macro.js ,编写代码如下:
// console.scope.macro.jsconst { createMacro } = require("babel-plugin-macros");function constantMacro({ references, state, babel }) { // references 是一个对象,包含了所有引用了宏的地方 // state 是一个对象,包含了一些有用的方法和属性 // babel 是 babel-core 的引用,可以用来操作 AST const handler = ` get(target, key) { if (key in target) { console.log('get key:', key, 'value:', target[key]); return target[key]; } throw new Error("Property " + key + " does not exist."); } `; const code = ` new Proxy($0, { ${handler} }) `; references.default.forEach(({ parentPath }) => { // parentPath 是一个 Babel NodePath,可以用来操作 AST // 例如,可以用 parentPath.replaceWith(newNode) 来替换节点 // 从 AST 中获取参数 const args = parentPath.get("arguments"); // 获取args的代码 const objectStr = args[0].getSource(); const newNode = babel.template.ast(code, { placeholderPattern: /^\$\d+/, }); newNode.expression.arguments[0].name = objectStr; // 替换节点 parentPath.replaceWith(newNode); });}module.exports = createMacro(constantMacro);
测试用例
在 tests/index.test.js 文件编写测试用例代码。
const pluginTester = require("babel-plugin-tester");const plugin = require("babel-plugin-macros");pluginTester({ plugin, snapshot: true, tests: [ { title: "转成Proxy对象", code: ` import constant from '../constant.macro'; const obj = constant({ a: 1, b: 2 }); `, }, ],});
通过 jest 跑测试用例,可以看到 snap 输出结果如下:
// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`macros 1. should work: 1. should work 1`] = `import constant from '../constant.macro';const obj = constant({ a: 1, b: 2 });↓ ↓ ↓ ↓ ↓ ↓const obj = new Proxy( { a: 1, b: 2 }, { get(target, key) { if (key in target) { return target[key]; } throw new Error('Property ' + key + ' does not exist.'); }, });`;
上面只是一个简单的代码转换,实际上 Babel 宏可以实现更复杂的代码转换,比如 styled-components 的 css 宏,将 css 转成 className。可以根据实际场景随意编写宏。