Odświeżona architektura Narzędzi deweloperskich: migracja do modułów JavaScript

Tim van der Lippe
Tim van der Lippe

Narzędzia deweloperskie w Chrome to aplikacja internetowa napisana w językach HTML, CSS i JavaScript. Z biegiem lat Narzędzia deweloperskie stały się bardziej rozbudowane i bogate w funkcje oraz oferują większą wiedzę o szerszej platformie internetowej. Choć z czasem narzędzia deweloperskie się rozrastały, ich architektura w dużej mierze przypomina pierwotną architekturę, która była jeszcze częścią WebKit.

Ten post jest częścią serii postów na blogu, w których opisano zmiany, jakie wprowadzamy w architekturze Narzędzi deweloperskich, oraz sposób ich tworzenia. Wyjaśnimy, jak sprawdzały się Narzędzia deweloperskie w przeszłości, jakie były korzyści i ograniczenia oraz co zrobiliśmy, aby je złagodzić. Dlatego teraz przyjrzyjmy się systemom modułów, wczytywaniu kodu i sposobie, w jaki korzystaliśmy z modułów JavaScript.

Na początku nie było

Chociaż obecny interfejs frontendu obejmuje różne systemy modułów z opartymi na nich narzędziami, a także ustandaryzowany format modułów JavaScript, żaden z nich nie istniał podczas tworzenia Narzędzi deweloperskich. Narzędzia deweloperskie są oparte na kodzie, który został początkowo udostępniony w WebKit ponad 12 lat temu.

Pierwsza wzmianka o systemie modułów w Narzędziach deweloperskich pochodzi z 2012 roku: przedstawiliśmy listę modułów z powiązaną listą źródeł. Była to część infrastruktury Pythona, której użyto w tamtych czasach do kompilowania i tworzenia narzędzi deweloperskich. W 2013 roku wyodrębniliśmy wszystkie moduły do osobnego pliku frontend_modules.json (zobowiązania), a w 2014 r. do osobnych plików module.json (zobowiązanie).

Przykładowy plik module.json:

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

Od 2014 roku wzorzec module.json jest używany w Narzędziach deweloperskich do określania jego modułów i plików źródłowych. W tym czasie szybko ewoluował ekosystem internetowy i powstały w nim różne formaty modułów, w tym UMD, CommonJS oraz ostatecznie ustandaryzowane moduły JavaScript. Jednak w Narzędziach deweloperskich utknął format module.json.

Choć korzystanie z Narzędzi deweloperskich miało kilka wad, to zastosowanie niestandardowego i unikalnego systemu modułów:

  1. Format module.json wymagał niestandardowych narzędzi do kompilacji, podobnych do nowoczesnych narzędzi do tworzenia pakietów.
  2. Nie było integracji z IDE, co wymagało użycia niestandardowych narzędzi do generowania plików, które byłyby zrozumiałe dla nowoczesnych IDE (pierwotny skrypt do generowania plików jsconfig.json w przypadku VS Code).
  3. Funkcje, klasy i obiekty zostały umieszczone w zakresie globalnym, aby umożliwić udostępnianie danych między modułami.
  4. Pliki były zależne od kolejności, co oznacza, że kolejność wymienionych elementów (sources) była ważna. Nie było żadnej gwarancji, że kod, na którym polegasz, zostanie wczytany, poza tym, że został zweryfikowany przez człowieka.

Ogólnie rzecz biorąc, oceniając bieżący stan systemu modułów w Narzędziach deweloperskich i innych (bardziej powszechnych) formatach modułów, doszliśmy do wniosku, że wzorzec module.json powoduje więcej problemów niż nie został rozwiązany, i przyszedł czas, aby zaplanować wycofanie się z niego.

Zalety standardów

Z istniejących systemów modułów wybraliśmy moduły JavaScript jako rozwiązanie, do którego przeprowadziliśmy migrację. W momencie tej decyzji moduły JavaScript nadal były objęte flagą w środowisku Node.js, a duża liczba pakietów dostępnych w NPM nie miała pakietu modułów JavaScript, których moglibyśmy użyć. Mimo to doszliśmy do wniosku, że najlepszym rozwiązaniem są moduły JavaScript.

Główną zaletą modułów JavaScript jest to, że jest to standaryzowany format modułów dla JavaScript. Gdy wymieniliśmy wady funkcji module.json (patrz wyżej), zdaliśmy sobie sprawę, że prawie wszystkie z nich były związane z używaniem niestandardowego i niepowtarzalnego formatu modułu.

Wybór niestandardowego formatu modułów oznacza, że musimy poświęcić czas na tworzenie integracji za pomocą narzędzi do tworzenia i narzędzi używanych przez naszych opiekunów.

Te integracje często były mało skuteczne i brakowało obsługi funkcji, co wymagało dodatkowego czasu na konserwację, co czasem wiązało się z drobnymi błędami, które w końcu trafiały do użytkowników.

Ponieważ moduły JavaScript były standardem, IDE takie jak VS Code, narzędzia do sprawdzania typów (Closure Compiler/TypeScript) i narzędzia do tworzenia, takie jak Rollup/minifiery, mogły zrozumieć napisany przez nas kod źródłowy. Poza tym osoba, która dołącza do zespołu Narzędzi deweloperskich, nie musi poświęcać czasu na naukę zastrzeżonego formatu module.json, podczas gdy (prawdopodobnie) zna już moduły JavaScript.

Oczywiście na początku tworzenia Narzędzi deweloperskich nie było żadnych z powyższych korzyści. Do osiągnięcia obecnej sytuacji w grupach standardowych, implementacjach środowisk wykonawczych i programistach zajęło się wiele lat pracy w grupach standardowych, a także programistów korzystających z modułów JavaScript przekazujących opinie. Jednak po udostępnieniu modułów JavaScript musieliśmy dokonać wyboru: zachować swój własny format lub zainwestować w migrację do nowego.

Koszt nowych

Chociaż moduły JavaScript miały wiele zalet, które chcieliśmy wykorzystać, pozostaniemy w niestandardowym świecie module.json. Wiedząc o zaletach modułów JavaScript, musieliśmy znacznie zainwestować w usunięcie długu technicznego, przeprowadzenie migracji, która mogłaby potencjalnie zakłócić działanie funkcji i wprowadzić błędy związane z regresją.

Tym razem nie było pytanie „Czy chcemy korzystać z modułów JavaScript?”, ale „Ile kosztuje możliwość korzystania z modułów JavaScript?”. W tym przypadku musieliśmy pogodzić ryzyko złamania zabezpieczeń naszych użytkowników przez regresje, koszty (dużej ilości) czasu poświęcanego przez inżynierów na migrację i tymczasowo gorszą sytuację, w której musielibyśmy pracować.

Ostatnia kwestia okazała się bardzo ważna. Mimo że w teorii moglibyśmy uzyskać dostęp do modułów JavaScript, podczas migracji musielibyśmy użyć kodu, który będzie brał pod uwagę zarówno moduły module.json, jak i JavaScript. Było to trudne nie tylko technicznie, ale oznaczało również, że wszyscy inżynierowie pracujący nad Narzędziami deweloperskimi musieli wiedzieć, jak pracować w takim środowisku. Muszą oni nieustannie zadawać sobie pytanie: „Czy w tej części bazy kodu są moduły module.json, JavaScript i jak wprowadzić zmiany?”.

Zapowiedź: ukryty koszt utrzymania innych opiekunów podczas migracji był większy, niż się spodziewaliśmy.

