用于 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);
},
}
};
};
所以这是我的问题:
- 我使用哪种访问者方法来提取 类 调用 - intl.formatMessage(真的是 CallExpression)吗?
- 如何检测对 formatMessage 的调用?
- 如何检测调用中的参数数量? (如果有格式化,应该不会发生替换)
- 如何更换? (intl.formatMessage({ id: 'something' }) 到 intl.messages['something'] ?
- (可选)有没有办法检测 formatMessage 是否真的来自 react-intl 库?
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
是否是标识符 intl
而 property
是否是标识符 formatMessage
。为此,我们使用 isIdentifier(node, opts)
,它带有第二个参数,允许您检查它是否具有给定值的 属性。所有 isX
方法都是提供快捷方式的形式,有关详细信息,请参阅 Check if a node is a certain type。他们还检查节点是否不是 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)
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
有一个key
和value
,我们可以用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.messages
是 MemberExpression
就像 intl.formatMessage
一样。 obj["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
,我们可以重用 path.node.callee.object
中的 intl
,因为我们想使用相同的对象,但要更改 属性。对于 属性 我们需要创建一个名为 messages
.
的 Identifier
t.memberExpression(path.node.callee.object, t.identifier("messages"))
只有前两个参数是必需的,其余参数我们使用默认值(false
表示 computed
,null
表示可选)。现在我们可以使用 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" })
);
我也没有刻意引入任何抽象以避免额外的认知负担,因此条件看起来很长。
有用的资源:
我注意到在比较 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 库?
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
是否是标识符 intl
而 property
是否是标识符 formatMessage
。为此,我们使用 isIdentifier(node, opts)
,它带有第二个参数,允许您检查它是否具有给定值的 属性。所有 isX
方法都是提供快捷方式的形式,有关详细信息,请参阅 Check if a node is a certain type。他们还检查节点是否不是 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)
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
有一个key
和value
,我们可以用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.messages
是 MemberExpression
就像 intl.formatMessage
一样。 obj["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
,我们可以重用 path.node.callee.object
中的 intl
,因为我们想使用相同的对象,但要更改 属性。对于 属性 我们需要创建一个名为 messages
.
Identifier
t.memberExpression(path.node.callee.object, t.identifier("messages"))
只有前两个参数是必需的,其余参数我们使用默认值(false
表示 computed
,null
表示可选)。现在我们可以使用 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" })
);
我也没有刻意引入任何抽象以避免额外的认知负担,因此条件看起来很长。
有用的资源: