[Перевод] Как Chrome DevTools с велосипеда на стандарт пересели

zmi4jauh3j1h1pv0bvvbxhc1xcw.jpeg

Краткая заметка о том, как в команде Chrome DevTools проходила миграция с внутреннего загрузчика модулей на стандартные модули JavaScript. Рассказываем, насколько и почему затянулась миграция, о скрытых издержках миграции и расскажем о выводах команды DevTools после завершения миграции. Но начнём с истории инструментов веб-разработчика.

Введение


Как вы, возможно, знаете, Chrome DevTools — это веб-приложение HTML, CSS и JavaScript. За эти годы DevTools стала многофункциональной, умной и хорошо осведомленной о современной веб-платформе. Хотя DevTools расширился, его архитектура во многом напоминает первоначальную, когда инструмент был частью WebKit.

Мы расскажем историю DevTools, опишем преимущества и ограничения решений и что мы сделали, чтобы смягчить эти ограничения. Поэтому давайте углубимся в модульные системы, в то, как загружать код и то, как мы в конечном счете использовали модули JavaScript.

Вначале не было ничего


Сейчас у фронтенда есть множество модульных систем и их инструментов, а также стандартизированный формат модулей JavaScript. Ничего этого не было, когда начинался DevTools. Инструмент построен поверх кода WebKit, написанного более 12 лет назад.

Первое упоминание о модульной системе в DevTools относится к 2012 году: это было введение списка модулей с соответствующим списком исходников. Часть инфраструктуры Python, используемой в то время для компиляции и сборки DevTools. В 2013 года модули были извлечены в файл frontend_modules.json этим коммитом, а затем, в 2014 году, в отдельные module.json (здесь). Пример module.json:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}


С 2014 года module.json используется в инструментах разработчика для указания на модули и исходные файлы. Тем временем экосистема веба быстро развивалась, и было создано множество форматов модулей: UMD, CommonJS и в конечном итоге стандартизированные модули JavaScript. Однако DevTools застрял на module.json. Не стандартизированная, уникальной модульная система имела несколько недостатков:

  1. module.json требовал собственные инструменты сборки.
  2. Не было интеграции с IDE. Конечно же, она требовала специальных инструментов для создания файлов, понятных ей: (оригинальный скрипт генерации jsconfig.json для VS Code).
  3. Функции, классы и объекты были помещены в глобальную область видимости, чтобы сделать возможным совместное использование между модулями.
  4. Был важен порядок перечисления файлов. Не было никакой гарантии, что код, на который вы полагаетесь, загружен, кроме проверки человеком.


В целом, оценивая текущее состояние модульной системы DevTools и других, более широко используемых форматов модулей, мы пришли к выводу, что module.json создавал больше проблем, чем решал.

Преимущества стандарта


Мы выбрали модули JavaScript. Когда принималось это решение, модули в языке еще включались флагом в Node.js и большое количество пакетов NPM не поддерживало их. Несмотря на это, мы пришли к выводу, что модули JavaScript были лучшим вариантом.

Основное преимущество модулей в том, что это стандартизированный в языке формат. Когда мы перечислили минусы module.json, мы поняли, что почти все они были связаны с использованием не стандартизированного, уникального формата модуля. Выбор не стандартизированного формата модуля означает, что мы должны вкладывать время в построение интеграций с помощью инструментов сборки и инструментов наших коллег. Такие интеграции часто были хрупкими и не поддерживали функции, требуя дополнительного времени на техническое обслуживание, а иногда приводя к хитрым багам. Баги в конечном итоге доставались пользователям.

Поскольку модули JavaScript были стандартными, это означало, что IDE, такие как VS Code, инструменты проверки типов, похожие на компилятор Closure/TypeScript, и инструменты сборки вроде Rollup и минификаторов смогут понять написанный исходный код. Более того, когда новый человек присоединяется к команде DevTools, ему не приходится тратить время на изучение проприетарного module.json.

Конечно, когда DevTools только начинались, ни одно из вышеперечисленных преимуществ не существовало. Потребовались годы работы в группах стандартов, при реализации среды выполнения. Потребовалось время на отзывы от разработчиков — пользователей модулей. Но когда модули в языке появились, у нас был выбор: либо продолжать поддерживать наш собственный формат, либо инвестировать в переход на новый формат.

Сколько стоит блеск новизны?


Несмотря на то, что модули JavaScript имели множество преимуществ, которые мы хотели бы использовать, мы оставались в мире module.json. Использование преимуществ модулей языка означало, что мы должны вкладывать значительные усилия в технический долг. Между тем, миграция могла вывести из строя функции и ввести баги регрессии.

