יום שלישי, 30 באפריל 2013

Require.js - צלילה לעומק

בפוסט הקודם הצגנו את שלוש הפקודות הבסיסיות של require:
  • define
  • require
  • require.config
בפוסט זה נסקור קצת יותר לעומק את המבנה וההתנהגות של Require.js.

שייך לסדרה: MVC בצד הלקוח, ובכלל.


חזרה והרחבה: Require מול האופציות השונות:

התגובות על הפוסט הקודם סייעו לי להבין שלמרות הסיפור על תולדות Require, עדיין לא ברור בדיוק הקשר בין require ל AMD ו CommonJS ומהן האלטרנטיבות השונות הזמינות. אנסה לספק מידע ישיר יותר בשאיפה שהוא יעזור להסיר את העננה.

ניסיתי לתת להרכבת הצבעים משמעות לתיאור הרכבת היכולות

Require הוא מימוש של AMD, תקן המטפל ב-2 נושאים:
  1. הגדרה של מודולים - עבור הבניית מודולריות בקוד.
  2. טעינה דינמית של קבצי ג'אווהסקריפט.
אלו הם שני צרכים שונים ומיד אראה שספריות רבות בחרו לטפל רק בנושא אחד, מבלי לטפל בשני.

שימו לב של-Require יש גם תאימות ל CommonJS. עובדה זו יכולה להיות מעט מבלבלת ולכן רציתי לשמור אותה לסוף פוסט ההמשך (כלומר הפוסט הנוכחי). אני מעריך שהסיבה מאחורי תאימות זו היא ההכרה שקוד רב נכתב ע"פ ההגדרות של CommonJs והרצון לאפשר אינטגרציה קלה בין 2 סגנונות הגדרת המודולים.


Require.js איננה בודדה בסצנת "הגדרת המודולים" ו/או בסצנת "טעינת משאבים דינמית" - יש הרבה מאוד מבחר. הנה כמה מהאלטרנטיביות היותר פופולריות / משמעותיות והקשר שלהן ל AMD ו CommonJS:


בעוד ספריות להגדרת מודולים (כמו Almond או Browserify) לרוב תואמות לאחד התקנים AMD/CommonJS, ספריות לטעינת משאבים בלבד אינן מחויבות לאף תקן. הנה השוואה שמצאתי בין מספר ספריות לטעינת משאבים.

אני רוצה להדגיש הבדל קטן בין "ספרייה לטעינה דינמית ג'אווהסקריפט" ו"ספריה לטעינה דינמית של משאבים". Require בבסיסה טוענת דינמית רק קבצי ג'אווהסקריפט, אולי בגלל שזה מה ש AMD מגדיר. בפועל יש צורך, חזק באותה המידה, לטעון דינמית קבצי CSS או snippets של HTML (לרוב templates עבור מנועי templating כגון handlebars, mustache וכו').

כפי שנראה בהמשך הפוסט, require היא ספרייה גדולה ומקיפה, ולא כ"כ סביר שתהיה יכולת נפוצה שהיא לא מכסה :). טעינה דינמית של CSS / HTML snippets מתבצעת ב require בעזרת פלאג-אין שנקרא text.js.

אציין עוד ש Almond ו Browserify הן שתי אופציות רזות ופופולריות להגדרה של מודולים ללא טעינה דינמית, אחת תואמת ל AMD והשנייה ל CommonJS. אם התכנית שלכם מספיק קטנה בכדי שתוכלו לאגד את כל קבצי ה javaScript לקובץ אחד גדול ולטעון אותו בעת העליה (כלומר, אינכם זקוקים לטעינה דינמית או יכולות מתקדמות) - אזי ספריות אלו יכולות לספק הגדרה של מודולים במחיר 1K~ של קוד ג'אווהסקריפט minified, במקום 15K~ של require.

סה"כ, מכל האלטרנטיבות הקיימות כיום, require היא כנראה הספרייה המקיפה ביותר ובעלת ה eco-system הגדול ביותר. מספר רב של כלים וספריות מספקים אינטגרציה ספציפית לספריית require.


ריבוי אפשרויות ב require

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

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


בכדי לקצר ו"להיפטר" מקובץ main בן 3 שורות קוד, require מאפשרת לבצע את האתחול מתוך require.config.
deps הוא הפרמטר המתאר את רשימת המודולים הנדרשים, בעוד callback הוא המצביע לפונקציה שתופעל לאחר שרשימת התלויות ב deps נטענה. סדר הפרמטרים (form) יהיה מתואם - ממש כמו בקריאת require.

מה קורה פה?
מבין 3 פקודות סה"כ, פקודה require יכולה להיעשות גם מתוך פקודת define וגם מתוך פקודת require.config. האם אין פה "הרבה דרכים לבצע אותו הדבר"?

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

דוגמה נוספת: את האתחול של require ניתן לבצע גם באופן הבא:

מזהים את ההבדל?

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

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






זיהוי ואיתור מודולים

AMD מציינת זיהוי של מודול ע"י ModuleId, מחרוזת המשמשת כ Alias לתיאור המודול.
URLs לקובץ הג'אווהסקריפט יכול להשתנות, והשימוש ב Alias מאפשר לנו להתמודד בקלות יחסית עם שינוי של ערך ה URL.

מצד שני, גם ניהול של Module Id יכול להיות דבר לא קל. לאחר זמן-מה עשויים להיגמר לנו ה"שמות המקוריים" למודולים. לזכור מה ההבדל בין 'MyModule63' לבין 'MyModule64' - עשויה להיות בעיה גדולה לא פחות.

על כן הפרקטיקה המקובלת ב require היא לקרוא לשם המודול כשם ה path היחסי בו נמצא קובץ הג'אווהסקריפט.
אם קיים מבנה הספריות הבא:

אזי נקרא ל storage בשם 'services/storage' ול registration נקרא בשם 'controllers/registration'.

שימוש ב path כ moduleId הפכה לפרקטיקה נפוצה ומומלצת, כך ש require תומכת בה באופן טבעי. אם משמיטים את  את ה moduleID מפקודת ה define אזי require תגדיר בעבורנו את ה ID של המודול ע"פ הנתיב היחסי.


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

חשוב לשים לב שאין לכתוב את הסיומת js. בשם המודול.
אם require נתקלת בסיומת js. - היא מניחה שזהו URL ולא ModuleId. רשימת התלויות בפקודת ה require (והוריאציות השונות שלה) יכולה להכיל גם moduleIds, אך גם URLs (יחסיים או אבסולוטיים).

בעיה שמיד עולה היא "כיצד require יודעת מאיפה להתחיל לחפש? איך אני יודע ששם הקובץ לא צריך להיות 'demo location/controllers/registration'?

Require מחפשת את המודולים יחסית ל baseUrl, אשר נקבע באופן הבא:
  1. אם צוין property של data-main בקובץ ה HTML - מיקום סקריפט ה main יהיה ה baseURL.
  2. אחרת מיקום קובץ ה html יקבע להיות ה baseUrl.
  3. ניתן לקבוע baseURL באופן מפורש בעזרת require.config.
שימוש ב URL כ Module Id איננה התנהגות מומלצת. אנו רוצים להמנע מהקלדה חוזרת של ה URL בקוד.
כאשר אנו רוצים לטעון Module ע"פ URL (סיבה לדוגמה: זו ספריה חיצונית ולא חלק מהפרוייקט שלנו), אנו נשתמש ב aliases, שזה סוג של שימוש בהגדרה שנקראת paths:



ה path הראשון, "jquery", משמש בפועל alias.

ב1 - אנחנו טוענים את jQuery דינמית לתוך $. זכרו ש jQuery הוא לא מודול בפרוייקט שהגדרנו בעזרת define. כיצד, אם כן אפשר לקרוא לו? ספציפית jQuery הוסיפה תמיכה בתחביר ה AMD החל מגרסה 1.7:


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

שימו לב שלמרות שציינתי URL, במקרה המיוחד של ה Alias אני עדיין משמיט את סיומת ה js. משם הקובץ. חבל ש Aliases נראים כמו באג ולא מתוארים בצורה מפורשת ופשוטה יותר.

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

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


הערה: דפדפן IE בגרסאות 6 עד 8 (Hello, hello) לא תומך באירוע script.onError ולכן fallsbacks לא יעבדו. ב IE9 יש באג ולכן יש מגבלות.


ה path השני והשלישי באמת משמשים כ paths.
אם אנו מבקשים לטעון מודול ששמו מתחיל באחד מה paths המצוינים, יוחלף אותו חלק ב path.

