react-intl

时间:2017-10-13 16:04:50

标签: javascript babeljs react-intl babel-core

我注意到在比较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);
      },
    }
  };
};

所以这是我的问题:

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

1 个答案:

答案 0 :(得分:2)

  

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

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

  

如何检测对formatMessage的调用?

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

首先要确定被叫方是intl.formatMessage。如您所知,这是一个简单的对象属性访问,相应的AST节点称为MemberExpression。在这种情况下,访问者会收到匹配的AST节点CallExpression,为path.node。这意味着我们需要验证path.node.calleeMemberExpression。值得庆幸的是,这非常简单,因为babel.typesisX的形式提供方法,其中X是AST节点类型。

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

现在我们知道它是MemberExpression,其objectproperty对应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" })
) {}
  

如何检测通话中的参数数量? (如果有格式化,则不应该进行替换)

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()搜索属性,标识为id

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

idProp如果存在,则为相应的ObjectProperty,否则为undefined。当它不是undefined时,我们想要替换节点。

  

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

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

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

intl.messages["section.someid"];

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

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

messages

只需要前两个参数,其余的我们使用默认值(t.memberExpression(path.node.callee.object, t.identifier("messages")) falsecomputed可选)。现在我们可以使用null作为对象,我们需要查找与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
    )
  );
}

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

正如我多次提到的那样,这是一个天真的版本,很多案例都没有涵盖,有资格进行转换。覆盖更多案例并不困难,但您必须识别它们并将它们粘贴到AST Explorer中,它将为您提供所需的所有信息。例如,如果对象是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 ) ); } } } } } } }; } 而不是{ "id": "section.someid" },则无法对其进行转换,但除了{{1}之外,还要检查{ id: "section.someid" }这一过},像这样:

StringLiteral

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

有用的资源: