用于 react-intl 的 Babel 插件开发

Babel plugin development for react-intl

我注意到在比较 intl.formatMessage({ id: 'section.someid' })intl.messages['section.someid'] 之后,react-intl 有一些性能提升的机会。 在此处查看更多信息:https://github.com/yahoo/react-intl/issues/1044

第二种速度快 5 倍(并且在包含大量翻译元素的页面中产生巨大差异)但似乎不是官方方法(我猜他们可能会在未来版本中更改变量名称).

所以我想创建一个 babel 插件来执行转换(formatMessage( 到 messages[)。但是我在做这件事时遇到了麻烦,因为 babel 插件的创建没有很好的记录(我找到了一些教程,但它没有我需要的)。我了解了基础知识,但还没有找到我需要的访问者函数名称。

我的样板代码目前是:

module.exports = function(babel) {
  var t = babel.types;
  return {
    visitor: {
      CallExpression(path, state) {
        console.log(path);
      },
    }
  };
};

所以这是我的问题:

Which visitor method do I use to extract classes calls - intl.formatMessage (is it it really CallExpression) ?

是的,它是一个CallExpression,与函数调用相比,方法调用没有特殊的AST节点,唯一改变的是接收者(被调用者)。每当您想知道 AST 是什么样子时,您可以使用神奇的 AST Explorer。作为奖励,您甚至可以通过在 Transform 菜单中选择 Babel 在 AST Explorer 中编写 Babel 插件。

How do I detect a call to formatMessage ?

为简洁起见,我将只关注对 intl.formatMessage(arg) 的确切调用,对于一个真正的插件,您还需要涵盖具有不同 AST 表示的其他情况(例如 intl["formatMessage"](arg))。

首先要确定被叫方是intl.formatMessage。如您所知,那是一个简单的对象 属性 访问,对应的 AST 节点称为 MemberExpression。访问者收到匹配的 AST 节点,在本例中为 CallExpression,为 path.node。这意味着我们需要验证 path.node.callee 是一个 MemberExpression。值得庆幸的是,这很简单,因为 babel.types 提供了 isX 形式的方法,其中 X 是 AST 节点类型。

if (t.isMemberExpression(path.node.callee)) {}

现在我们知道它是一个MemberExpression,它有一个object和一个property对应于object.property。所以我们可以检查 object 是否是标识符 intlproperty 是否是标识符 formatMessage。为此,我们使用 isIdentifier(node, opts),它带有第二个参数,允许您检查它是否具有给定值的 属性。所有 isX 方法都是提供快捷方式的形式,有关详细信息,请参阅 Check if a node is a certain type。他们还检查节点是否不是 nullundefined,因此 isMemberExpression 在技术上不是必需的,但您可能希望以不同方式处理其他类型。

if (
  t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
  t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
) {}

How do I detect the number of parameters in the call ? (the replacement is not supposed to happen if there is formatting)

CallExpression有一个arguments属性,是参数的AST节点数组。同样,为简洁起见,我只考虑仅使用一个参数的调用,但实际上您也可以转换 intl.formatMessage(arg, undefined) 之类的东西。在这种情况下,它只是检查 path.node.arguments 的长度。我们还希望参数是一个对象,因此我们检查 ObjectExpression.

if (
  path.node.arguments.length === 1 &&
  t.isObjectExpression(path.node.arguments[0])
) {}

一个ObjectExpression有一个properties属性,这是一个ObjectProperty节点的数组。您可以从技术上检查 id 是唯一的 属性,但我将在此处跳过它,而只查找 id 属性。 ObjectProperty有一个keyvalue,我们可以用Array.prototype.find()来搜索属性,key是标识符id

const idProp = path.node.arguments[0].properties.find(prop =>
  t.isIdentifier(prop.key, { name: "id" })
);

idProp如果存在就是对应的ObjectProperty,否则就是undefined。当不是undefined我们要替换节点

How I do the replacement ? (intl.formatMessage({ id: 'something' }) to intl.messages['something'] ?

我们要替换整个CallExpression,Babel 提供了path.replaceWith(node)。唯一剩下的就是创建应该替换它的 AST 节点。为此,我们首先需要了解 intl.messages["section.someid"] 在 AST 中是如何表示的。 intl.messagesMemberExpression 就像 intl.formatMessage 一样。 obj["property"] 是计算的 属性 对象访问,在 AST 中也表示为 MemberExpression,但 computed 属性 设置为 true.这意味着 intl.messages["section.someid"] 是一个 MemberExpressionMemberExpression 作为对象。

记住这两个在语义上是等价的:

intl.messages["section.someid"];

const msgs = intl.messages;
msgs["section.someid"];

要构建 MemberExpression 我们可以使用 t.memberExpression(object, property, computed, optional)。为了创建 intl.messages,我们可以重用 path.node.callee.object 中的 intl,因为我们想使用相同的对象,但要更改 属性。对于 属性 我们需要创建一个名为 messages.

Identifier
t.memberExpression(path.node.callee.object, t.identifier("messages"))

只有前两个参数是必需的,其余参数我们使用默认值(false 表示 computednull 表示可选)。现在我们可以使用 MemberExpression 作为对象,我们需要查找与 id 的值相对应的计算 属性(第三个参数设置为 true) 属性,在我们之前计算的 idProp 上可用。最后我们用新创建的节点替换 CallExpression 节点。

if (idProp) {
  path.replaceWith(
    t.memberExpression(
      t.memberExpression(
        path.node.callee.object,
        t.identifier("messages")
      ),
      idProp.value,
      // Is a computed property
      true
    )
  );
}

完整代码:

export default function({ types: t }) {
  return {
    visitor: {
      CallExpression(path) {
        // Make sure it's a method call (obj.method)
        if (t.isMemberExpression(path.node.callee)) {
          // The object should be an identifier with the name intl and the
          // method name should be an identifier with the name formatMessage
          if (
            t.isIdentifier(path.node.callee.object, { name: "intl" }) &&
            t.isIdentifier(path.node.callee.property, { name: "formatMessage" })
          ) {
            // Exactly 1 argument which is an object
            if (
              path.node.arguments.length === 1 &&
              t.isObjectExpression(path.node.arguments[0])
            ) {
              // Find the property id on the object
              const idProp = path.node.arguments[0].properties.find(prop =>
                t.isIdentifier(prop.key, { name: "id" })
              );
              if (idProp) {
                // When all of the above was true, the node can be replaced
                // with an array access. An array access is a member
                // expression with a computed value.
                path.replaceWith(
                  t.memberExpression(
                    t.memberExpression(
                      path.node.callee.object,
                      t.identifier("messages")
                    ),
                    idProp.value,
                    // Is a computed property
                    true
                  )
                );
              }
            }
          }
        }
      }
    }
  };
}

完整代码和一些测试用例可以在this AST Explorer Gist.

中找到

正如我多次提到的,这是一个幼稚的版本,许多情况没有涵盖,这些情况有资格进行转换。涵盖更多案例并不难,但您必须识别它们并将它们粘贴到 AST Explorer 中,这将为您提供所需的所有信息。例如,如果对象是 { "id": "section.someid" } 而不是 { id: "section.someid" },则它不会被转换,但是覆盖它就像检查 StringLiteral 除了 Identifier 一样简单,像这样:

const idProp = path.node.arguments[0].properties.find(prop =>
  t.isIdentifier(prop.key, { name: "id" }) ||
  t.isStringLiteral(prop.key, { value: "id" })
);

我也没有刻意引入任何抽象以避免额外的认知负担,因此条件看起来很长。

有用的资源: