Разработка плагина Babel для реагирования
Я заметил, что есть некоторые возможности для повышения производительности для реакции-Intl после сравнения intl.formatMessage({ id: 'section.someid' })
против intl.messages['section.someid']
, Смотрите больше здесь: 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: 'что-то' }) для intl.messages['что-то']?
- (опционально) Есть ли способ определить, действительно ли formatMessage исходит из библиотеки response-intl?
1 ответ
Какой метод посетителя я использую для извлечения вызовов классов - intl.formatMessage (действительно ли это CallExpression)?
Да, это CallExpression
, нет специального узла AST для вызова метода по сравнению с вызовом функции, единственное, что изменяется, это получатель (вызываемый). Всякий раз, когда вам интересно, как выглядит AST, вы можете использовать фантастический AST Explorer. В качестве бонуса вы даже можете написать плагин Babel в AST Explorer, выбрав Babel в меню Transform.
Как я могу обнаружить звонок в formatMessage?
Для краткости я сосредоточусь только на точном обращении к intl.formatMessage(arg)
для реального плагина вы должны были бы охватить и другие случаи (например, intl["formatMessage"](arg)
) которые имеют другое представление AST.
Прежде всего, нужно определить, что вызываемый 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: 'что-то' }) для intl.messages['что-то']?
Мы хотим заменить весь CallExpression
и Бабель обеспечивает path.replaceWith(node)
, Осталось только создать узел AST, которым он должен быть заменен. Для этого нам сначала нужно понять, как intl.messages["section.someid"]
представлен в АСТ. intl.messages
это MemberExpression
как intl.formatMessage
было. obj["property"]
является вычисляемым объектом доступа объекта, который также представлен в виде 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
мы можем использовать intl
от path.node.callee.object
как мы хотим использовать тот же объект, но изменить свойство. Для собственности нам нужно создать Identifier
с именем messages
,
t.memberExpression(path.node.callee.object, t.identifier("messages"))
Требуются только первые два аргумента, а для остальных мы используем значения по умолчанию (false
за computed
а также null
по желанию). Теперь мы можем использовать это MemberExpression
в качестве объекта, и нам нужно искать вычисляемое свойство (третий аргумент установлен в true
), что соответствует значению id
свойство, которое доступно на 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
)
);
}
}
}
}
}
}
};
}
Полный код и некоторые тестовые примеры можно найти в этом 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" })
);
Я также не вводил никаких абстракций специально, чтобы избежать дополнительной когнитивной нагрузки, поэтому условия выглядят очень длинными.
Полезные ресурсы: