מבוא למפות מקור ב-JavaScript

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

מפות מקור הן דרך למפות קובץ משולב/מוקטן בחזרה למצב ללא build. כשאתם יוצרים build לסביבת הייצור, בנוסף לצמצום קובצי ה-JavaScript ושילובם, אתם יוצרים מפת מקור שמכילה מידע על הקבצים המקוריים. כששולחים שאילתה לגבי מספר שורה ומספר עמודה מסוימים ב-JavaScript שנוצר, אפשר לבצע חיפוש במפת המקור שמחזיר את המיקום המקורי. כלים למפתחים (נכון לעכשיו, גרסאות build לילה של WebKit, ‏ Google Chrome או Firefox מגרסה 23 ואילך) יכולים לנתח את מפת המקור באופן אוטומטי ולגרום לכך שייראה כאילו אתם מריצים קבצים לא מקוצרים ולא משולבים.

בדמו אפשר ללחוץ לחיצה ימנית במקום כלשהו בתיבת הטקסט שמכילה את המקור שנוצר. אם בוחרים באפשרות 'קבלת המיקום המקורי', המערכת תבצע שאילתה במפת המקור על ידי העברת מספר השורה ומספר העמודה שנוצרו, ותחזיר את המיקום בקוד המקורי. חשוב לוודא שהמסוף פתוח כדי שתוכלו לראות את הפלט.

דוגמה לספריית מפות המקור של JavaScript של Mozilla בפעולה.

עולם אמיתי

לפני שצופים בהטמעה הבאה של מפות המקור בעולם האמיתי, צריך לוודא שהפעלתם את התכונה 'מפות מקור' ב-Chrome Canary או ב-WebKit nightly. לשם כך, לוחצים על גלגל השיניים של ההגדרות בחלונית של כלי הפיתוח ומסמנים את האפשרות 'הפעלת מפות מקור'.

איך מפעילים מפות מקור בכלי הפיתוח של WebKit.

בממשק של Firefox מגרסה 23 ואילך, מפות המקור מופעלות כברירת מחדל בכלי הפיתוח המובנים.

איך מפעילים מפות מקור בכלי הפיתוח של Firefox.

למה כדאי להשתמש במפות מקור?

נכון לעכשיו, מיפוי מקורות פועל רק בין JavaScript לא דחוס/משולב ל-JavaScript דחוס/לא משולב, אבל העתיד נראה מבטיח עם דיונים על שפות שעבר תהליך הידור ל-JavaScript כמו CoffeeScript, ואפילו על האפשרות להוסיף תמיכה במעבדים מראש של CSS כמו SASS או LESS.

בעתיד נוכל להשתמש בקלות כמעט בכל שפה, כאילו היא נתמכת באופן מקורי בדפדפן באמצעות מפות מקור:

  • CoffeeScript
  • ECMAScript 6 ואילך
  • SASS/LESS ועוד
  • כמעט כל שפה שאפשר לבצע לה קומפילציה ל-JavaScript

כדאי לצפות בקלטת המסך הזו של ניפוי באגים ב-CoffeeScript בגרסה ניסיונית של מסוף Firefox:

לאחרונה נוספה ל-Google Web Toolkit ‏ (GWT) תמיכה במפות מקור. Ray Cromwell מצוות GWT יצר סרטון מדהים שמראה את התמיכה במפות מקור בפעולה.

דוגמה נוספת שערכתי משתמשת בספריית Traceur של Google, שמאפשרת לכתוב ES6 (ECMAScript 6 או Next) ולקמפל אותו לקוד תואם ל-ES3. המהדר של Traceur יוצר גם מפת מקור. כדאי לעיין בהדגמה הזו של מאפיינים וכיתות ב-ES6 שבהם נעשה שימוש כאילו הם נתמכים באופן מקורי בדפדפן, בזכות מפת המקור.

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

ניפוי באגים ב-Traceur ES6 באמצעות מפות מקור.

דגמה: כתיבת ES6, ניפוי באגים וצפייה במיפוי המקור

איך פועלת מפת המקור?

היחיד מבין המהדררים/המיניפיירים של JavaScript שיש להם תמיכה כרגע ביצירת מפות מקור הוא Closure compiler. (אסביר איך משתמשים בו בהמשך). אחרי שמשלבים ומצמצמים את ה-JavaScript, לצד הקובץ הזה יופיע קובץ מפת מקור.

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

//# sourceMappingURL=/path/to/file.js.map