ב2 (מתוך דוגמת הקוד למעלה) - אנחנו יכולים לראות 2 דוגמאות לכך, אחת כ URL ואחת כ path בתוך ההיררכיה של baseUrl.
בקשה לטעינת 'gili/utils', תגרום ל require לטעון קובץ בשם 'https://cdn.gili.com/utils.js'.





קונפיגורציה של מודולים

לעתים אנו רוצים לספק למודולים שלנו קונפיגורציה שאיננה חלק מהקוד.
סיבה נפוצה אחת היא יצירת קובץ קונפיגורציה (שיכול להיות בסיומת js.) שבעזרתו יוכל ה Administrator לשנות פרמטרים של המערכת.
אני אישית משתמש ביכולת זו על מנת "להחדיר" state למודולים או Mocks מותך בדיקות-היחידה.

הנה הדרך שבה ניתן לבצע קונפיגורציה שכזו, ולצרוך אותה:

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

ה ModuleId שאנו מגדירים יחופש ע"פ הלוגיקה שתוארה בפסקה הקודמת. כלומר: אם יש paths או maps (אני מזכיר אותם בסוף הפוסט) - אזי הם יילקחו בחשבון. תכונה זו חשובה להתמודדות עם מקרי-קצה בפרוייקטים מורכבים. למשל: אנו מגדירים מהי הקונפיגורציה ל ModuleId, אך רק ע"פ קונפיגורציה נוספת ייקבע מיהו המודול שיענה ל Module Id הזה ויקבל את הקונפיגורציה בזמן-ריצה.

כדי לשלוף את הקונפיגורציה, יש בהגדרת המודול להוסיף תלות ב Module ID "שמור" של require (יש עוד כמה כאלו) בשם: "module". על האובייקט שנקבל כתוצאה מתלות זו אפשר לבדוק את ה ID של המודול שלנו (module.id) את ה url לקובץ (module.url) או את הקונפיגורציה שהגדרנו, בעזרת ()module.config. 



התמודדות עם קוד שלא הוגדר כ AMD Module

כשאנו כותבים אפליקציית javaScript בעזרת require, ניהול התלויות בקוד החדש שכתבנו מתנהל בקלות.
מה קורה כאשר אנו רוצים לטעון ספריית צד שלישי שלא נכתבה ע"פ התחביר של AMD?
האם אפשר לטעון אותה דינמית? כיצד נוכל לגשת אליה?

Require מתמודדת עם בעיה זו בעזרת אלמנט קונפיגורציה שנקרא shim (להלן פירוש השם).
להזכיר: ספריות שאינן תואמות ל AMD משתמשות בדרך תקשורת "פרימיטיבית" של רישום משתנים גלובליים (כמו $ או BackBone) בכדי לאפשר גישה לספרייה. קונפיגורציית shim מלמדת את הספריות הללו נימוסים ומאפשרות לצרוך אותן ע"פ כללי הטקס של AMD.

בואו נראה כיצד יש לטפל בספריית Backbone.js (אותה כיסיתי כחלק מהסדרה MVC בצד הלקוח ובכלל). Backbone תלויה בשתי ספריות אחרות: JQuery ו Underscore.


בואו נזכר: כיצד נראית פקודת define מתוקנת ותרבותית?

define (moduleID, [deps], callback func);

ה moduleId הוא ערך המפתח על אובייקט ה shim. בדוגמה זו בחרתי בהפגנתיות לקרוא למודול של Backbone בשם "bakcboneModule", אולם בעבודה יומיומית הייתי נצמד לקונבנציה וקורא לו פשוט "backbone". המודול השני הוא underscore. כפי שציינתי קודם לכן, jQuery (גרסה 1.7 ומעלה) היא תואמת AMD ולכן אין צורך להגדיר אותה כ shim.

את התלויות אנו מתארים בפרמטר ה deps (אם יש). כדאי לציין שתלויות יכולות להיות shims אחרים או מודולים שהוגדרו בעזרת "define" אבל אין להם תלויות במודולים אחרים. לרוב מגבלה זו לא תפריע.

את ה callback function אין צורך להגדיר, מכיוון שהקוד של הסקריפט (הלא מתורבת הזה!) ירוץ אוטומטית כאשר הסקריפט נטען. כל שנותר לנו הוא לאסוף מצביע לתוצאת ההרצה ולהעביר אותה למודול שביקש להשתמש במודול מלכתחילה.

