什么是宏?
在计算机编程中,宏(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 宏的好处就是
1. scope 方法只在需要的地方引入,不会全局污染,比如 eslint 会提示 scope 方法不存在。
2. 一些工具,比如 create-react-app 已经默认支持宏,不需要额外配置。
3. 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.js
const { 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/fbAQLP
exports[`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。可以根据实际场景随意编写宏。