Всем привет! Меня зовут Кирилл и я работаю фронтенд-разработчиком. Я расскажу о том, как мы перевели несколько тысяч файлов, написанных на JavaScript, с легаси кода, который использовал goog.module
, на новые ES6-модули с помощью построения и преобразования абстрактного синтаксического дерева.
Эта статья будет полезна тем, у кого тоже возникла потребность в рефакторинге большого количества кода.
Причины, почему мы решили переводить нашу кодовую базу
В нашем проекте мы используем Google Closure Library. Google Closure Library — это JavaScript-библиотека, предоставляющая инструменты для работы с модулями. Подробнее об этом я расскажу ниже, но также можно прочитать документацию здесь.
Минусы goog.module
:
Длинные пространства имён
C goog-модулями плохо работают подсказки IDE
Появление скрытых зависимостей
Актуальность технологий
В итоге у нас появилась потребность перевода кодовой базы на ES6, но нас немного пугала необходимость внести изменения в большую кодовую базу (900 000 строк кода) и ничего при этом не сломать.
Выбор инструмента
Нам нужно было определить, каким образом мы будем изменять такое большое количество файлов.
Ручное изменение файлов проекта
Первое о чем мы задумались — это менять проект постепенно и вручную. Но вручную переводить код долго, неинтересно и можно ошибиться. Поэтому мы решили автоматизировать перевод.
Регулярные выражения
Этот вариант может оказаться очень даже привлекательным, и сначала нам тоже так казалось. Модули, объявленные с помощью goog.module
, перевести с помощью регулярных выражений легко, так как нам нужно только удалить объявление модуля и поменять формат экспорта. Пример goog.module
:
goog.module('my.module.Foo') // Достаточно удалить goog.module
const googArray = goog.require('goog.array')
class Foo {}
exports = Foo // И поменять exports = Foo на export {Foo}
Основные проблемы появляются, когда нужно переводить старые модули, в которых встречались конструкции, сложные для обработки регулярными выражениями:
goog.provide('old.module.Foo') // Нужно удалить goog.provide
goog.require('goog.array')
// Поменять goog.require('goog.array') на const array = goog.require('goog.array')
goog.scope(() => { // Удалить goog.scope
const array = goog.array // Удалить синоним
// Избавиться от namespace'а
old.module.Foo = goog.defineClass(null, { // Изменить синтаксис классов
constructor: function Foo() {
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = array.filter(numbers, num => num % 2 === 0);
console.log('Четные числа:', evenNumbers);
}
})
})
// Добавить экспорт export {Foo}
goog.provide('myapp.utils') // Нужно удалить goog.provide
// Избавиться от namespace'а function sum() {}
myapp.utils.sum = function (a, b) { return a + b }
// Избавиться от namespace'а function mul() {}
myapp.utils.mul = function (a, b) { return a * b }
// Добавить экспорт export {sum, mul}
Описать изменение таких файлов с помощью регулярных выражений в общем случае невозможно, поэтому мы выбрали выбрали другой способ преобразования кода.
Рефакторинг через преобразование структуры программы
Мы воспользовались утилитой jscodeshift. Это инструмент командной строки для автоматизированной модификации JavaScript-кода с помощью шаблонов.
jscodeshift строит абстрактное синтаксическое дерево (AST) из исходного кода, которое затем можно преобразовывать при помощи кодмодов (codemod), скриптов, которые манипулируют абстрактными синтаксическими деревьями.
Плюсы jscodeshift:
Декларативный стиль написания кодмодов (удобство чтения).
Многопоточная обработка файлов.
Возможность запускать кодмоды в режиме “dry run”, то есть без перезаписи обрабатываемых файлов. Это помогает во время написания кодмодов.
Минусы jscodeshift:
При преобразовании AST обратно в исходный код частично искажается форматирование кода. Например, некоторые пробелы могут замениться табуляциями.
Далее расскажем подробнее про абстрактное синтаксическое дерево и jscodeshift.
Абстрактное синтаксическое дерево
Абстрактное синтаксическое дерево (Abstract Syntax Tree, AST) представляет собой структуру данных, которая описывает синтаксическую структуру программы или её фрагмента.
Вот несколько ключевых понятий AST в контексте JavaScript:
Узлы: Узлы AST представляют конструкции языка JavaScript, такие как вызовы функций, объявления переменных, операторы присваивания и т.д.
Типы узлов: Каждый узел AST имеет свой тип, который указывает на конкретный элемент языка JavaScript. Примеры типов узлов: "FunctionDeclaration", "VariableDeclaration", "BinaryExpression" и т.д.
Листья: Листья AST представляют элементарные конструкции. Например, листьями могут быть идентификаторы переменных, строковые литералы, числовые литералы и т. д.
Древовидная структура: AST представляет собой иерархическую структуру, где каждый узел может иметь ноль или более дочерних узлов. Например, узел "FunctionDeclaration" может иметь дочерние узлы для имени функции, списка параметров и тела функции.
Принцип работы jscodeshift
jscodeshift работает в несколько этапов:
Строит AST из исходного кода
Преобразует AST с помощью кодмода
Превращает AST обратно в код
Теперь разберём представление AST в JavaScript и написание кодмода на таком примере:
goog.provide('math.add')
Визуально AST для этой программы выглядит так:
Это представление применяется при работе с деревом из кодмод-скриптов:
{
type: 'CallExpression',
callee: {
type: 'MemberExpression',
object: {
type: 'Identifier',
name: 'goog'
},
property: {
type: 'Identifier',
name: 'provide'
}
}
}
Пример кодмода, удаляющего все вызовы goog.provide:
// src/removeGoogProvideTransform.js
// Пример кодмода, удаляющего все вызовы goog.provide
export default removeGoogProvide(file, api) {
const jscs = api.jscodeshift
const root = jscs(file.source) // Получаем AST-дерево
return root
// Ищем CallExpression (вызовы функций)
// по шаблону {object: goog, property: provide}
.find(jscs.CallExpression, {
callee: {
object: {
name: 'goog'
},
property: {
name: 'provide'
}
}
})
// Удаляем все найденные элементы
.remove()
// Преврашаем AST в файл
.toSource()
}
Этот вызов утилиты jscodeshift удаляет из файла math/Add.js
все вызовы функции goog.provide
, используя написанный выше кодмод:
jscodeshift -t src/removeGoogProvideTransform.js math/Add.js
# флаг -t указывает путь к кодмоду
Процесс перевода
Мы также применили jscodeshift, чтобы автоматически модернизировать объявления классов, так как в нашем коде оставалось много мест, где классы объявлялись с помощью функции goog.defineClass
:
var Foo = goog.defineClass(Bar, {
constructor: function() {...}
doSomething: function() {...}
})
Кодмод преобразовывал объявление классов функцией goog.defineClass
в объявление с синтаксис ES6:
class Foo extends Bar {
constructor() {}
doSomething() {}
}
После того, как мы написали скрипт, мы запустили его на трёх небольших проектах (всего около 500 файлов). Перевод занял 1 час, который ушёл на запуск скрипта и ручную правку некоторых сложных моментов.
Затем мы провели обновление кода самого большого из наших проектов. Эта работа прошла в несколько этапов:
Автоматическое обновление исходников с помощью кодмодов.
Исправление ошибок и предупреждений сборки, которые проявились из-за того, что мы автоматизировали только часто встречающиеся паттерны. Недочёты, остающиеся в коде в нескольких местах, быстрее оказалось исправить вручную, чем писать и отлаживать кодмод.
Тестирование основных пользовательских кейсов проекта заняло около недели. После чего мы зарелизились и, кажется, ничего не сломали😁.
Итоги
Всего на данный момент мы изменили 3 908 файлов в 4 проектах. Мы планируем продолжать рефакторинг, чтобы в конечном итоге полностью избавиться от старых модулей в коде.
Использование jscodeshift значительно помогло нам в этом преобразовании, поэтому мы советуем вам тоже попробовать этот инструмент, чтобы ускорить процесс рефакторинга.
Советы
Если у вас есть похожая задача по обновлению большой кодовой базы, хотим поделиться несколькими рекомендациями:
Начинайте с маленьких проектов. Лучше переведите сначала маленькие проекты, у которых нет внешних зависимостей или их мало. Обкатайте решение на них. После этого будет проще переводить большие проекты.
Умейте остановиться. Рекомендуем автоматически трансформировать только часто встречающиеся паттерны. Недочёты, остающиеся в коде в нескольких местах, оказалось исправить вручную быстрее, чем писать и отлаживать кодмод.
Полезные ссылки
Мы собрали список ссылок с полезными материалами, которые могут помочь вам в переводе goog-модулей на ES6-модули
https://github.com/google/closure-compiler/wiki/Migrating-from-goog.modules-to-ES6-modules - Статья от Google по миграции с goog-модулей на ES6-синтаксис
https://github.com/facebook/jscodeshift - jscodeshift
https://www.youtube.com/watch?v=-YZt2DW75h8 - доклад от Александра Мышова по jscodeshift, который поможет понять принцип работы с jscodeshift
https://github.com/schmidtk/opensphere-jscodeshift/ - мы вдохновлялся фрагментами кода из этого репозитория, где разработчики решали проблему подобную нашей
https://astexplorer.net - построение AST дерева кода, полезно при использовании jscodeshift
https://doc.esdoc.org/github.com/mason-lang/esast/ - Описание узлов синтаксического дерева для JavaScript