2013-04-26

AMD ו Require.js

תבנית העיצוב AMD (ראשי תיבות של Async Module Definition) היא בהחלט לא MVC. מדוע עם כן אני עוסק בה בסדרה על MVC?

AMD היא תבנית עיצוב חדשה-יחסית, המיושמת בעיקר בג'אווהסקריפט ובאה לסייע לכתוב כמות גדולה של קוד ג'אווהסקריפט כך שיהיה קל לתחזוקה.
הדמיון ל MVC הוא במטרה - שמירה של סדר בעבודה עם כמות גדולה של קוד. דרך הפעולה - שונה לגמרי. אפשר (וכדאי) להשתמש גם ב MVC וגם ב AMD במקביל על מנת "לעשות סדר" בקוד הג'אווהסקריפט שלנו.

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





הקדמה

בשנת 2009, בחור בשם קוין דנגור (Kevin Dangoor) יזם פרויקט בשם ServerJS. מטרת הפרויקט: להתאים את שפת ג'אווהסקריפט לפיתוח צד-השרת.

הפרויקט, כלל קבוצות עבודה שהגדירו APIs לצורת עבודה שתהיה נוחה ואפקטיבית בפיתוח ג'אווהסקריפט בשרת.
פרויקט ServerJS הצליח לעורר הדים והשפיע בצורה משמעותית על עולם הג'אווהסקריפט. פרויקטים מפורסמים שהושפעו ממנו כוללים את CouchDB, Node.js ו MongoDB.

ServerJS הצליח כל-כך, עד שגם הדפדפנים (שלהם הפרויקט לא-יועד) החלו לממש רעיונות מתוך ServerJS, בהתאמה קלה לעולם הדפדפנים. אנשי ServerJS קיבלו את האורחים החדשים, ושינו את שם הפרויקט ל: "CommonJS". כלומר: הפרויקט של כ-ו-ל-ם.

עם אלו בעיות ServerJS מנסה להתמודד?
  • הגדרה של מודולים (modules) וחבילות (packages) בכדי לארגן את הקוד. חבילות הן קבוצות של מודולים.
  • כתיבת בדיקות-יחידה.
  • כלי עזר לכתיבת קוד אסינכרוני כך שהקוד יישאר מודולרי  - אותו כיסיתי בפוסט מקביליות עם jQuery (ובכלל) (תקן ה Promises/A, היחסית-מפורסם)
  • עבודה עם מערכות קבצים.
  • טעינה דינמית של קוד.
  • ועוד כמה...
אחד התקנים בעל ההשפעה הרבה ביותר הוא תקן בשם Modules/1.1, תקן המתאר כיצד להגדיר מודולים. צורך זה הוא בסיסי מאוד והרבה frameworks משתמשים ב "CommonJS Sytle" (כלומר - בתחביר של התקן, או כזו שדומה לו מאוד) על מנת להגדיר מודולים של קוד ג'אווהסקריפט.

אנשי הדפדפנים, התאימו את Modules/1.1 לעולם הדפדפן (ישנם כמה הבדלי התנהגות חשובים) וקראו לו: Async Module Definition, או בקיצור: AMD [א].

רק להסביר: AMD היא הגדרה המתארת API לטעינה דינמית של מודולים - אך אין מאחורי AMD קוד. ל AMD יש מימושים רבים בדמות ספריות כמו: lsjs, curl, require, dojo ועוד.

המימוש הבולט ביותר ל AMD היא ספרייה בשם require.js.
כיום require.js היא הספרייה הנפוצה ביותר, בפער גדול, על שאר האלטרנטיבות. המצב מזכיר במעט את המצב של jQuery מול MooTools או Prototype - תקן "דה-פאקטו".



היתרונות של AMD (בעצם: require.js)

מלבד היכולת להפציץ חברים לעבודה במושגים (כמו CommonJS, AMD או Modules/1.1), תבנית-העיצוב AMD מספקת יתרונות משמעותיים לאפליקציות גדולות. מרגע זה ואילך אתייחס ספציפית ל require.js, או בקיצור: "require".

#1: ניהול "אוטומטי" של תלויות בין קובצי javaScript
האם קרה לכם שהיה לכם באג בקוד שנבע מסדר לא-נכון של תגיות ה <script> ב head של קובץ ה HTML?
קרוב לוודאי שבמערכת גדולה יהיו מספר רב של קובצי javaScript ולכן - מספר רב של תלויות. אין דרך ברורה להגדיר קשרים בין קבצי javaScript (כולם נרשמים במרחב זיכרון משותף), כל שקשרים אלו הם לא-מפורשים ואינם קלים לתיעוד או למעקב.

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

הערה קטנה: require לא נבנתה לנהל קשרים בהם יש cycles, אולם יש "טכניקה" בה ניתן לטעון קבצים עם תלות מעגלית - אם כי בצורה מעט מסורבלת.


