רענון הארכיטקטורה של כלי הפיתוח: מעבר למודולים של JavaScript

Tim van der Lippe
Tim van der Lippe

כידוע, כלי הפיתוח ל-Chrome הם אפליקציית אינטרנט שנכתבה באמצעות HTML, ‏ CSS ו-JavaScript. לאורך השנים, כלי הפיתוח הפכו לעשירים יותר בתכונות, חכמים יותר ומקושרים יותר לפלטפורמת האינטרנט הרחבה. DevTools התרחב עם השנים, אבל הארכיטקטורה שלו דומה במידה רבה לארכיטקטורה המקורית, כשהכלי עדיין היה חלק מ-WebKit.

הפוסט הזה הוא חלק מסדרת פוסטים בבלוג שמתארים את השינויים שאנחנו מבצעים בארכיטקטורה של DevTools ואת אופן הבנייה שלו. נסביר איך DevTools פעל בעבר, מה היו היתרונות והמגבלות שלו ומה עשינו כדי לצמצם את המגבלות האלה. לכן, נתעמק בנושא מערכות מודולים, נלמד איך לטעון קוד ונבין איך הגענו לשימוש במודולים של JavaScript.

בהתחלה לא היה כלום

בממשק הקצה הנוכחי יש מגוון מערכות של מודולים עם כלים שנבנו סביבם, וגם את הפורמט הסטנדרטי של מודולי JavaScript, אבל אף אחד מהם לא היה קיים כש-DevTools נוצר לראשונה. כלי הפיתוח מבוססים על קוד ששוחרר לראשונה ב-WebKit לפני יותר מ-12 שנים.

האזכור הראשון של מערכת מודולים בכלי הפיתוח נובע משנת 2012: הקדמה לרשימת מודולים עם רשימת מקורות משויכת. זה היה חלק מתשתית Python ששימשה אז כדי לקמפל ולבנות את DevTools. שינוי המשך חילוץ את כל המודולים לקובץ frontend_modules.json נפרד (commit) בשנת 2013, ולאחר מכן לקובצי module.json נפרדים (commit) בשנת 2014.

קובץ module.json לדוגמה:

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

מאז 2014, נעשה שימוש בדפוס module.json בכלי הפיתוח כדי לציין את המודולים ואת קובצי המקור שלו. בינתיים, הסביבה העסקית של האינטרנט התפתחה במהירות ונוצרו כמה פורמטים של מודולים, כולל UMD,‏ CommonJS ובסופו של דבר מודולים סטנדרטיים של JavaScript. עם זאת, כלי הפיתוח נשאר עם הפורמט module.json.

כלי הפיתוח המשיכו לפעול, אבל היו כמה חסרונות בשימוש במערכת מודולים ייחודית ולא סטנדרטית:

  1. הפורמט module.json דרש כלי build מותאמים אישית, בדומה ל-bundlers מודרניים.
  2. לא הייתה שילוב עם סביבת פיתוח משולבת (IDE), ולכן נדרשו כלים מותאמים אישית ליצירת קבצים שסביבות פיתוח משולבות מודרניות יכולות להבין (הסקריפט המקורי ליצירת קבצי jsconfig.json ל-VS Code).
  3. הפונקציות, הכיתות והאובייקטים הועברו להיקף הגלובלי כדי לאפשר שיתוף בין המודולים.
  4. הקבצים היו תלויים בסדר, כלומר הסדר שבו sources מופיעים ברשימה היה חשוב. לא הייתה ערובה לכך שהקוד שאתם מסתמכים עליו ייטען, מלבד העובדה שבן אדם אימת אותו.

בסופו של דבר, בעת בדיקת המצב הנוכחי של מערכת המודול בכלי הפיתוח ובפורמטים אחרים של מודולים (הנפוצים יותר), הגענו למסקנה שהדפוס module.json יוצר יותר בעיות ממה שהוא פתר, והגיע הזמן לתכנן את המעבר ממנו.