Po przeanalizowaniu kosztów doszliśmy do wniosku, że warto przejść na moduły JavaScript. W związku z tym naszym głównym celem było:

  1. Dopilnuj, aby użycie modułów JavaScript przynosiło jak najwięcej korzyści.
  2. Upewnij się, że integracja z dotychczasowym systemem opartym na module.json jest bezpieczna i nie powoduje negatywnego wpływu na użytkowników (błędy związane z regresją, niezadowolenie użytkowników).
  3. Poprowadź wszystkich użytkowników Narzędzi deweloperskich przez proces migracji, korzystając głównie z wbudowanych mechanizmów sprawdzania i równoważenia zasobów, które zapobiegają przypadkowym pomyłkom.

Arkusze kalkulacyjne, przekształcenia i dług technologiczny

Choć cel był jasny, ograniczenia nałożone przez format module.json okazały się trudne do obejścia. Przed opracowaniem dogodnego dla nas rozwiązania wymagało kilku iteracji, prototypów i zmian architektonicznych. Przygotowaliśmy dokument projektowy z opracowaną przez nas strategią migracji. W dokumencie projektowym podano również początkowy szacowany czas: 2–4 tygodnie.

Uwaga spojler: najbardziej intensywna część migracji trwała 4 miesiące, a od początku do końca trwała 7 miesięcy.

Początkowy plan przetrwał jednak próbę czasu: nauczyliśmy środowisko wykonawcze Narzędzi deweloperskich w stary sposób wczytywać wszystkie pliki wymienione w tablicy scripts w pliku module.json, podczas gdy wszystkie pliki znajdujące się w tablicy modules za pomocą importu dynamicznego modułów JavaScript. Każdy plik, który znajduje się w tablicy modules, będzie mógł korzystać z importów/eksportów ES.

Ponadto migracja przeprowadziliśmy w 2 fazach (ostatecznie podzieliliśmy ostatnią fazę na 2 podepy): export i import. Stan modułu, w którym znajduje się dana faza śledzenia w dużym arkuszu kalkulacyjnym:

Arkusz migracji modułów JavaScript

Fragment arkusza postępu jest publicznie dostępny tutaj.

export-faza

Pierwsza faza polega na dodaniu instrukcji export dla wszystkich symboli, które powinny być wspólne między modułami/plikami. Przekształcenie zostanie ukończone automatycznie przez uruchomienie skryptu na folder. Biorąc pod uwagę, że ten symbol występuje w świecie module.json:

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

(w tym miejscu Module to nazwa modułu, a File1 – nazwa pliku. W naszym drzewie źródłowych będzie to front_end/module/file1.js).

Przekształciłoby się one w taki sposób:

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

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

Początkowo planowaliśmy zmodyfikować importy tych samych plików również na tym etapie. W powyższym przykładzie należałoby na przykład przepisać funkcję Module.File1.localFunctionInFile na język localFunctionInFile. Zdaliśmy sobie jednak sprawę, że łatwiej byłoby automatyzację i bezpieczniej zastosować ją po rozdzieleniu tych 2 przekształceń. W związku z tym „Przenieś wszystkie symbole w tym samym pliku” stanie się drugą podetapem etapu import.

Ponieważ dodanie słowa kluczowego export w pliku przekształca plik ze „skryptu” w „moduł”, wiele elementów infrastruktury DevTools wymaga odpowiednich aktualizacji. Obejmowało to środowisko wykonawcze (z dynamicznym importem) i narzędzia takie jak ESLint, które działały w trybie modułu.

Podczas rozwiązywania tych problemów zauważyliśmy, że nasze testy działały w trybie niechlujnym. Ponieważ moduły JavaScript sugerują, że pliki działają w trybie "use strict", wpłynie to również na nasze testy. Jak się okazało, na tym luźnym poziomie założono niezbędną liczbę testów, w tym test z wykorzystaniem wyrażenia with 😱

Ostatecznie zaktualizowanie pierwszego folderu tak, aby zawierał instrukcje export, trwało około tygodnia i kilka prób z relands.

import-faza