במקרה שלנו, require יצטרך לדעת איזה ערך לשים בתוך המשתנה m עבור המודול שהגדיר תלות ב Backbone (בצורה תרבותית):

define (['backboneModule'], function(m) {
  ...
}

הערך שיושם ב m במקרה זה מוגדר ע"י פרמטר ה exports (כלומר: "הספריה הנ"ל חושפת את עצמה ע"י...), שהוא שם של משתנה גלובלי עליו רשומה הספריה כגון 'Backbone' או 'jQuery'. ספריית Underscore באמת חושפת את עצמה תחת השם "_" (ומכאן שמה).

לבסוף עלינו להגדיר aliases, היכן נמצאים הקבצים של ספריית Backbone והתלויות שלה. זכרו שיש להשמיט את סיומת ה "js." בכדי שה alias יעבוד. בנוסף הרשתי לעצמי לציין fallback ל jQuery אם ה URL הראשון לא זמין.







עוד כמה נקודות מעניינות (בקצרה)

require נוסח CommonJS
למרות ש require נצמדת לתחביר של AMD, היא מספקת גם תאימות לתחביר של CommonJS. תאימות זו לא תמיד אלגנטית, ואני לא בטוח שהיא שלמה.
למה אני מספר זאת? מצאתי את פקודת התאימות לתחביר CommonJS שימושית לבעיות יומיומיות, שאינן קשורות ל CommonJS.


כאשר אני קורא ל require עם פרמטר יחיד (שאינו מערך) אזי מופעלת פקודת ה require ע"פ commonJS.
בפועל אני אקבל מידית (כלומר: סינכרונית) מצביע למודול שביקשתי, בהנחה שהוא כבר נטען ע"י require. אם הוא לא נטען - אקבל exception.

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

עוד תאימות מעניינת היא לתקן ה Packages/1.0 של CommonJS - עליה תוכלו לקרוא כאן.


בדיקות-יחידה.
Require עושה חיים קלים בצד ניהול הקוד והתלויות, אולם אם אתם רוצים לכתוב בדיקות-יחידה שיבדקו את המודולים כאשר הם נטענים בעזרת require - זה עשוי להיות קשה יותר.

אני משתמש ב framework שנקרא Karma (עד לא מזמן נקרא Testacular), שהוא (סליחה על עומס המושגים): porting של jsTestDriver ל node.js, כחלק מפרויקט AngularJs, של גוגל.

בקיצור: זו תשתית בדיקה נהדרת, שיכולה לעבוד גם עם Jasmine וגם עם QUnit בצורה יפה וגם יש לה תמיכה מובנית ב Require.
ל Karma יש את כל היתרונות של jsTestDriver (הרצה מהירה ומקומית של הבדיקות, בדיקה על מספר דפדפנים אמתיים במקביל). בנוסף, יש לה תמיכה מובנית ב require, קונפיגורציה גמישה יותר (וללא הבאג של תיקיות יחסיות) והכי מגניב: היא מאזינה לשינויים במערכת הקבצים וכל פעם שאתם שומרים קובץ היא מריצה את בדיקות היחידה אוטומטית ומציגה את התוצאות ב console. מאוד שימושי ל TDD.

ספריית jsTestDriver הוזנחה בתקופה האחרונה ע"י הקהילה שלה, אז מעבר ל Karma הוא טבעי ונכון. אם אתם עובדים עם קבצי HTML שאתם צריכים לפתוח כל פעם ידנית - כדאי לעבור ל Karma שנותנת פידבק על תקינות הקוד בצורה מהירה הרבה יותר.


גרסאות של ספריות
ל Require יש מנגנון שמאפשר לטעון במצבים שונים גרסאות שונות של ספריות. נניח jQuery 1.9 או jQuery 2.0. אפשר לקרוא על מנגנון זה בלינק הבא.


סיכום

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

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


שיהיה בהצלחה!




6 תגובות:

  1. תגובה זו הוסרה על ידי המחבר.

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

    עדיף לקחת דוגמאות קוד אחת ולהסביר עליה את כל מה שרוצים.

    פלות מלל יותר מיקוד בדברים החשובים...

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

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

      מחק
  3. ישר כוח ליאור - פוסט נפלא, כרגיל.

    השבמחק