כך כלים לפיתוח יכולים למפות את הקריאות בחזרה למיקום שלהן בקובצי המקור המקוריים. בעבר, ה-pragma של התגובה היה //@, אבל בגלל בעיות מסוימות עם ה-pragma הזה ועם תגובות של הידור מותנה ב-IE, הוחלט לשנות אותו ל-//#. בשלב הזה, הדפדפנים Chrome Canary,‏ WebKit Nightly ו-Firefox מגרסה 24 ואילך תומכים בפרוגמה החדשה של התגובה. שינוי התחביר הזה משפיע גם על sourceURL.

אם אתם לא אוהבים את הרעיון של התגובה המוזרה, תוכלו להגדיר כותרת מיוחדת בקובץ ה-JavaScript המהדר:

X-SourceMap: /path/to/file.js.map

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

דוגמה ל-WebKit Devtools של מפות מקור שפועלות ומפות מקור מושבתות.

קובץ מפת המקור יורד רק אם מפות המקור מופעלות וכלי הפיתוח פתוחים. תצטרכו גם להעלות את הקבצים המקוריים כדי שכלי הפיתוח יוכלו להפנות אליהם ולהציג אותם במקרה הצורך.

איך יוצרים מפת מקור?

צריך להשתמש ב-Closure compiler כדי לבצע אופטימיזציה לקובצי JavaScript, לצרף אותם ולייצר מפת מקור. הפקודה היא:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

שני הדגלים החשובים של הפקודה הם --create_source_map ו---source_map_format. צריך לעשות זאת כי גרסת ברירת המחדל היא V2, ואנחנו רוצים לעבוד רק עם V3.

המבנה של מפת מקור

כדי להבין טוב יותר את מפת המקור, נשתמש בדוגמה קטנה לקובץ מפת מקור שייווצר על ידי המהדר Closure, ונרחיב על אופן הפעולה של הקטע 'מיפויים'. הדוגמה הבאה היא וריאציה קלה על הדוגמה של מפרט V3.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

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

  • מספר הגרסה שממנה מבוססת מפת המקור
  • שם הקובץ של הקוד שנוצר (קובץ הייצור המינימלי/המאוחד)
  • sourceRoot מאפשר להוסיף לתחילת המקורות מבנה תיקיות – זו גם טכניקה לחיסכון במקום
  • המשתנה sources מכיל את כל שמות הקבצים ששולבו
  • המשתנה names מכיל את כל שמות המשתנים/השיטות שמופיעים בקוד.
  • לבסוף, המאפיין mappings הוא המקום שבו קורה הקסם באמצעות ערכים של VLQ ב-Base64. כאן מתבצע החיסכון האמיתי במקום.

Base64 VLQ ושמירה על מפת המקור קטנה

במקור, למפרט של מפת המקור היה פלט מפורט מאוד של כל המיפויים, וכתוצאה מכך מפת המקור הייתה גדולה פי 10 בערך מהקוד שנוצר. בגרסה השנייה הגודל הצטמצם בכ-50%, ובגרסה השלישית הוא הצטמצם שוב ב-50%. כך, בקובץ של 133KB, מקבלים מפת מקור בגודל של כ-300KB.

אז איך הצליחו להקטין את הגודל ועדיין לשמור על המיפויים המורכבים?

נעשה שימוש ב-VLQ (כמות באורך משתנה) יחד עם קידוד הערך לערך Base64. מאפיין המיפויים הוא מחרוזת גדולה מאוד. במחרוזת הזו מופיעות נקודות-פסיק (;) שמייצגות את מספר השורה בקובץ שנוצר. בכל שורה יש פסיקים (;) שמייצגים כל מקטע בשורה הזו. כל אחד מהפלחים האלה הוא 1, 4 או 5 בשדות באורך משתנה. חלק מהם עשויים להיראות ארוכים יותר, אבל הם מכילים ביטים להמשך. כל מקטע מבוסס על המקטע הקודם, וכך עוזר להקטין את גודל הקובץ כי כל ביט הוא יחסי לקטעים הקודמים שלו.

פירוט של מקטע בקובץ ה-JSON של מפת המקור.

כפי שצוין למעלה, כל מקטע יכול להיות באורך משתנה של 1, 4 או 5. התרשים הזה נחשב באורך משתנה של ארבע עם ביט המשך אחד (g). ננתח את הקטע הזה ונראה איך מפת המקור קובעת את המיקום המקורי.

הערכים שמוצגים למעלה הם הערכים שפורקו מ-Base64 בלבד. יש צורך בעוד עיבוד כדי לקבל את הערכים האמיתיים שלהם. בדרך כלל, כל קטע מחשב חמישה דברים:

  • עמודה שנוצרה
  • הקובץ המקורי שבו הופיע התוכן
  • מספר השורה המקורי
  • העמודה המקורית
  • וגם, אם יש כזה, השם המקורי

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