Ponieważ wszystkie symbole zostały wyeksportowane za pomocą instrukcji export i pozostały w zakresie globalnym (starsza wersja), musieliśmy zaktualizować wszystkie odniesienia do symboli w innych plikach, aby korzystać z importów ES. Końcowym celem będzie usunięcie wszystkich „starszych obiektów eksportu” w celu oczyszczenie zakresu globalnego. Przekształcenie zostanie ukończone automatycznie przez uruchomienie skryptu na folder.

Na przykład w przypadku tych symboli, które występują w świecie module.json:

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

Te wartości zostałyby przekształcone w taki sposób:

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();

Takie podejście wiąże się jednak z pewnymi zastrzeżeniami:

  1. Nie każdy symbol został nazwany Module.File.symbolName. Niektóre symbole zostały nazwane wyłącznie Module.File lub nawet Module.CompletelyDifferentName. Ta niezgodność oznaczała, że musieliśmy utworzyć wewnętrzne mapowanie ze starego obiektu globalnego na nowy zaimportowany obiekt.
  2. Czasami mogą występować konflikty między nazwami moduleScoped. Przede wszystkim użyliśmy wzorca deklarowania określonych typów parametru Events, w którym każdy symbol nazywano tylko Events. Oznaczało to, że jeśli nasłuchiwano wielu typów zdarzeń zadeklarowanych w różnych plikach, w instrukcji import--" w przypadku tych Events wystąpi konflikt nazw.
  3. Jak się okazało, między plikami zachodziły zależności cykliczne. Nie było to problem w kontekście zakresu globalnego, ponieważ użycie symbolu miało miejsce po wczytaniu całego kodu. Jeśli jednak potrzebujesz elementu import, zależność cykliczna będzie jawna. Nie stanowi to od razu problemu, chyba że w globalnym kodzie zakresu masz zawarte wywołania funkcji efektu ubocznego, które również były dostępne w Narzędziach deweloperskich. Przede wszystkim transformacja ta wymagała wykonania pewnych operacji i refaktoryzacji.

Zupełnie nowy świat z modułami JavaScript

W lutym 2020 roku, 6 miesięcy po rozpoczęciu września 2019 roku, w folderze ui/ przeprowadzono ostatnie sprzątanie. Oznaczało to nieoficjalne zakończenie migracji. Gdy kurz się ustabilizował, oficjalnie oznaczyliśmy migrację jako zakończoną 5 marca 2020 roku. 🎉

Teraz wszystkie moduły w Narzędziach deweloperskich korzystają do udostępniania kodu za pomocą modułów JavaScript. Niektóre symbole nadal znajdują się w zakresie globalnym (w plikach module-legacy.js) na potrzeby starszych testów lub do integracji z innymi częściami architektury Narzędzi deweloperskich. Z czasem będą one usuwane, ale nie traktujemy ich jako blokady w przyszłości. Przygotowaliśmy też przewodnik stylistyczny dotyczący korzystania z modułów JavaScript.

Statystyki

Konserwatywne szacunki liczby list zmian (skrót od listy zmian – termin używany w Gerrit, który reprezentuje zmianę – podobnie jak w przypadku żądania pull z GitHuba) biorących udział w tej migracji to około 250 CL, które są w większości wykonywane przez 2 inżynierów. Nie dysponujemy ostatecznymi statystykami dotyczącymi wielkości wprowadzonych zmian, ale zachowawcze oszacowanie liczby zmienionych wierszy (obliczone jako suma bezwzględnej różnicy między wstawieniami i usunięciami dla każdej listy zmian) wynosi około 30 000 (około 20% całego kodu frontendu w Narzędziach deweloperskich).

Pierwszy plik korzystający z export został udostępniony w Chrome 79, który został udostępniony w wersji stabilnej w grudniu 2019 r. Ostatnia zmiana służąca do migracji do import została udostępniona w Chrome 83, która została opublikowana w maju 2020 roku.