#2: טעינה עצלה ומקבילית של קבצי javaScript [ביצועים]
Require מנצלת את היתרון שהגדרתם כבר את התלויות בין הסקריפטים לא רק בכדי לטעון אותם בצורה נכונה, כי גם בכדי לטעון אותם בצורה אופטימלית מבחינת ביצועים.
  • Require לא תטען קובץ עד לרגע שצריך אותו בפועל (lazy loading).
  • Require טוענת קבצים בעזרת תכונת ה async של תגית ה <script> - משהו שכמעט בלתי-אפשרי לנהל באופן ידני בפרוייקט גדול.

טעינה דינמית של קבצים מפגישה 2 כוחות מנוגדים:
מפתחים - שמעוניינים בהרבה קבצים קלים בהם קל לנהל את הקוד.
אנשי production / operations - שרוצים שיהיו מינימום roundtrips לשרת.

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


#3: ניהול תלויות בין קובצי ה javaScript השונים
שני היתרונות הקודמים הם בהחלט חשובים, אך הפאנץ' ליין נמצא כאן, לטעמי.
כאשר אתם מגדירים תלויות בין מודולים - require תסייע לכם לאכוף את התלויות הללו ולוודא שאינכם "עוקפים" אותן.
משהו שבשפות אחרות היינו מקבלים כמשפט "import" או "include" ולעתים היה נראה כמעמסה בעת כתיבה - מתגלה כחשוב מאוד כשהוא חסר.

הניסיונות שלי לנהל פרוייקטים בעזרת namespaces במרחב הגלובלי (בצורת {} || var myns = myns) נגמרו לבסוף בעשרות תלויות בלתי-רצויות בקוד ש"הזדחלו" מבלי שהרגשנו. ברגע שרצינו להשתמש במודולריות של הקוד, כפי שתכננו - לא יכולנו לעשות זאת ללא refactoring משמעותי.

מה שווה MVC, אם ה"מודל" מפעיל פונקציות שלא היה אמור מתוך ה "View"??
מה שווה חלוקה ל Layers, אם היא לא נאכפת בפועל??

Require תסייע לכם לוודא שהקוד אכן מיישם את ה design שתכננתם.







מבוא קצר ל Require.js


חטא נפוץ הוא להציג את require.js כספריה קטנטנה ונטולת-מורכבות. מדוע חטא? מכיוון שהרשת מלאה במדריכי "hello world" ל require, המציגים רק את היכולות הבסיסיות ביותר. אחרי כמה שעות עם עבודה ב require קרוב לוודאי שתזדקקו ליותר - אך ידע זה קשה להשגה. התיעוד הרשמי של require הוא טכני ולא הדרגתי - ממש כמו לקרוא מסמך Specification. מתאים בעיקר למי שכבר מתמצא.

התוצאה: עקומת למידה לא קלה לשימוש ב require - וללא סיבה מוצדקת.

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

אני אתחיל בחטא, אך אתקן אותו בהמשך.



מבוא נאיבי ל Require.js


Require היא ספריה פשוטה וחמודה.

