[英]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)吗?
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
是否是标识符intl
和property
标识符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. 它们还检查节点是否为
null
或undefined
,因此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
有一个key
和value
,我们可以使用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
我们可以重用的intl
从path.node.callee.object
因为我们希望使用相同的对象,但更改属性。 For the property we need to create an Identifier
with the name messages
. 对于属性,我们需要创建一个名为
messages
的Identifier
。
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
表示computed
, null
表示可选)。 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.