Речь не шла о том, использовать ли мы модули JavaScript. Стоял вопрос, как дорого стоит возможность использовать модули JavaScript. Мы должны были уравновесить риск раздражения пользователей регрессиями, затратами времени инженеров на миграцию и периодом ухудшением состояния системы, в котором мы будем работать.

Последний пункт оказался очень важным. Несмотря на то, что теоретически мы могли добраться до модулей JavaScript, во время миграции мы получили бы код, который должен был бы учитывать оба типа модулей. Такое не только технически сложно, но и означает, что все инженеры, работающие над DevTools, должны знать, как работать в такой среде. Они должны были бы постоянно спрашивать себя: «Что происходит в этом коде, это module.json или JS, и как я могу внести изменения?».

Скрытая стоимость миграции в смысле обучения коллег была больше, чем мы ожидали.

После анализа затрат мы пришли к выводу, что все же стоит перейти на модули в языке. Поэтому нашими основными целями были:

  1. Убедиться, что стандартные модули максимально полезны.
  2. Убедиться, что интеграция с существующими модулями на базе module.json безопасна и не приводит к негативному воздействию на пользователя (ошибки регрессии, разочарование пользователя).
  3. Давать руководства по миграции DevTools. В первую очередь с помощью встроенных в процесс сдержек и противовесов, чтобы предотвратить случайные ошибки.


Электронная таблица, преобразования и технический долг


Цель была ясна. Но ограничения module.json оказалось трудно обойти. Потребовалось несколько итераций, прототипов и архитектурных изменений, прежде чем мы разработали удобное решение. Мы закончили тем, что написали проектный документ со стратегией миграции. В этом документе указывалась первоначальная оценка времени: 2–4 недели.

Самая интенсивная часть миграции заняла 4 месяца, а от начала до конца прошло 7 месяцев!

Первоначальный план, однако, выдержал испытание временем: мы хотели научить среду выполнения DevTools загружать все файлы, старым способом использовать перечисленные в массиве scriptsmodule.json, в то время как все файлы, перечисленные в массиве modules должны были загружаться динамическим импортом языка. Любой файл, который будет находиться в массиве modules, может работать с import и export из ES6.

Кроме того, мы хотели мигрировать в 2 фазы. В конечном итоге мы разделили последнюю фазу на 2 подфазы: экспорт и импорт. Модули и фазы отслеживались в большой электронной таблице:

qgli9qbhglkylqfxamc3e9jvkyg.png


Фрагмент таблицы миграции здесь.

Фаза экспорта


Первым этапом было добавление операторов экспорта для всех сущностей, которые должны быть разделены между модулями/файлами. Трансформация автоматизировалась с помощью запуска скрипта для каждой папки. Допустим, в module.json есть такая сущность:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};


Здесь Module — это имя модуля. File1 — имя файла. В дереве кода это выглядит так: front_end/module/file1.JS.

Код выше преобразуется в такой:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};


Первоначально мы планировали переписать импорт в один файл на этом этапе. Например, в приведенном выше примере мы бы переписали Module.File1.localFunctionInFile на localFunctionInFile. Однако мы осознали, что было бы проще автоматизировать и безопаснее разделить эти два преобразования. Таким образом, «перенос всех сущностей в один файл» станет второй подфазой импорта.

Поскольку добавление ключевого слова export превращает файл из «скрипта» в «модуль», большая часть инфраструктуры DevTools должна была быть обновлена соответствующим образом. Инфраструктура включала среду выполнения с динамическим импортом, а также такие инструменты, как ESLint для запуска в режиме модуля.

Одна неприятность заключалась в том, что наши тесты выполнялись в «нестрогом» режиме. Модули JavaScript подразумевают, что файлы работают в строгом режиме. Это влияло на тесты. Как оказалось, нетривиальное количество тестов полагалось на нестрогий режим, включая тест, в котором присутствовал оператор with.

В конце концов, обновление самой первой папки (добавление export) заняло около недели и несколько попыток с перекладываниями.

Фаза импорта


После того как все сущности были экспортированы с помощью export-операторов, оставаясь в глобальной области видимости из-за легаси, нам пришлось обновить все ссылки на сущности, если они в нескольких файлах, чтобы использовать импорт ES. Конечная цель — удаление всех устаревших объектов экспорта очисткой глобальной области видимости. Трансформация автоматизировалась с помощью запуска скрипта для каждой папки.

Например, следующие сущности module.json:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();


Преобразуются в:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();


