react-intl的Babel插件开发



在比较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.calleeMemberExpression。值得庆幸的是,这非常简单,因为babel.types提供了isX形式的方法,其中X是AST节点类型。

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

现在我们知道它是一个MemberExpression,它有一个对应于object.propertyobjectproperty。因此,我们可以检查object是否是标识符intlproperty是否是标识符formatMessage。为此,我们使用isIdentifier(node, opts),它采用第二个参数,允许您检查它是否具有给定值的属性。所有isX方法都是这种形式,以提供快捷方式,有关详细信息,请参阅检查节点是否为特定类型。它们还检查节点是否不是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:'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,因为我们希望使用相同的对象,但要更改属性。对于属性,我们需要创建一个名为messagesIdentifier

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

只需要前两个参数,其余参数使用默认值(false表示computednull表示可选)。现在,我们可以使用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课程

最新更新