היא מציגה בסה"כ 3 פקודות:
  • define - הגדרה של מודול (יחידת קוד של ג'אווהסקריפט בעלת אחידות גבוהה ותחום-אחריות ברור).
  • require - בקשה לטעינה של מודול בו אנו רוצים להשתמש.
  • require.config - הגדרות גלובליות על התנהגות הספרייה.

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


ModuleID הוא מזהה טקסטואלי שם המודול. ה Id בעזרתו אוכל לבקש אותו מאוחר יותר.
את הקוד של המודול כותבים בתוך פונקציה, כך שלא "תלכלך" את המרחב הגלובלי (global space).
קונבנציה מקובלת ומומלצת היא לחשוף את החלק הפומבי (public) של המודול בעזרת החזרת object literal עם מצביעים (ורוד) לפונקציות שאותם ארצה לחשוף (טורקיז). כמובן שאני יכול להגדיר משתנים / פונקציות נוספים שלא ייחשפו ויהיו פרטיים.

מבנה זה נקרא "Revealing Module" והוא פרקטיקה ידועה ומומלצת בשפת javaScript. ספריית require מסייעת להשתמש במבנה זה. דיון מפורט במבנה זה ניתן למצוא תחת הפסקה "הרצון באובייקטים+הכמסה = Module" בפוסט מבוא מואץ ל JavaScript עבור מפתחי Java / #C מנוסים - חלק 2.



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


הפונקציה בדוגמה היא callback שתפעל רק לאחר שהקוד של מודולים 1 ו2 נטען ואותחל. m1 הוא reference ל מודול1 (אותו object literal שהוחזר ב return וחושף את החלקים הציבוריים) ו m1 הוא reference למודול2. השיוך נעשה ע"פ סדר הפרמטרים.

doStuff היא כבר סתם פונקציה שעושה משהו עם m1 ו m2.

בפועל, רוב הפעמים יהיו לנו קוד שגם:
  1. מגדיר מודול.
  2. וגם תלוי במודולים אחרים.

משהו שנראה כך:


קוד זה הוא מעט מסורבל, ויותר גרוע - טומן בתוכו חשיפה לאופי האסינכרוני בו טוענת require את קובצי ה javascript. ייתכן וה return יופעל לפני ש doStuff הוגדרה - מה שיחזיר undefined כמצביע ל doStuff לקוד שביקש אותו.

כתיבת קוד אסינכרוני שתבטיח שה return יופעל רק לאחר ש doStuff הוגדרה תוסיף עוד מספר שורות קוד - ותהפוך את קטע הקוד למסורבל עוד יותר. על כן require (בעצם AMD) הגדירה תחביר מקוצר למצב של מודול שתלוי בקוד אחר. זהו בעצם המצב הנפוץ ביותר:


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

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

הדרך המקובלת ביותר לטעון את require ב HTML היא באופן הבא:


שימו לב שגם בפרויקט גדול, אין צורך להגדיר ב HTML יותר מסקריפט אחד: require. הוא כבר יטען את כל השאר.
data-main הוא שם קובץ ה javascript של פונקציית ה "main" שלנו שמאתחלת את התכנית. יש להקליד את שם הקובץ ללא סיומת .js.


זוכרים שיש פקודה שלישית? config? - היא לא כ"כ חשובה. 
היא משמשת להגדרות גלובליות מתקדמות לגבי ההתנהגות של require. למשל קטע הקוד הבא:


קוד זה מגדיר שאם קובץ לא נטען (ברשת) תוך 10 שניות, require יוותר ויזרוק exception. מצב זה סביר בעיקר כאשר אתם טוענים קובץ מאתר מרוחק. ה default הוא time-out של 7 שניות.


זהו, סיימנו!

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


סיכום

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

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




----

[א] למען הדיוק אפשר לציין ש AMD התחיל כ Modules/Transport/A (תחת קורת הגג של CommonJS, אם השם המוזר לא הבהיר זאת) - אך הוא נזנח תוך כדי עבודה. כרגע מנסים להחזיר אותו חזרה "הביתה" ל CommonJS בדמות התקן Modules/AsynchronousDefinition, בעיקר על בסיס העבודה שנעשתה ב AMD.


9 תגובות:

  1. מעולה כתמיד.
    ראוי לציין שעד כמה שזה נוח בסביבת פיתוח, ב-production מדובר על הרבה מאוד קריאות לשרת ככל שהמודולים נטענים, אז מומלץ להשתמש בכלי שיבצע build - כלומר יבחן את ה-dependencies ויכין קובץ JS אחד מקומפרס. (לדוגמה Google Closure Compiler).

    * כמובן שאני מדבר על פיתוח לדפדפן

    השבמחק
    תשובות
    1. תודה על התגובה.

      מסכים לגמרי.

      r.js (שהזכרתי למעלה) מבצע גם איחוד לקובץ גדול ע"פ ניתוח התלויות וגם מבצע minification. היתרון היחסי שלו הוא שהוא יודע לקרוא את התלויות שהוגדרו ב require, כל שהוא יכול לאחד 70% מהקוד, וששאר הקוד (שלא תמיד נדרש) ייטען באופן דינאמי.

      ליאור

      מחק
    2. צודק, פספסתי את החלק הזה במאמר.

      מחק
  2. אנונימי28/4/13 11:41

    אם מדובר בsingle page app, מודול מכיל בנוסף ל javacript גם css ו hmtl. איך מתמודדים עם זה?

    השבמחק
    תשובות
    1. נקודה טובה:

      ישנו Plugin ל require שנקרא text.js שיכול לטעון דינמית גם קובצי css וגם snippets של HTML (אני מניח שדיברת על templates. אם אתה מעוניין קובץ HTML ראשי אחר אזי פקודת location.replace בג'אווהסקריפט תעשה את העבודה).

      הנה הלינק ל text.js, אנסה לכסות אותו בפוסט ההמשך:
      https://github.com/requirejs/text

      ליאור

      מחק
    2. אנונימי28/4/13 17:10

      רוב תודות :)
      הצלחת לסקרן אותי, אז חיפשתי ונתקלתי ב curl, שגם היא ספריית amd עם טיפול טוב גם ב css ו טמפלטים ומה לא :
      https://github.com/cujojs/curl

      מחק
    3. curl, עד כמה שאני יודע היא המימוש השני הכי נפוץ של AMD אחרי require.
      השורשים של curl, כמו require, הם מספרייה שנקראת dojo - ולכן לא אתפלא אם הן ממש חולקות קוד.

      סה"כ נראה של require יש קהילה גדולה יותר ומספר גדול יותר של כלים / ספריות בעלות אינטגרציה ספציפית ל require. כרגע עולה לי בראש Karma - אך אני בטוח שיש עוד.

      ספריה ידועה אחרת היא lsjs (אם כי אינני מנסה לעשות השוואה בין הספריות השונות). אם כוללים ספריות התואמות ל CommonJS וספריות לטעינה דינאמית של משאבים ללא תאימות מיוחדת - המבחר הוא גדול מאוד.

      ליאור

      מחק
  3. אנונימי29/4/13 04:59

    מעניין אותי לדעת, איך נראה amd בjs טהור, בלי ספריה?

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

      המימוש הכי פשוט ו"נקי" של AMD שידוע לי הוא אלמונד:
      https://github.com/jrburke/almond
      הקוד קצר יחסית וקריא - אתה יכול פשוט לעבור עליו.

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

      ליאור

      מחק