Разработка плагина 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" })
);

Я также не вводил никаких абстракций специально, чтобы избежать дополнительной когнитивной нагрузки, поэтому условия выглядят очень длинными.

Полезные ресурсы:

Другие вопросы по тегам