Однако при таком подходе есть оговорки:

  1. Не каждая сущность была названа по принципу Module.File.symbolName. Некоторые сущности были названы Modele.File или даже Module.CompletelyDifferentName. Несоответствие означало, что мы должны были создать внутреннее сопоставление от старого глобального объекта к новому импортированному объекту.
  2. Иногда возникали конфликты имен moduleScoped. Наиболее заметно, что мы использовали шаблон объявления определенных типов Events, там, где каждая сущность названа просто Events. Это означало, что при прослушивании нескольких типов событий, объявленных в разных файлах, в операторе import у этих Events возникнет коллизия именования.
  3. Как оказалось, между файлами существовали циклические зависимости. Это было прекрасно в контексте глобальной области видимости, так как сущность использовалась после загрузки всего кода. Однако, если вам требуется импорт, циклическая зависимость проявит себя. Такое не приводит к проблеме сразу, если только у вас нет побочных вызовов функций в коде глобальной области видимости, (они были у DevTools). В общем, потребовалось некоторое хирургическое вмешательство и рефакторинг, чтобы обезопасить трансформацию.


Дивный новый мир модулей JavaScript


В феврале 2020 года, через 6 месяцев после старта в сентябре 2019 года, были выполнены последние очистки в папке ui/. Так миграция неофициально закончилась. Когда осела пыль, мы официально отметили миграцию как законченную 5 марта 2020 года.

Теперь DevTools работают только с модулями JavaScript. Мы все еще помещаем некоторые сущности в глобальную область видимости (в файлах легаси module.js) для устаревших тестов или интеграций с другими частями инструментов разработчика архитектуры. Они будут удалены со временем, но мы не рассматриваем их как блокирующие развитие. У нас также есть руководство по стилю работы с модулями JavaScript.

Статистика


Консервативные оценки количества CL (аббревиатура changelist — термин, используемый в Gerrit, аналогичное пул-реквесту GitHub), участвующих в этой миграции, составляют около 250 CL, в основном выполняемых 2 инженерами. У нас нет окончательной статистики о размере внесенных изменений, но консервативная оценка измененных строк (сумма абсолютной разницы между вставками и удалениями для каждого CL) составляет примерно 30 000 строк, то есть около 20% всего интерфейсного кода DevTools.

Первый файл с применением экспорта поддерживается в Chrome 79, выпущенном в стабильном релизе в декабре 2019 года. Последнее изменение для перехода на импорт поставляется в Chrome 83, выпущенном в стабильном релизе в мае 2020 года.

Мы знаем об одной регрессии из-за миграции в стабильном Chrome. Автозавершение фрагментов кода в меню команд сломалось из-за постороннего экспорта по умолчанию. Было и несколько других регрессий, но наши автоматизированные тестовые наборы и пользователи Chrome Canary сообщили о них. Мы исправили ошибки до того, как они могли попасть в стабильные релизы Chrome.

Вы можете увидеть всю историю, зарегистрировавшись здесь. Не все, но большинство CL привязано к этой ошибке.

Чему мы научились?


  1. Принятые в прошлом решения могут оказать долгосрочное влияние на ваш проект. Несмотря на то, что модули JavaScript (и другие форматы модулей) были доступны в течение довольно долгого времени, DevTools не был в состоянии оправдать миграцию. Решение о том, когда мигрировать, а когда нет, трудно и держится на обоснованных догадках.
  2. Первоначальные оценки времени — недели, а не месяцы. В значительной степени это связано с тем, что мы обнаружили больше неожиданных проблем, чем ожидали при первоначальном анализе затрат. Даже при том, что план миграции был основательным, технический долг блокировал работу чаще, чем нам хотелось бы.
  3. Миграция включала большое количество (казалось, не связанных между собой) очисток технического долга. Переход к современному стандартизированному формату модулей позволил нам перестроить лучшие практики кодирования на современную веб-разработку. Например, мы смогли заменить пользовательский упаковщик Python на минимальную конфигурацию Rollup.
  4. Несмотря на большое влияние на нашу кодовую базу (~20% измененного кода), было зарегистрировано очень мало регрессий. Хотя было много проблем с миграцией первых двух файлов, через некоторое время у нас был надежный, частично автоматизированный рабочий процесс. Это означало, что негативное влияние на наших стабильных пользователей было минимальным.
  5. Научить коллег тонкостям конкретной миграции трудно, а иногда и невозможно. Миграции такого масштаба трудно отслеживать и они требуют большого объема знаний в предметной области. Передача этих знаний другим людям, работающим в той же кодовой базе, сама по себе нежелательна для работы, которую они выполняют. Знание того, чем делиться, а чем не делиться — необходимое искусство. Поэтому крайне важно сократить количество крупных миграций или, по крайней мере, не выполнять их одновременно.


image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:
Eще курсы

© Habrahabr.ru