什么是宏?
在计算机编程中,宏(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 结尾,比如 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 });
`,
},
],
});
可以看到 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。可以根据实际场景随意编写宏。