Обновление архитектуры DevTools: переход на модули JavaScript

Тим ван дер Липпе
Tim van der Lippe

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

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

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

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

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

Пример файла module.json :

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

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

Хотя DevTools продолжал работать, было несколько недостатков использования нестандартизированной и уникальной системы модулей:

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

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

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

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

Основное преимущество модулей JavaScript заключается в том, что это стандартизированный формат модулей для JavaScript . Когда мы перечислили недостатки module.json (см. выше), мы поняли, что почти все они связаны с использованием нестандартизированного и уникального формата модуля.

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

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

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

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

Стоимость блестящей новинки

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

На этом этапе речь шла не о том, «Хотим ли мы использовать модули JavaScript?», а о том , «Насколько дорого обходится использование модулей JavaScript?» . Здесь нам пришлось сбалансировать риск поломки наших пользователей с помощью регрессий, затраты инженеров, тратящих (большое количество) времени на миграцию, и временное худшее состояние, в котором мы будем работать.

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

Краткий обзор: скрытые затраты на сопровождение наших коллег-сопровождающих во время миграции оказались больше, чем мы ожидали.

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

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

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

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

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

Однако первоначальный план выдержал испытание временем: мы научим среду выполнения DevTools загружать все файлы, перечисленные в массиве scripts в файле module.json , используя старый способ, в то время как все файлы, перечисленные в массиве modules , будут загружать модули JavaScript. динамический импорт . Любой файл, который будет находиться в массиве modules , сможет использовать импорт/экспорт ES.

Кроме того, мы выполнили миграцию в два этапа (в конечном итоге мы разделили последний этап на два подэтапа, см. ниже): этапы export и import . Статус того, на каком этапе будет находиться модуль, отслеживался в большой электронной таблице:

Таблица миграции модулей JavaScript

Фрагмент прогресс-листа находится в открытом доступе здесь .

фаза export

Первым этапом будет добавление операторов export для всех символов, которые должны были использоваться совместно модулями/файлами. Преобразование будет автоматизировано путем запуска сценария для каждой папки . Учитывая, что в мире 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 . Однако мы поняли, что было бы проще автоматизировать и безопаснее применять, если бы мы разделили эти два преобразования. Таким образом, «перенос всех символов в один файл» станет вторым подэтапом этапа import .

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

При работе над этими проблемами мы сделали одно открытие: наши тесты выполнялись в «небрежном» режиме. Поскольку модули JavaScript предполагают, что файлы выполняются в "use strict" режиме, это также повлияет на наши тесты. Как оказалось, на эту неряшливость опиралось нетривиальное количество тестов , в том числе тест , в котором использовался оператор with 😱.

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

фаза import

После того, как все символы были экспортированы с помощью операторов 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 . Некоторые символы назывались исключительно Module.File или даже Module.CompletelyDifferentName . Это несоответствие означало, что нам пришлось создать внутреннее сопоставление старого глобального объекта с новым импортированным объектом.
  2. Иногда возникали конфликты между именами модулей. Наиболее заметно то, что мы использовали шаблон объявления определенных типов Events , где каждый символ назывался просто Events . Это означало, что если вы прослушивали несколько типов событий, объявленных в разных файлах, в операторе import для этих Events возникал конфликт имен.
  3. Как оказалось, между файлами существовали циклические зависимости. Это было нормально в контексте глобальной области видимости, поскольку символ использовался после загрузки всего кода. Однако если вам требуется import , циклическая зависимость будет явной. Это не проблема сразу, если только у вас нет вызовов функций с побочным эффектом в вашем глобальном коде, который также был в DevTools. В общем, потребовались некоторые операции и рефакторинг, чтобы сделать преобразование безопасным.

Совершенно новый мир с модулями JavaScript

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

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

Статистика

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

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

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

Посмотреть полный путь (не все CL привязаны к этой ошибке, но большинство из них) можно на crbug.com/1006759 .

Что мы узнали

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

Загрузите предварительный просмотр каналов

Рассмотрите возможность использования Chrome Canary , Dev или Beta в качестве браузера для разработки по умолчанию. Эти каналы предварительного просмотра дают вам доступ к новейшим функциям DevTools, тестируют передовые API-интерфейсы веб-платформы и находят проблемы на вашем сайте раньше, чем это сделают ваши пользователи!

Связь с командой Chrome DevTools

Используйте следующие параметры, чтобы обсудить новые функции и изменения в публикации или что-либо еще, связанное с DevTools.