Всем привет! Меня зовут Кирилл и я работаю фронтенд-разработчиком. Я расскажу о том, как мы перевели несколько тысяч файлов, написанных на 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
punzik
Чего только не придумают, лишь бы не использовать lisp.
alexeymalov
Можете раскрыть свою мысль подробнее? Как здесь помог бы lisp?
punzik
Вообще, это была шутка. С долей нешутки, конечно. Гомоиконность лиспа позволяет модифицировать код легко и непринуждённо.