היתרונות של תקנים

מבין מערכות המודולים הקיימות, בחרנו במודולים של JavaScript כאלה שנעביר אליהם. בזמן קבלת ההחלטה הזו, מודולים של JavaScript עדיין נשלחו עם דגל ב-Node.js, ומספר גדול של חבילות שזמינות ב-NPM לא הכילו חבילה של מודולים של JavaScript שאפשר להשתמש בה. למרות זאת, הגענו למסקנה שמודול JavaScript הוא האפשרות הטובה ביותר.

היתרון העיקרי של מודולים של JavaScript הוא שזהו פורמט המודול הסטנדרטי של JavaScript. כשציינו את החסרונות של module.json (ראו למעלה), הבנו שרובם קשורים לשימוש בפורמט מודול ייחודי ולא סטנדרטי.

בחירה בפורמט של מודול שאינו סטנדרטי מחייבת אותנו להשקיע זמן בבניית שילובים עם כלי ה-build וכלים שהתחזקים שלנו השתמשו בהם.

השילובים האלה היו לרוב לא עמידים ולא היו תומכים בתכונות מסוימות, ולכן נדרשו זמן תחזוקה נוסף, ולפעמים הם הובילו לבאגים עדינים שהועברו בסופו של דבר למשתמשים.

מאחר שמודולים של JavaScript היו סטנדרטיים, המשמעות היא שסביבות פיתוח משולבות (IDE) כמו VS Code, בודקי סוגים כמו Closure Compiler/TypeScript וכלי build כמו Rollup/minifiers יוכלו להבין את קוד המקור שכתבנו. בנוסף, כשמפתח חדש יצטרף לצוות DevTools, הוא לא יצטרך להקדיש זמן ללמידת פורמט module.json קנייני, אבל סביר להניח שכבר יהיה לו ניסיון עם מודולים של JavaScript.

כמובן, כשכלי הפיתוח נוצר לראשונה, אף אחת מההטבות שלמעלה לא הייתה קיימת. נדרשו שנים של עבודה בקבוצות סטנדרטיות, בהטמעות זמן ריצה ובמפתחים שהשתמשו במודולים של JavaScript כדי לספק משוב כדי להגיע לנקודה שבה הם נמצאים היום. אבל כשמודולים של JavaScript הפכו לזמינים, נאלצנו לבחור: להמשיך לתחזק את הפורמט שלנו או להשקיע בהעברה לפורמט החדש.

העלות של המוצר החדש והנוצץ

למרות שלמודולים של JavaScript יש הרבה יתרונות שרצינו להשתמש בהם, נשארנו בעולם הלא סטנדרטי של module.json. כדי לנצל את היתרונות של מודולים של JavaScript, נאלצנו להשקיע משמעותית בניקוי חוב טכני, לבצע העברה שעשויה לשבור תכונות וליצור באגים של רגרסיה.

בשלב הזה, השאלה לא הייתה 'אנחנו רוצים להשתמש במודולים של JavaScript?', אלא 'כמה יקר להשתמש במודולים של JavaScript?'. במקרה הזה, נאלצנו לאזן בין הסיכון לגרום למשתמשים שלנו לבעיות חזרה לאחור, העלות של הזמן שהמהנדסים יידרשו להשקיע בהעברה והמצב הזמני הגרוע יותר שבו נעבוד.

הנקודה האחרונה הזו התבררה כחשובה מאוד. למרות שתיאוריה יכולנו להגיע למודולים של JavaScript, במהלך ההעברה נקבל קוד שיהיה צורך להתייחס גם למודולים של module.json וגם למודולים של JavaScript. לא רק מבחינה טכנית קשה להשיג את המטרה הזו, אלא גם שכל המהנדסים שעובדים על כלי הפיתוח יצטרכו לדעת איך לעבוד בסביבה הזו. הם יצטרכו לשאול את עצמם כל הזמן "בחלק הזה של קוד הבסיס, האם מדובר ב-module.json או במודולים של JavaScript, ואיך מבצעים שינויים?".

