简体   繁体   English

反应-intl的Babel插件开发

[英]Babel plugin development for react-intl

I noticed that there are some performance gain opportunities for react-intl after comparing intl.formatMessage({ id: 'section.someid' }) vs intl.messages['section.someid'] . 我注意到在比较intl.formatMessage({ id: 'section.someid' })intl.messages['section.someid']之后,react-intl有一些性能获得机会。 See more here: https://github.com/yahoo/react-intl/issues/1044 在此处查看更多信息: https//github.com/yahoo/react-intl/issues/1044

The second is 5x faster (and makes a huge difference in pages with a lot of translated elements) but doesn't seem to be the official way to do it (I guess they might change the variable name in future versions). 第二个是快5倍(并且在具有大量翻译元素的页面中产生巨大差异)但似乎不是正式的方法(我猜他们可能会在未来版本中更改变量名称)。

So I had the idea to create a babel plugin that does the transformation (formatMessage( to messages[). But I'm having trouble doing it because babel plugins creation is not well documented (I found some tutorials but it doesn't have what I need). I understood the basics but didn't find the visitor function name I need yet. 所以我有想法创建一个进行转换的babel插件(formatMessage(对于messages [)。但是我很难做到这一点因为babel插件创建没有很好的文档记录(我发现了一些教程,但它没有什么我需要)。我理解基础但没有找到我需要的访客功能名称。

My boilerplate code is currently: 我的样板代码目前是:

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

So here are my questions: 所以这是我的问题:

  • Which visitor method do I use to extract classes calls - intl.formatMessage (is it it really CallExpression) ? 我使用哪种访问器方法来提取类调用 - intl.formatMessage(它真的是CallExpression)吗?
  • How do I detect a call to formatMessage ? 如何检测对formatMessage的调用?
  • How do I detect the number of parameters in the call ? 如何检测呼叫中的参数数量? (the replacement is not supposed to happen if there is formatting) (如果有格式化,则不应该进行替换)
  • How I do the replacement ? 我如何更换? (intl.formatMessage({ id: 'something' }) to intl.messages['something'] ? (intl.formatMessage({id:'something'})到intl.messages ['something']?
  • (optionally) Is there a way to detect if formatMessage really comes from react-intl library ? (可选)有没有办法检测formatMessage是否真的来自react-intl库?

Which visitor method do I use to extract classes calls - intl.formatMessage (is it it really CallExpression) ? 我使用哪种访问器方法来提取类调用 - intl.formatMessage(它真的是CallExpression)吗?

Yes, it is a CallExpression , there is no special AST node for a method call compared to a function call, the only thing that changes is the receiver (callee). 是的,它是一个CallExpression ,与函数调用相比,方法调用没有特殊的AST节点,唯一改变的是接收器(被调用者)。 Whenever you're wondering what the AST looks like, you can use the fantastic AST Explorer . 无论何时你想知道AST是什么样的,你都可以使用梦幻般的AST Explorer As a bonus, you can even write the Babel plugin in the AST Explorer by selecting Babel in the Transform menu. 作为奖励,您甚至可以通过在“变换”菜单中选择“Babel”来在AST Explorer中编写Babel插件。

How do I detect a call to formatMessage ? 如何检测对formatMessage的调用?

For brevity I will only focus on the exact call to intl.formatMessage(arg) , for a real plugin you would need to cover other cases as well (eg intl["formatMessage"](arg) ) which have a different AST representation. 为了简洁起见,我将只关注对intl.formatMessage(arg)的确切调用,对于一个真正的插件,你需要覆盖其他情况(例如intl["formatMessage"](arg) ),它们具有不同的AST表示。

The first thing is to identify that the callee is intl.formatMessage . 首先要确定被调用者是intl.formatMessage As you know, that is a simple object property access, and the corresponding AST node is called MemberExpression . 如您所知,这是一个简单的对象属性访问,相应的AST节点称为MemberExpression The visitor receives the matching AST node, CallExpression in this case, as path.node . 访问者接收匹配AST节点, CallExpression在这种情况下,作为path.node That means we need to verify that path.node.callee is a MemberExpression . 这意味着我们需要验证path.node.callee是一个MemberExpression Thankfully, that is quite simple, because babel.types provides methods in the form of isX where X is the AST node type. 幸运的是,这是很简单的,因为babel.types提供的形式的方法isX其中X是AST节点类型。

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

Now we know that it's a MemberExpression , which has an object and a property that correspond to object.property . 现在我们知道它是一个MemberExpression ,它有一个object和一个与object.property对应的property So we can check if object is the identifier intl and property the identifier formatMessage . 因此我们可以检查object是否是标识符intlproperty标识符formatMessage For this we use isIdentifier(node, opts) , which takes a second argument that allows you to check that it has a property with the given value. 为此,我们使用isIdentifier(node, opts) ,它接受第二个参数,允许您检查它是否具有给定值的属性。 All isX methods are of that form to provide a shortcut, for details see Check if a node is a certain type . 所有isX方法都具有提供快捷方式的形式,有关详细信息,请参阅检查节点是否为特定类型 They also check for the node not being null or undefined , so the isMemberExpression was technically not necessary, but you might want to handle another type differently. 它们还检查节点是否为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) (如果有格式化,则不应该进行替换)

The CallExpression has an arguments property, which is an array of the AST nodes of the arguments. CallExpression有一个arguments属性,它是arguments的AST节点的数组。 Again, for brevity, I'll only consider calls with exactly one argument, but in reality you could also transform something like intl.formatMessage(arg, undefined) . 同样,为简洁起见,我只考虑只有一个参数的调用,但实际上你也可以转换像intl.formatMessage(arg, undefined)这样的东西。 In this case it's simply checking the length of path.node.arguments . 在这种情况下,它只是检查path.node.arguments的长度。 We also want the argument to be an object, so we check for an ObjectExpression . 我们还希望参数是一个对象,因此我们检查一个ObjectExpression

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

An ObjectExpression has a properties property, which is an array of ObjectProperty nodes. ObjectExpression有一个properties属性,它是ObjectProperty节点的数组。 You could technically check that id is the only property, but I will skip that here and instead only look for an id property. 你可以从技术上检查id是唯一的属性,但我会在这里跳过它,而只是查找id属性。 The ObjectProperty has a key and value , and we can use Array.prototype.find() to search for the property with the key being the identifier id . ObjectProperty有一个keyvalue ,我们可以使用Array.prototype.find()来搜索属性为标识符id的属性。

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

idProp will be the corresponding ObjectProperty if it exists, otherwise it will be undefined . idProp将是相应的ObjectProperty如果存在),否则将是undefined When it is not undefined we want to replace the node. 当它没有undefined我们想要替换节点。

How I do the replacement ? 我如何更换? (intl.formatMessage({ id: 'something' }) to intl.messages['something'] ? (intl.formatMessage({id:'something'})到intl.messages ['something']?

We want to replace the entire CallExpression and Babel provides path.replaceWith(node) . 我们想要替换整个CallExpression而Babel提供了path.replaceWith(node) The only thing left, is creating the AST node that it should be replaced with. 唯一剩下的就是创建应该替换它的AST节点。 For that we first need to understand how intl.messages["section.someid"] is represented in the AST. 为此,我们首先需要了解如何在AST中表示intl.messages["section.someid"] intl.messages is a MemberExpression just like intl.formatMessage was. intl.messages是一个MemberExpression ,就像intl.formatMessage一样。 obj["property"] is a computed property object access, which is also represented as a MemberExpression in the AST, but with the computed property set to true . obj["property"]是计算属性对象的访问,其也被表示为MemberExpression在AST,但与computed属性设置为true That means that intl.messages["section.someid"] is a MemberExpression with a MemberExpression as the object. 这意味着intl.messages["section.someid"]是一个MemberExpression其中一个MemberExpression作为对象。

Remember that these two are semantically equivalent: 请记住,这两个在语义上是等价的:

intl.messages["section.someid"];

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

To construct a MemberExpression we can use t.memberExpression(object, property, computed, optional) . 要构造一个MemberExpression我们可以使用t.memberExpression(object, property, computed, optional) For creating intl.messages we can reuse the intl from path.node.callee.object as we want to use the same object, but change the property. 为了创建intl.messages我们可以重用的intlpath.node.callee.object因为我们希望使用相同的对象,但更改属性。 For the property we need to create an Identifier with the name messages . 对于属性,我们需要创建一个名为messagesIdentifier

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

Only the first two arguments are required and for the rest we use the default values ( false for computed and null for optional). 只需前两个参数,其余的我们使用默认值( false表示computednull表示可选)。 Now we can use that MemberExpression as the object and we need to lookup the computed property (the third argument is set to true ) that corresponds to the value of the id property, which is available on the idProp we computed earlier. 现在我们可以使用MemberExpression作为对象,我们需要查找与id属性值相对应的计算属性(第三个参数设置为true ),该属性在我们之前计算的idProp上可用。 And finally we replace the CallExpression node with the newly created one. 最后,我们用新创建的节点替换CallExpression节点。

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

Full code: 完整代码:

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
                  )
                );
              }
            }
          }
        }
      }
    }
  };
}

The full code and some test cases can be found in this AST Explorer Gist . 完整的代码和一些测试用例可以在这个AST Explorer Gist中找到

As I've mentioned a few times, this is a naive version and many cases are not covered, which are eligible for the transformation. 正如我多次提到的那样,这是一个天真的版本,很多案例都没有涵盖,有资格进行转换。 It isn't difficult to cover more cases, but you have to identify them and pasting them into the AST Explorer will give you all the information you need. 覆盖更多案例并不困难,但您必须识别它们并将它们粘贴到AST Explorer中,它将为您提供所需的所有信息。 For example, if the object is { "id": "section.someid" } instead of { id: "section.someid" } it won't be transformed, but covering this is as simple as also checking for a StringLiteral besides an Identifier , like this: 例如,如果对象是{ "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" })
);

I also didn't introduce any abstractions on purpose to avoid additional cognitive load, therefore the conditions look very lengthy. 我也没有故意引入任何抽象来避免额外的认知负荷,因此条件看起来很长。

Helpful resources: 有用的资源:

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM