在比较intl.formatMessage({ id: 'section.someid' })
和intl.messages['section.someid']
之后,我注意到react intl有一些性能提升的机会。点击此处查看更多信息:https://github.com/yahoo/react-intl/issues/1044
第二种速度是原来的5倍(在有很多翻译元素的页面上会有很大的不同),但似乎不是官方的方法(我想他们可能会在未来的版本中更改变量名)。
因此,我有了创建一个babel插件的想法,它可以进行转换(formatMessage(to 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库
我使用哪个访问者方法来提取类调用-intl.formatMessage(它真的是CallExpression吗)?
是的,它是CallExpression
,与函数调用相比,方法调用没有特殊的AST节点,唯一改变的是接收器(被调用者)。当你想知道AST是什么样子的时候,你可以使用奇妙的AST Explorer。作为奖励,您甚至可以在AST Explorer中通过在Transform菜单中选择Babel来编写Babel插件。
如何检测对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
方法都是这种形式,以提供快捷方式,有关详细信息,请参阅检查节点是否为特定类型。它们还检查节点是否不是null
或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])
) {}
ObjectExpression
具有properties
属性,该属性是ObjectProperty
节点的阵列。从技术上讲,您可以检查id
是唯一的属性,但我将跳过这里,只查找id
属性。ObjectProperty
有一个key
和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中是如何表示的。CCD_ 50和CCD_ 52一样是CCD_。obj["property"]
是计算属性对象访问,在AST中也表示为MemberExpression
,但computed
属性设置为true
。这意味着intl.messages["section.someid"]
是以MemberExpression
为对象的MemberExpression
。
请记住,这两个在语义上是等价的:
intl.messages["section.someid"];
const msgs = intl.messages;
msgs["section.someid"];
为了构建CCD_ 60,我们可以使用CCD_。对于创建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
)
);
}
}
}
}
}
}
};
}
完整的代码和一些测试用例可以在这个ASTExplorerGist中找到。
正如我多次提到的,这是一个幼稚的版本,许多情况都没有涵盖在内,这些情况有资格进行转换。覆盖更多的案例并不困难,但你必须识别它们,并将它们粘贴到AST Explorer中,这将为你提供所需的所有信息。例如,如果对象是{ "id": "section.someid" }
而不是{ id: "section.someid" }
,则不会对其进行转换,但覆盖这一点就像除了检查Identifier
之外还检查StringLiteral
一样简单,如下所示:
const idProp = path.node.arguments[0].properties.find(prop =>
t.isIdentifier(prop.key, { name: "id" }) ||
t.isStringLiteral(prop.key, { value: "id" })
);
我也没有故意引入任何抽象来避免额外的认知负荷,因此条件看起来很长。
有用资源:
- 巴别塔插件手册
babel-types
- AST资源管理器
- 代码转换和使用AST的Linting-Kent C.Dods的FrontendMasters课程