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

Tim van der Lippe
Tim van der Lippe

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

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

בהתחלה לא היה שום דבר

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

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

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

העלות של העיצוב החדש והנוצץ

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

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

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

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

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

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

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

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

התראת ספוילר: החלק האינטנסיבי ביותר של ההעברה נמשך 4 חודשים, מתחילתו ועד סופו – 7 חודשים!

עם זאת, התוכנית הראשונית עמדה במבחן הזמן: היינו מלמדים את זמן הריצה של DevTools לטעון את כל הקבצים הרשומים במערך scripts בקובץ module.json באמצעות השיטה הקודמת, ואילו כל הקבצים שרשומים במערך modules עם ייבוא דינמי של מודולי JavaScript. כל קובץ שנמצא במערך modules יוכל להשתמש בייבוא/ייצוא של ES.

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

אחד הגילויים שגילינו כשטיפלנו בבעיות האלה הוא שהבדיקות שלנו פעלו במצב 'מרושל'. מאחר שהמודולים של JavaScript מרמזים שקבצים פועלים במצב "use strict", יש לכך השפעה גם על הבדיקות שלנו. כפי שהתברר, כמות לא מציאותית של בדיקות הסתמכה על הרשלנות הזו, כולל בדיקה שהשתמשה בהצהרה with 😱.

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

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

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

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

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

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

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

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

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

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

מה למדנו

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

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

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

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

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

  • שלחו לנו הצעה או משוב דרך crbug.com.
  • כדי לדווח על בעיה בכלי הפיתוח, לוחצים על אפשרויות נוספות   עוד   > עזרה > דיווח על בעיות בכלי הפיתוח בכלי הפיתוח.
  • אפשר לשלוח ציוץ אל @ChromeDevTools.
  • אפשר לכתוב תגובות לגבי 'מה חדש' בסרטוני YouTube בכלי הפיתוח או בסרטונים ב-YouTube בקטע 'טיפים לשימוש בכלי הפיתוח'.