什么是宏?

在计算机编程中,宏(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-macroscreateMacro 来实现的,比如要实现一个对象转成 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。可以根据实际场景随意编写宏。