Jak pewnie wiesz, Narzędzia deweloperskie w Chrome to aplikacja internetowa napisana w językach HTML, CSS i JavaScript. Z czasem DevTools stał się bogatszy o nowe funkcje, mądrzejszy i bardziej świadomy szerszej platformy internetowej. Chociaż przez lata Narzędzia deweloperskie się rozrastały, ich architektura w dużej mierze przypomina pierwotną architekturę, gdy były jeszcze częścią WebKit.
Ten post jest częścią serii postów na blogu, w których opisujemy zmiany wprowadzane w architekturze DevTools i sposób jej tworzenia. Wyjaśnimy, jak działały DevTools w przeszłości, jakie były ich zalety i ograniczenia oraz co zrobiliśmy, aby je ograniczyć. Przyjrzyjmy się więc bliżej systemom modułów, sposobom wczytywania kodu i temu, jak w naszym przypadku wykorzystaliśmy moduły JavaScript.
Na początku nie było nic
Chociaż obecna sytuacja na rynku front-endu obejmuje wiele systemów modułowych z dołączonymi narzędziami oraz ustandaryzowany format modułów JavaScriptu, w czasie tworzenia DevTools nie istniało nic z tych rozwiązań. Narzędzie DevTools zostało stworzone na podstawie kodu, który został pierwotnie dołączony do WebKit ponad 12 lat temu.
Pierwsza wzmianka o systemie modułów w DevTools pochodzi z 2012 roku: wprowadzenie listy modułów z powiązaną listą źródeł.
Była to część infrastruktury Pythona używanej wtedy do kompilowania i tworzenia narzędzi deweloperskich.
W 2013 r. wprowadzono zmianę, która wyodrębniła wszystkie moduły do osobnego pliku frontend_modules.json
(commit), a potem w 2014 r. do osobnych plików module.json
(commit).
Przykładowy plik module.json
:
{
"dependencies": [
"common"
],
"scripts": [
"StylePane.js",
"ElementsPanel.js"
]
}
Od 2014 r. w Narzędziach dla programistów do określania modułów i plików źródłowych używany jest wzór module.json
.
Tymczasem ekosystem internetowy szybko się rozwijał i powstało wiele formatów modułów, w tym UMD, CommonJS i ostatecznie standardowe moduły JavaScript.
Narzędzia deweloperskie jednak używają formatu module.json
.
Chociaż Narzędzia deweloperskie nadal działały, korzystanie z niestandardowego i wyjątkowego systemu modułów miało kilka wad:
- Format
module.json
wymagał niestandardowych narzędzi do kompilacji, podobnych do nowoczesnych narzędzi do tworzenia pakietów. - Nie było integracji z IDE, co wymagało użycia niestandardowych narzędzi do generowania plików zrozumiałych dla nowoczesnych IDE (oryginalny skrypt do generowania plików jsconfig.json dla VS Code).
- Funkcje, klasy i obiekty zostały umieszczone w zakresie globalnym, aby umożliwić udostępnianie między modułami.
- Pliki były zależne od kolejności, co oznacza, że istotna była kolejność, w jakiej były wymienione
sources
. Nie było żadnej gwarancji, że kod, na którym polegasz, zostanie załadowany, poza tym, że został zweryfikowany przez człowieka.
Podsumowując, po ocenie obecnego stanu systemu modułów w DevTools i innych (bardziej popularnych) formatach modułów doszliśmy do wniosku, że wzór module.json
stwarza więcej problemów niż je rozwiązuje, i że nadszedł czas, by od niego odejść.
Korzyści ze standardów
Spośród istniejących systemów modułowych wybraliśmy moduły JavaScript. W momencie podjęcia tej decyzji moduły JavaScript były nadal dostępne w ramach flagi w Node.js, a wiele pakietów dostępnych w NPM nie zawierało pakietu modułów JavaScript, którego moglibyśmy użyć. Mimo to doszliśmy do wniosku, że moduły JavaScript są najlepszą opcją.
Główną zaletą modułów JavaScriptu jest to, że są one standardowym formatem modułów JavaScriptu.
Gdy wymieniliśmy wady module.json
(patrz wyżej), zdaliśmy sobie sprawę, że prawie wszystkie z nich są związane z użyciem niestandardowego i niepowtarzalnego formatu modułu.
Wybór niestandardowego formatu modułu oznacza, że musimy poświęcić czas na tworzenie integracji z narzędziami kompilacji i narzędziami używanymi przez naszych opiekunów.
Te integracje były często niestabilne i nie obsługiwały niektórych funkcji, wymagały dodatkowego czasu na konserwację i czasami powodowały drobne błędy, które ostatecznie trafiały do użytkowników.
Ponieważ moduły JavaScript były standardem, oznaczało to, że IDE, takie jak VS Code, sprawdzacze typów, takie jak Closure Compiler/TypeScript, oraz narzędzia do kompilacji, takie jak Rollup/minifiers, mogły zrozumieć napisany przez nas kod źródłowy.
Ponadto, gdy nowy opiekun dołączy do zespołu DevTools, nie będzie musiał poświęcać czasu na zapoznanie się z formatem module.json
, ponieważ (prawdopodobnie) zna już moduły JavaScript.
Oczywiście, gdy narzędzia deweloperskie były tworzone po raz pierwszy, nie było żadnych z tych zalet. Dojście do obecnego stanu zajęło wiele lat pracy w grupach opracowujących standardy, implementacjach w czasie wykonywania i używających modułów JavaScriptu deweloperów, którzy przekazywali opinie. Gdy jednak stały się dostępne moduły JavaScriptu, musieliśmy dokonać wyboru: kontynuować utrzymywanie własnego formatu lub zainwestować w migrację na nowy.
Koszt nowego
Mimo że moduły JavaScriptu miały wiele zalet, z których chcielibyśmy skorzystać, pozostaliśmy w niestandardowym środowisku module.json
.
Aby czerpać korzyści z modułów JavaScript, musieliśmy znacząco zainwestować w uporządkowanie zaległości technicznych, przeprowadzić migrację, która mogłaby potencjalnie spowodować przerwanie działania funkcji i wprowadzić błędy regresji.
W tym momencie nie chodziło o to, czy chcemy używać modułów JavaScript, ale o to, jak drogie jest używanie modułów JavaScript. W tym przypadku musieliśmy znaleźć złoty środek między ryzykiem, że regresje spowodują problemy u użytkowników, kosztami, jakie poniosą inżynierowie, którzy będą musieli poświęcić dużo czasu na migrację, oraz tym, że tymczasowo pogorszy się stan aplikacji.
Ten ostatni punkt okazał się bardzo ważny. Chociaż teoretycznie można by użyć modułów JavaScript, podczas migracji powstałby kod, który musiałby uwzględniać zarówno moduły module.json
, jak i moduły JavaScript.
Nie tylko było to trudne pod względem technicznym, ale oznaczało też, że wszyscy inżynierowie pracujący nad DevTools musieli wiedzieć, jak pracować w tym środowisku.
Programista musi stale zadawać sobie pytanie: „Czy ta część kodu to module.json
czy moduły JavaScriptu i jak wprowadzić zmiany?”.
Wstępny przegląd: ukryte koszty przeprowadzenia innych programistów przez proces migracji były większe niż się spodziewaliśmy.
Po analizie kosztów doszliśmy do wniosku, że warto przejść na moduły JavaScript. Dlatego naszymi głównymi celami były:
- Upewnij się, że korzystanie z modułów JavaScriptu przynosi jak największe korzyści.
- Upewnij się, że integracja z dotychczasowym systemem opartym na
module.json
jest bezpieczna i nie wpływa negatywnie na użytkowników (błędy w regressji, frustracja użytkowników). - Poprowadzić wszystkich administratorów Narzędzi deweloperskich przez proces migracji, przede wszystkim za pomocą wbudowanych mechanizmów kontroli i równowagi, które zapobiegają przypadkowym błędom.
Arkusze kalkulacyjne, przekształcenia i dług technologiczny
Chociaż cel był jasny, ograniczenia narzucone przez format module.json
okazały się trudne do obejścia.
Zanim udało nam się opracować odpowiednie rozwiązanie, musieliśmy przejść przez kilka iteracji, stworzyć prototypy i wprowadzić zmiany w architekturze.
Ostatecznie opracowaliśmy dokument projektowy ze strategią migracji.
Dokument z projektem zawierał też nasz wstępny szacowany czas: 2–4 tygodnie.
Spoiler: najbardziej intensywna część migracji zajęła 4 miesiące, a całość – 7 miesięcy.
Pierwotny plan przetrwał jednak próbę czasu: wczytywanie wszystkich plików wymienionych w tablicy scripts
w pliku module.json
odbywało się w sposób tradycyjny, a wszystkie pliki wymienione w tablicy modules
– za pomocą dynamicznego importu modułów JavaScript.
Każdy plik, który znajduje się w tablicy modules
, może korzystać z importu/eksportu ES.
Dodatkowo przeprowadzimy migrację w 2 etapach (ostatni etap podzieliliśmy na 2 podetapy, patrz poniżej): etap export
i etap import
.
W dużym arkuszu kalkulacyjnym śledzono stan modułu na poszczególnych etapach:
Fragment arkusza postępów jest publicznie dostępny tutaj.
export
-fazowy
Pierwszym etapem byłoby dodanie instrukcji export
do wszystkich symboli, które miały być wspólne dla modułów/plików.
Przekształcenie zostanie zautomatyzowane przez uruchomienie skryptu w poszczególnych folderach.
W świecie module.json
istnieje ten symbol:
Module.File1.exported = function() {
console.log('exported');
Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
console.log('Local');
};
(Tutaj Module
to nazwa modułu, a File1
to nazwa pliku. W naszym sourcetree jest to front_end/module/file1.js
.
Zostanie on przekształcony w ten 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 też na tym etapie przerobić importowanie plików o tej samej nazwie.
Na przykład w przykładzie powyżej zastąpilibyśmy Module.File1.localFunctionInFile
wartością localFunctionInFile
.
Zdaliśmy sobie jednak sprawę, że rozdzielenie tych 2 przekształceń ułatwi ich automatyzację i zwiększy bezpieczeństwo ich stosowania.
Dlatego „migracja wszystkich symboli w tym samym pliku” stanie się drugą fazą podrzędną etapu import
.
Dodanie do pliku słowa kluczowego export
powoduje, że plik przekształca się z „skryptu” w „moduł”, dlatego należało odpowiednio zaktualizować wiele elementów infrastruktury DevTools.
Obejmuje to środowisko uruchomieniowe (z importem dynamicznym), ale też narzędzia takie jak ESLint
, które działają w trybie modułowym.
Podczas rozwiązywania tych problemów odkryliśmy, że nasze testy były wykonywane w trybie „niedbały”.
Ponieważ moduły JavaScriptu wskazują, że pliki są uruchamiane w trybie "use strict"
, wpłynie to również na nasze testy.
Okazało się, że spora liczba testów korzystała z tej nieostrożności, w tym test, który używał instrukcji with
😱.
W końcu zaktualizowanie pierwszego foldera tak, aby zawierał instrukcje export
, zajęło około tygodnia i wiele prób z relands.
import
-fazowy
Po wyeksportowaniu wszystkich symboli za pomocą instrukcji export
i pozostawieniu ich w zakresie globalnym (starego typu) musieliśmy zaktualizować wszystkie odwołania do symboli w wielu plikach, aby można było używać importów ES.
Ostatecznym celem jest usunięcie wszystkich „starszych obiektów eksportu”, aby posprzątać w zakresie globalnym.
Przekształcenie zostanie zautomatyzowane przez uruchomienie skryptu w poszczególnych folderach.
Na przykład w przypadku tych symboli występujących w świecie module.json
:
Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();
Zostaną one przekształcone w:
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 miało jednak pewne ograniczenia:
- Nie wszystkie symbole miały nazwę
Module.File.symbolName
. Niektóre symbole miały nazwę tylkoModule.File
lub nawetModule.CompletelyDifferentName
. Ta niespójność oznaczała, że musieliśmy utworzyć wewnętrzne mapowanie starego obiektu globalnego na nowy zaimportowany obiekt. - Czasami dochodzi do kolizji nazw na poziomie modułu.
Najważniejsze jest to, że zastosowaliśmy wzór deklarowania określonych typów
Events
, w którym każdy symbol miał nazwęEvents
. Oznacza to, że jeśli nasłuchujesz wielu typów zdarzeń zadeklarowanych w różnych plikach, w przypadku tych zdarzeń w oświadczeniuimport
wystąpi konflikt nazw.Events
- Okazało się, że między plikami występowały zależności cykliczne.
W kontekście globalnym było to w porządku, ponieważ symbol został użyty po załadowaniu całego kodu.
Jeśli jednak potrzebujesz
import
, zależność cykliczna zostanie wyraźnie zaznaczona. Nie stanowi to problemu, chyba że w Twoim kodzie o zasięgu globalnym występują wywołania funkcji, które wywołują skutki uboczne, co miało miejsce w przypadku DevTools. Ogólnie rzecz biorąc, aby bezpiecznie przeprowadzić transformację, musieliśmy wykonać pewne zmiany i refaktoryzację.
Nowy świat z modułami JavaScript
W lutym 2020 r., czyli 6 miesięcy po rozpoczęciu procesu w wrześniu 2019 r., w folderze ui/
zostały przeprowadzone ostatnie działania porządkujące.
To oznaczało nieoficjalne zakończenie migracji.
Gdy emocje opadły, 5 marca 2020 r. oficjalnie zakończyliśmy migrację. 🎉
Obecnie wszystkie moduły w DevTools używają modułów JavaScript do udostępniania kodu.
Niektóre symbole nadal umieszczamy w zakresie globalnym (w plikach module-legacy.js
) na potrzeby starszych testów lub integracji z innymi częściami architektury DevTools.
Z czasem zostaną one usunięte, ale nie uważamy, aby uniemożliwiały dalszy rozwój.
Mamy też poradnik dotyczący stosowania modułów JavaScriptu.
Statystyki
Konserwatywne szacunki dotyczące liczby list zmian (skrót od changelist – termin używany w Gerrecie, który reprezentuje zmianę – podobny do żądania pull request w GitHub) zaangażowanych w tej migracji wynosi około 250 list zmian, w większości wykonanych przez 2 inżynierów. Nie mamy dokładnych statystyk dotyczących rozmiaru wprowadzonych zmian, ale ostrożna ocena liczby zmienionych linii kodu (obliczona jako suma bezwzględna różnicy między wstawkami a usunięciami w każdym CL) wynosi około 30 tys. wierszy kodu (~20% całego kodu interfejsu DevTools).
Pierwszy plik korzystający z export
został dostarczony w Chrome 79, który został wydany w stabilnej wersji w grudniu 2019 r.
Ostatnia zmiana, która umożliwia migrację do import
, została wprowadzona w Chrome 83, która została udostępniona w wersji stabilnej w maju 2020 r.
Wiemy o jednym regresie, który został wprowadzony w Chrome stabilnej w ramach tej migracji.
Autouzupełnianie fragmentów w menu poleceń przestało działać z powodu niepotrzebnego eksportu default
.
Wystąpiło kilka innych regresji, ale zostały zgłoszone przez automatyczne zestawy testów i użytkowników Chrome Canary. W rezultacie udało nam się naprawić je, zanim dotarły do użytkowników stabilnej wersji Chrome.
Pełną ścieżkę (nie wszystkie CL są powiązane z tym błędem, ale większość z nich) znajdziesz w crbug.com/1006759.
Czego się nauczyliśmy?
- Decyzje podjęte w przeszłości mogą mieć długotrwały wpływ na projekt. Mimo że moduły JavaScript (i inne formaty modułów) były dostępne od dłuższego czasu, w przypadku Narzędzi deweloperskich nie było to uzasadnione. Podjęcie decyzji, kiedy przeprowadzić migrację, a kiedy nie, jest trudne i oparte na świadomych przypuszczeniach.
- Nasz początkowy szacowany czas był podany w tygodniach, a nie miesiącach. Wynika to głównie z tego, że znaleźliśmy więcej nieoczekiwanych problemów niż przewidywaliśmy w ramach wstępnej analizy kosztów. Mimo że plan migracji był solidny, to problemy techniczne (częściej niż nam się podobało) blokowały postępy.
- Migracja modułów JavaScriptu obejmowała dużą liczbę (pozornie niezwiązanych) długów technicznych. Migracja do nowoczesnego, ujednoliconego formatu modułów pozwoliła nam dostosować nasze sprawdzone metody kodowania do współczesnego tworzenia stron internetowych. Udało nam się na przykład zastąpić niestandardowy pakiet Pythona minimalną konfiguracją Rollup.
- Pomimo dużego wpływu na nasz kod źródłowy (zmieniliśmy ok. 20% kodu) odnotowaliśmy bardzo niewiele regresje. Chociaż mieliśmy wiele problemów z przeniesieniem pierwszych kilku plików, po pewnym czasie udało nam się stworzyć solidny, częściowo zautomatyzowany proces. Oznacza to, że w przypadku tej migracji wpływ na użytkowników korzystających z wersji stabilnej był minimalny.
- Wyjaśnienie zawiłości konkretnej migracji innym administratorom jest trudne, a czasami niemożliwe. Migracje na taką skalę są trudne do prześledzenia i wymagają dużej wiedzy w danej dziedzinie. Przekazywanie tej wiedzy innym osobom pracującym w ramach tej samej bazy kodu nie jest w ogóle pożądane. Wiedzieć, co udostępniać, a czego nie, to sztuka, ale konieczna. Dlatego ważne jest, aby ograniczyć liczbę dużych migracji lub przynajmniej nie przeprowadzać ich w tym samym czasie.
Pobieranie kanałów podglądu
Rozważ użycie jako domyślnej przeglądarki deweloperskiej wersji Canary, Dev lub Beta przeglądarki Chrome. Te kanały wersji wstępnej zapewniają dostęp do najnowszych funkcji DevTools, umożliwiają testowanie najnowocześniejszych interfejsów API platformy internetowej i pomagają znaleźć problemy w witrynie, zanim zrobią to użytkownicy.
Kontakt z zespołem Narzędzi deweloperskich w Chrome
Aby omówić nowe funkcje, aktualizacje lub inne kwestie związane z Narzędziami deweloperskimi, skorzystaj z tych opcji.
- Przesyłaj opinie i prośby o dodanie funkcji na stronie crbug.com.
- Zgłoś problem z Narzędziami deweloperskimi, klikając Więcej opcji > Pomoc > Zgłoś problem z Narzędziami deweloperskimi w Narzędziach deweloperskich.
- Wyślij tweeta do @ChromeDevTools.
- Dodaj komentarze do filmów w YouTube z serii „Co nowego w Narzędziach deweloperskich” lub Wskazówki dotyczące Narzędzi deweloperskich.