3

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

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

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

我的样板代码目前是:

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

所以这是我的问题:

  • 我使用哪种访问者方法来提取类调用 - intl.formatMessage(它真的是 CallExpression)吗?
  • 如何检测对 formatMessage 的调用?
  • 如何检测调用中的参数数量?(如果有格式化,则不应该发生替换)
  • 我如何更换?(intl.formatMessage({ id: 'something' }) 到 intl.messages['something'] ?
  • (可选)有没有办法检测 formatMessage 是否真的来自 react-intl 库?
4

1 回答 1

2

我使用哪种访问者方法来提取类调用 - intl.formatMessage(它真的是 CallExpression)吗?

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

如何检测对 formatMessage 的调用?

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

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

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

现在我们知道它是一个MemberExpression,它有一个objectproperty对应于object.property。所以我们可以检查是否object是标识符intlproperty标识符formatMessage。为此,我们使用isIdentifier(node, opts),它接受第二个参数,允许您检查它是否具有具有给定值的属性。所有isX方法都采用这种形式来提供快捷方式,有关详细信息,请参阅检查节点是否为某种类型。他们还会检查节点是否不是nullor undefined,因此从isMemberExpression技术上讲这不是必需的,但您可能希望以不同的方式处理另一种类型。

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

如何检测调用中的参数数量?(如果有格式化,则不应该发生替换)

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

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

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

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

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

我如何更换?(intl.formatMessage({ id: 'something' }) 到 intl.messages['something'] ?

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

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

intl.messages["section.someid"];

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

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

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

只有前两个参数是必需的,其余的我们使用默认值(falseforcomputednullfor 可选)。现在我们可以使用它MemberExpression作为对象,我们需要查找与属性true值相对应的计算属性(第三个参数设置为 ),该id属性在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
                  )
                );
              }
            }
          }
        }
      }
    }
  };
}

完整的代码和一些测试用例可以在这个 AST Explorer Gist中找到。

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

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

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

有用的资源:

于 2017-10-13T21:36:52.380 回答