הצצה: העלות המוסתרת של הדרכת המטפלים שלנו בתהליך ההעברה הייתה גבוהה מהצפוי.

אחרי ניתוח העלויות, הגענו למסקנה שעדיין כדאי לעבור למודול JavaScript. לכן, המטרות העיקריות שלנו היו:

  1. חשוב לוודא שהשימוש במודולים של JavaScript קוטע את היתרונות ככל האפשר.
  2. מוודאים שהשילוב עם המערכת הקיימת שמבוססת על module.json בטוח ולא משפיע לרעה על המשתמשים (באגים של רגרסיה, תסכול של משתמשים).
  3. מדריכים את כל המתחזקים של כלי הפיתוח במהלך ההעברה, בעיקר באמצעות בדיקות ויתרות מובנות למניעת טעויות.

גיליונות אלקטרוניים, טרנספורמציות וחובות טכניים

המטרה הייתה ברורה, אבל קשה היה למצוא פתרון עקיף למגבלות שהוטלו על ידי הפורמט module.json. עברנו כמה גרסאות, אב טיפוס ושינויים בארכיטקטורה עד שפיתחנו פתרון שאנחנו מרוצים ממנו. כתבנו מסמך עיצוב עם אסטרטגיית ההעברה שבחרנו. במסמך העיצוב צוין גם אומדן הזמן הראשוני שלנו: שבועיים עד 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 שם הקובץ. ב-sourcetree שלנו, זה יהיה 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 נמשך כמעט שבוע ודרשו כמה ניסיונות עם הורדות מחדש.

import-phase

אחרי שכל הסמלים יוצאו באמצעות הצהרות 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, התלות המעגלית תהיה מפורשת. זו לא בעיה מיידית, אלא אם קיימות קריאות לפונקציות תופעות צד בקוד ההיקף הגלובלי שלך, שגם הוא היה בכלי הפיתוח. בסך הכול, נדרשו כמה פעולות ניתוח ופיתוח מחדש כדי להפוך את הטרנספורמציה לבטוחה.

עולם חדש לגמרי עם מודולים של JavaScript

בפברואר 2020, 6 חודשים אחרי ההתחלה בספטמבר 2019, בוצעו הניקויים האחרונים בתיקייה ui/. זה סימן את הסיום הלא רשמי של ההעברה. אחרי שהסערה שקטה, סימנו באופן רשמי שהמיגרציה הסתיימה ב-5 במרץ 2020. 🎉

עכשיו, כל המודולים ב-DevTools משתמשים במודולים של JavaScript כדי לשתף קוד. אנחנו עדיין מוסיפים מספר סמלים להיקף הגלובלי (בקובצי module-legacy.js) בשביל הבדיקות הקודמות שלנו או כדי לשלב אותם עם חלקים אחרים של ארכיטקטורת כלי הפיתוח. התכונות האלה יוסרו עם הזמן, אבל אנחנו לא מחשיבים אותן כחוסמות לפיתוח עתידי. יש לנו גם מדריך סגנונות לשימוש שלנו במודולים של JavaScript.

נתונים סטטיסטיים

לפי אומדנים שמרניים, מספר רשימות השינויים (CL) שכלולות בהעברה הזו הוא כ-250 רשימות שינויים, שבוצעו בעיקר על ידי שני מהנדסים. אין לנו נתונים סטטיסטיים מדויקים לגבי היקף השינויים שבוצעו, אבל אומדן שמרני של השורות שהשתנו (החישוב מבוסס על הסכום של ההבדל המוחלט בין ההוספות למחיקות בכל בקשת שינוי) הוא כ-30,000 שורות (כ-20% מכל הקוד של חזית DevTools).

הקובץ הראשון עם export שוחרר ב-Chrome 79, והגרסה היציבה שלו שוחררה בדצמבר 2019. השינוי האחרון במעבר ל-import נשלח ב-Chrome 83, ששוחרר לגרסה היציבה במאי 2020.