Wiemy o jednej regresji, która została wysłana do stabilnej wersji Chrome i wprowadzona w ramach tej migracji. Autouzupełnianie fragmentów w menu poleceń nie działa z powodu nadmiernego eksportu default. Mieliśmy kilka innych regresji, ale nasze zautomatyzowane pakiety testowe i użytkownicy Chrome Canary zgłaszali je i usunęliśmy je, zanim umożliwiliśmy im dostęp do wersji stabilnej Chrome.

Cały przebieg procesu (nie wszystkie listy zmian są dołączone do tego błędu, ale większość z nich jest) rejestrowana na stronie crbug.com/1006759.

Czego się nauczyliśmy?

  1. Decyzje podjęte z przeszłości mogą mieć długotrwały wpływ na Twój projekt. Chociaż moduły JavaScript (i inne formaty modułów) były dostępne od jakiegoś czasu, DevTools nie było w stanie uzasadnić migracji. Decyzja o tym, kiedy i kiedy przeprowadzić migrację, jest trudna i zależy od przewidywań.
  2. Początkowe szacunki były podawane w tygodniach, a nie w miesiącach. Wynika to głównie z faktu, że znaleźliśmy więcej nieoczekiwanych problemów, niż oczekiwaliśmy podczas wstępnej analizy kosztów. Mimo że plan migracji był solidny, dług technologiczny był (częściej, niż by tego chciał) przeszkodą.
  3. Migracja modułów JavaScript obejmowała dużą liczbę (pozornie niepowiązanych) długów technicznych. Przejście na nowoczesny, ustandaryzowany format modułów pozwoliło nam dostosować nasze sprawdzone metody kodowania do współczesnego programowania stron internetowych. Mogliśmy na przykład zastąpić nasz niestandardowy pakiet SDK Pythona minimalną konfiguracją o pełnym zakresie.
  4. Pomimo duży wpływ na naszą bazę kodu (ok. 20% zmiany kodu), odnotowaliśmy bardzo niewiele błędów. Mieliśmy wiele problemów przy przenoszeniu pierwszych kilku plików, ale po jakimś czasie mieliśmy doskonały, częściowo zautomatyzowany przepływ pracy. Oznaczało to, że ta migracja miała minimalny negatywny wpływ na użytkowników wersji stabilnej.
  5. Uczenie szczegółów konkretnej migracji z innymi opiekunami jest trudne, a czasem nawet niemożliwe. Migracje na tak dużą skalę są trudne do śledzenia i wymagają dużej wiedzy. Przekazanie wiedzy z tej dziedziny innym osobom pracującym w tej samej bazie kodu nie jest wskazane w ich przypadku. Wiedza o tym, co udostępniać, a czego nie, to sztuka, ale niezbędna. Dlatego tak ważne jest ograniczenie liczby dużych migracji, a przynajmniej nieprzeprowadzanie ich w tym samym czasie.

Pobieranie kanałów podglądu

Jako domyślnej przeglądarki dla programistów możesz używać Chrome Canary, Dev lub Beta. Te kanały podglądu dają dostęp do najnowszych funkcji Narzędzi deweloperskich, umożliwiają testowanie najnowocześniejszych interfejsów API platform internetowych oraz wykrywanie problemów w witrynie, zanim zdołają zrobić użytkownicy.

Kontakt z zespołem Narzędzi deweloperskich w Chrome

Użyj poniższych opcji, aby omówić nowe funkcje i zmiany w poście lub wszelkie inne kwestie związane z Narzędziami dla deweloperów.

  • Prześlij nam sugestię lub opinię na stronie crbug.com.
  • Aby zgłosić problem z Narzędziami deweloperskimi, kliknij Więcej opcji   Więcej   > Pomoc > Zgłoś problemy z Narzędziami deweloperskimi.
  • zatweetuj na @ChromeDevTools.
  • Napisz komentarz o nowościach w filmach w YouTube dostępnych w Narzędziach deweloperskich lub z poradami dotyczącymi narzędzi dla deweloperów w filmach w YouTube.