אחרי עיבוד נוסף, התרשים שלמעלה AAgBC יחזיר את הערכים 0, 0, 32, 16, 1 – הערך 32 הוא ביט ההמשך שעוזר ליצור את הערך הבא של 16. הערך של B לאחר פענוח ב-Base64 הוא 1. לכן הערכים החשובים שבהם נעשה שימוש הם 0, 0, 16, 1. כך אנחנו יודעים ששורה 1 (השורות נספרות לפי הנקודתיים) עמודה 0 בקובץ שנוצר ממופה לקובץ 0 (מערך הקבצים 0 הוא foo.js), שורה 16 בעמודה 1.

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

כדי להבין איך מקבלים את הערך 16 מ-B, צריך להבין את האופרטורים הבסיסיים של ביטים ואת אופן הפעולה של המפרט למיפוי מקורות. הספרה הקודמת, g, מסומנת כביט המשך על ידי השוואה בין הספרה (32) לבין VLQ_CONTINUATION_BIT (100000 או 32 בינארי) באמצעות האופרטור AND (&) בינארי.

32 & 32 = 32
// or
100000
|
|
V
100000

הפונקציה מחזירה 1 בכל מיקום ביט שבו הוא מופיע בשני המחרוזות. לכן, ערך של 33 & 32 שעבר פענוח Base64 יחזיר את הערך 32, כי הם חולקים רק את המיקום של 32 הביט, כפי שמוצג בתרשים שלמעלה. לאחר מכן, ערך ההזזה של הבייט גדל ב-5 לכל ביט המשך שקודם לו. במקרה שלמעלה, הוא הוזז ב-5 רק פעם אחת, כך ש-1 (B) הוזז ב-5 שמאלה.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

לאחר מכן, הערך הזה מומר מערך VLQ עם סימן על ידי הזזה ימינה של המספר (32) מקום אחד.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

אז זהו: כך הופכים 1 ל-16. התהליך הזה עשוי להיראות מסובך מדי, אבל ככל שהמספרים גדלים, הוא הופך הגיוני יותר.

בעיות אפשריות בנושא XSSI

במפרט מפורטות בעיות שקשורות להכללת סקריפטים באתרים שונים, שעשויות להתרחש כתוצאה משימוש במפת מקור. כדי לצמצם את הבעיה, מומלץ להוסיף את הערך ')]}' לתחילת השורה הראשונה של מפת המקור כדי לבטל את התוקף של JavaScript בכוונה, וכך לגרום לשגיאת תחביר. כלי הפיתוח של WebKit כבר יכולים לטפל בזה.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

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

sourceURL ו-displayName בפעולה: פונקציות Eval ופונקציות אנונימיות

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

הכלי הראשון לעזרה דומה מאוד למאפיין //# sourceMappingURL, והוא מוזכר למעשה במפרט של מפת המקור V3. אם תכללו את התגובה המיוחדת הבאה בקוד, היא תבוצע, ותוכלו לתת שמות לפעולות הערכה (eval) כך שיופיעו בשמות לוגיים יותר בכלי הפיתוח. כדאי לצפות בהדגמה פשוטה באמצעות המהדר של CoffeeScript:

דוגמה: הצגת הקוד של eval() כסקריפט דרך sourceURL

//# sourceURL=sqrt.coffee
איך נראה תגובה מיוחדת של sourceURL בכלי הפיתוח

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

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
הצגת המאפיין displayName בפעולה.

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

נכון למועד כתיבת המאמר, האפשרות לתת שמות ל-eval זמינה רק בדפדפני Firefox ו-WebKit. הנכס displayName קיים רק בגרסאות WebKit nightlies.

בואו נאחד כוחות

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

ב-UglifyJS יש גם בעיה במפת המקור שצריך לבדוק.

יש הרבה כלים שיוצרים מפות מקור, כולל המהדר של coffeescript. לדעתי זה לא רלוונטי עכשיו.

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

זה לא מושלם

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

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

בעיות

לאחרונה נוספה ל-jQuery 1.9 תמיכה במיפויי מקור כשהם מוצגים מחוץ ל-CDNs הרשמיים. הוא גם הצביע על באג מוזר כשמשתמשים בהערות של הידור מותנה ב-IE‏ (‎//@cc_on) לפני טעינת jQuery. מאז, בוצע commit לצמצם את הבעיה על ידי עטיפה של sourceMappingURL בתגובה בכמה שורות. הלקח: אל תשתמשו בתגובות מותנות.

הבעיה טופלה לאחר מכן על ידי שינוי התחביר ל-//#.

כלים ומשאבים

ריכזנו כאן כמה מקורות מידע וכלים נוספים שכדאי לעיין בהם:

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

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