אנחנו מודעים לרגרסיה אחת שנשלחה ליציבות של Chrome ושנשלחה כחלק מההעברה הזו. ההשלמה האוטומטית של קטעי קוד בתפריט הפקודות התקלקלה בגלל ייצוא default זר. היו לנו כמה נסיגות אחרות, אבל חבילות הבדיקה האוטומטיות שלנו ומשתמשי Chrome Canary דיווחו עליהן ותיקנו אותן לפני שהן יכלו להגיע למשתמשי Chrome בגרסה היציבה.

אפשר לראות את התהליך המלא (לא כל בקשות התמיכה מצורפות לבאג הזה, אבל רובן כן) ביומן בכתובת crbug.com/1006759.

מה למדנו

  1. החלטות שהתקבלו בעבר יכולות להשפיע על הפרויקט לאורך זמן. למרות שמודול JavaScript (ופורמטים אחרים של מודולים) היו זמינים במשך זמן מה, כלי הפיתוח לא היו מוכנים להצדיק את ההעברה. קשה להחליט מתי ומתי לא לעבור, ומבוססת על ניחוש מושכלות.
  2. האומדנים הראשוניים שלנו היו בשבועות ולא בחודשים. הסיבה העיקרית לכך היא שזיהינו יותר בעיות בלתי צפויות ממה שציפינו בניתוח העלויות הראשוני. למרות שתוכנית ההעברה הייתה טובה, הבעיה העיקרית הייתה חוב טכני (לעיתים קרובות יותר ממה שרצינו).
  3. ההעברה של מודולי JavaScript כללה כמות גדולה של פעולות ניקוי של חובות טכניים (שנראה שהן לא קשורות). המעבר לפורמט מודול סטנדרטי מודרני אפשר לנו להתאים את שיטות הקוד המומלצות שלנו לפיתוח אתרים מודרני. לדוגמה, הצלחנו להחליף את Bundler המותאם אישית של Python בתצורת אוסף כללית מינימלית.
  4. למרות ההשפעה הגדולה על קוד הבסיס שלנו (כ-20% מהקוד השתנה), דווחו מעט מאוד נסיגות. אמנם היו לנו בעיות רבות בהעברת הקבצים הראשונים, אבל אחרי זמן מה פיתחנו תהליך עבודה יציב ואוטומטי באופן חלקי. לכן, ההשפעה השלילית על המשתמשים היציבים שלנו הייתה מינימלית.
  5. קשה, ולפעמים בלתי אפשרי, ללמד את המורכבות של העברה מסוימת למנהלי תוכנה אחרים. קשה לעקוב אחר העברות בקנה מידה כזה ונדרש ידע רב בתחום. העברת הידע הזה לאנשים אחרים שעובדים באותו קוד לא רצויה בפני עצמה לצורך העבודה שהם מבצעים. חשוב לדעת מה לשתף ואילו פרטים לא לשתף, וזוהי אומנות שצריך לשלוט בה. לכן חשוב לצמצם את מספר ההעברות הגדולות, או לפחות לא לבצע אותן בו-זמנית.

הורדת הערוצים לתצוגה מקדימה

כדאי להשתמש ב-Chrome Canary, Dev או בטא כדפדפן הפיתוח שמוגדר כברירת מחדל. הערוצים לתצוגה מקדימה אלה מעניקים לך גישה לתכונות החדשות של כלי הפיתוח, מאפשרים לך לבדוק ממשקי API מתקדמים של פלטפורמות אינטרנט ולעזור לך לגלות בעיות באתר שלך לפני שהמשתמשים עושים זאת.

יצירת קשר עם צוות כלי הפיתוח ל-Chrome

אתם יכולים להשתמש באפשרויות הבאות כדי לדון בתכונות החדשות, בעדכונים או בכל דבר אחר שקשור ל-DevTools.