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

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

הצצה מהירה על Backbone.js (ספריית MVC)

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

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

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

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

סמל, שאם תכתבו ב Backbone.js – תיתקלו בו עוד הרבה…

רקע

לא ניתן להתעלם מתרומתה הרבה של ספריית jQuery ודומותיה[ב] בעיצוב מקומה החדש של שפת ג׳אווהסקריפט. במעגל השני של ההשפעה (לפחות בצד-הלקוח) נמצאות ספריות שעוזרות בכתיבת אפליקציות צד-לקוח גדולות. ספריות אלו נקראות ״MVC Frameworks” או ״ספריות ארכיטקטוניות״. השם MVC עשוי לבלבל את מי שכבר מכיר ועבד עם ספריות MVC צד-שרת כגון Struts או ASP.NET MVC.

בעוד ספריות “MVC קלאסיות” מטפלות באפליקציות רבות דפים והניווט בניהם, ספריות ה “JavaScript MVC” לרוב מטפלות באפליקציות דף-יחיד (כגון GMail) בהן יש מסך עקרי אחד שחלקים בו מתחלפים / משתנים – אך אין ניווט משמעותי. בכלל, עצם הפעולה בצד-הלקוח, ב Ajax, בו אין refresh לדף בעת פעולה – משנה את כללי המשחק: מציב בעיות חדשות ופתרונות אחרים. עוד הבדל בולט הוא שברוב ספריות ה “JavaScript MVC” אין Controller. השם MVC בא לבטא את הרזולוציה ואת הבעיה שהן באות לפתור: ארגון קוד באפליקציית UI גדולה ומורכבת.

קיימות כיום מספר רב של ספריות “JavaScript MVC” וצפוי בהן עוד תהליך של קונסולידציה. שלושת הגדולות הן:

  • Backbone.js כנראה הנפוצה והבוגרת מכולן. מספקת מסגרת כללית בלבד ומשאירה חופש / חללים למפתח להחליט ולמלא בעצמו.
  • Knockout.js ששמה במרכז את ה Data Binding ומאפשרת לכתוב בקלות אפליקציות “מונחות-נתונים” (מערכות מידע וכיוצ”ב) נפוצה במיוחד בעולם ה NET.
  • Ember.js (לשעבר Amber.js), ספרייה מקיפה שכוללת גם Class System[ג] ושואבת רעיונות רבים מ Rails ״כתוב מעט קוד, אך עשה זאת בדרך שלנו” צעירה יחסית – אך זוכה למומנטום משמעותי. נפוצה במיוחד בקרב מתכנתי רובי.

ספריות משמעותיות אחרות הן Spine.js, JavaScriptMVC, Batman.js ו AngularJS (מבית גוגל). האתר המצוין todoMVC מציע השוואה מעמיקה בין האפשרויות בכך שמימש אפליקציית דוגמה בכל אחת מספריות הנ”ל (ועוד כמה ספריות נוספות).

רגע של התפלספות

כפי שציינתי, ספריות “JavaScript MVC” הן לא בדיוק MVC קלאסי. בעצם, גם ל”MVC קלאסי״ יש כמה פרשנויות שונות (לדוגמה, האם ה Controller מתמקד בעיקר בטיפול ב input או אחריות על ניהול הflow והתלויות?[ד]).

המדקדקים מגדירים את Knockout.js כ MVVM) Model, View, View-Model) – תבנית עיצוב שמייקרוסופט מקדמת בעולם הNET. זה דיי נכון.
את Backbone.js ו Ember.js נוהגים להגדיר כ Model, View, Presenter) MVP). האמת, ספריות כמו ASP.NET או GWT מתאימות יותר להגדרה זו. הגדרה כגון Model, View-Controller, Router (בקיצור MVCR) תהיה יותר מדויקת.

ההגדרה המועדפת עלי כרגע היא !Model, View, Whatever (בקיצור MVW או *MV).
התחלתי את הפוסט הזה בניסיון לעקוב אחר ההיסטוריה של תבנית העיצוב MVC, הוריאציות והשינויים שהיא עברה במשך השנים, בעיקר MVP ו MVVM, והניסיון להסביר כיצד ספריות ה “JavaScript MVC” מתאימות לתבניות אלו.

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

בסופו של דבר – MVC הוא ניסיון לארגן אפליקציית UI מורכבת בצורה שתהיה קלה לתחזוקה ושינויים. כל דור של UI (Web, Fat Client, Fully Client Side, Command Line) – והאתגרים שלו.
כפי שמרטין פאולר ציין פעם: “אנשים נוטים להסתבך עם MVC. החלק הכי חשוב הוא לבצע הפרדה אמיתית בין Business Logic ל UI. כל השאר – משני”. אני מסכים ב 100%.

לכל ספרייה יש את הדרך שלה לארגן את הקוד – ואולי יותר מעשי ומעניין פשוט להתבונן בישומים קונקרטיים.
בפוסט זה אתן הצצה בסיסית ביותר ל MVW בג׳אווהסקריפט בדמות Backbone.js – הספרייה הנפוצה והמינימליסטית שיכולה להיות בסיס לימודי מצוין להבנת MVC בג׳אווהסקריפט.

אז מה יש ב Backbone.js?

אקצר מעתה ואקרא ל Backbone.js פשוט BB.
  • BB היא קטנה למדי. 700 שורות קוד בערך. 1400 שורות עם הערות.
  • BB משתמשת ב underscore.js (ספרייה טובה ל javaScript Utils מאותו היוצר) ו jQuery. אם אתם עובדים עם BB – יהיה לכם מאוד נוח להשתמש גם כן בספריות אלו, וקצת פחות נוח (אך אפשרי לחלוטין) להשתמש בספריות אחרות. BB מעודדת שימוש בספריית Templating – ואתם יכולים לבחור את זו של underscore (תחביר JSP-like, שלי אישית, עושה צמרמורת) או כל אחת אחרת.
  • ל BB יש תיעוד טוב (reference) ו Tutorials סבירים. יש קהילה גדולה ופעילה.
  • בגלל ש BB פשוטה – מייחסים אותה כמתאימה למערכות פשוטות יחסית – אך בפועל יש גם מערכות גדולות מאוד ומורכבות שנכתבו בעזרתה.
  • מאוד קל לעבוד עם BB מול שרת שמספק REST APIs – במיוחד אם תבנית-הנתונים היא JSON.
  • ל BB יש ארכיטקטורת Plug-Ins וניתן למצוא Plug-Ins רבים שמרחיבים את יכולות-הבסיס שלה.
  • BB מספקת מסגרת לא-מחייבת. אם אתם רוצים לפעול קצת אחרת, יש לכם את החופש לעשות זאת. כאשר מדובר במבנה של האפליקציה שלי – אני אוהב את זה.
  • כמו ספריות “JS MVC” אחרות, BB מספקת:
    1. מערכת-מחלקות (Class System) עבור Model ו Views (וגם Routers ו Collections – שהם רכיבים מרכזיים אחרים בBB)
    2. ניהול של המודלים (ה instances), כולל הקשרים בניהם.
    3. כלים לאימות ערכים (Validation Logic) במודל.

הנה דוגמה למודל מינימלי שניתן לייצר ב BB:

הערה: משפטי ה expect…toBe הן בדיקות-יחידה המציגות את התנהגות הקוד. כל הבדיקות עוברות בהצלחה.

המודל שלנו, Person בדוגמה זו, הוא פשוט למדי. הייתי יכול ליצור אותו גם ללא defaults – רק עלי ידי הורשה ריקה מהמחלקה Backbone.Model. תכונות המודל (כמו שם פרטי/משפחה) הן דינמיות ומוגדרות בזמן יצירת האובייקט הספציפי. ה default היא היכולת לחייב שתכונות מסוימות יהיו בכל מקרה.

ב1 – אנו יוצרים מודל של אדם בשם אוזי כהן.
ב2 – בכדי להדגיש את השוני בינו ובין אמן אחר בשם ״ליאונרד״ ובעל שם משפחה זהה, אוזי משנה את שם המשפחה שלו.
ב3 – אנו מקבלים את המודל המלא בפורמט JSON.

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

השתמשתי במודל הכי פשוט שאפשר – כי ברצוני להתמקד ב View.

PersonView היא המחלקה שאחראית לצייר מודל של Person. יכולים להיות, כמובן, כמה Views שונים לאותו המודל. את ה markup שה View יוצר, ארצה לשלב היכנשהו ב DOM. בדוגמה זו – זה יהיה container$, שמשתמש ב span בכדי לא לבלבל עם ה div ש Backbone יוצר באופן אוטומטי לכל מופע של המודל.

el הוא בעצם אלמנט ה DOM שיצר לי BB כחלק מה View. הוא עושה זאת על מנת לאפשר ל markup בפנים להשתנות, בלי שאצטרך לחבר את ה markup שוב ל container. הבחירה ב div היא כמובן רק default, ואם אני זקוק ל list element (תג

  • ) – אפשר לקבוע זאת.ב1- אני בודק מה יש בתוך ה container – ניתן לראות את ה markup שנוצר בתוך הפונקציה render. זו דוגמה דיי מכוערת – לרוב נשתמש בספריית templating על מנת שהקוד שיוצר את ה html snippet יהיה אלגנטי. לא רציתי להעמיס על דוגמה זו ולכן הקוד ה”פרימיטיבי”.

    ב2- אנחנו בוחנים את ה markup כולל ה container. רק לוודא שהתמונה הכללית ברורה.

    ב3- אנחנו מבצעים שינוי למודל ורואים שה markup ב container השתנה באופן פלאי. “פלאי” – כמובן שלא. אנחנו כותבים את הקוד שעושה זאת – backbone רק מכווין אותנו למבנה מסוים ועוזר לנו.

    בואו נבחן את הפונקציה initialize שהיא לצורך העניין הבנאי (constructor) של מחלקת ה PersonView שלנו, וכוללת 2 משפטים מוזרים:
    ב5- אנחנו מבצעים פעולת JavaScripts’s bind לכל המתודות במחלקה שאנו הולכים ל”ייצא” כ callbacks לאובייקטים אחרים. במקרה זה – יש רק את ״render״, אך יכולתי באותה מידה לשרשר גם שמות של מתודות נוספות. למה צריך לעשות JavaScript bind? – כדי ש this ימשיך להצביע לאובייקט שלנו. מבולבלים? קראו את הפוסט להבין את JavaScript this.
    הסימן “_” (קו תחתון) הוא הקיצור לגשת לספרייה underscore.js, ממש כמו שניגשים עם $ ל jQuery. אלו המקומות ש underscore מסייעת מאוד לכתוב קוד ב BB. אתם לא חייבים – אבל כדאי.

    ב6- אנחנו “מחייטים” את ה View שלנו לבצע פעולת render כל פעם שהמודל משתנה (change’ event’). מאוד דומה לאירועים ב jQuery.
    הבהרה קלה: אם אתם מכירים את תבנית העיצוב MVC אתם יכולים להבחין שה View ב Backbone מבצע פעולות (“חיוט”) של Controller. זה נכון: אפשר לומר שה View ב BB הוא בעצם View-Controller: הוא עושה הרבה פעולות שבמקור יוחסו ל Controller.
    אם אתם מכירים את תבנית העיצוב MVP נראה שה View הוא בעצם יותר Presenter. הוא אחראי ל Presentation Logic. זה נכון: בעצם מה שנקרא “View” ב BB הוא דומה ל MVP’s Presenter בעוד ספריית ה Templating (בעצם ה Template עצמו) – דומה מאוד ל MVP’s View.

    סיכום

    הצצנו ל Backbone וטעמנו כיצד נראה קוד שמשתמש ב Backbone.js. טעימה קטנה.
    דיברנו גם על החשיבות של ספריות “JavaScript MVC” – כאשר קוד הג’אווסקריפט שלכם מתחיל לגדול ולהסתבך. וגם על החשיבות לא לחפור בתבנית-העיצוב MVC וההיסטוריה שלה יותר מדי 🙂

    ייתכן ואמשיך לכתוב קצת על Backbone על מנת להתעמק בו קצת יותר, בניסיון לבחון JavaScript MVC “מהשטח”.

    בהצלחה!

    ——

    [א] נקראת Harmony, ג׳אווהסקריפט 2.0 או ECMAScript 6.0. כוללות, ככל הנראה, מחלקות, מודולים (חבילות של מחלקות עם ניהול תלויות), תיקונים לכמה כשלים של שפת ג׳אווהסקריפט ועוד. ממש מהפיכה!

    [ב] Prototype ו MooTools היו גם הן מועמדות ראויות, אך קהל המפתחים בחר לבסוף בjQuery. ניתן לקרוא על העיקרון מאחורי jQuery בפוסט מבוא מואץ ל jQuery.

    [ג] כלים / Framework להגדרת מחלקות וכל מה שקשור בהן. בג’אווהסקריפט, כפי מתואר בפוסט מחלקות בג’אווהסקריפט. בשפת ג’אווהסקריפט לא קיימות מחלקות – ועל המתכנת לייצר אותן לבד. בעוד ספריות “JavaScript MVC” רבות מספקות סביבה אחידה / מוגנת ליצירת מחלקות עבור האלמנטים הבסיסיים בספריה (כמו Model או View).- ספריית Ember מספקת כלים שתוכלו להשתמש בהם עבור כל המחלקות בפרויקט שלכם.

    [ד] המקור של MVC הוא בשפת Smalltalk בשנות ה-70. כשהגיעו ה Fat Clients (ב ++C), ולאחר מכן הווב – נוצרו פרשנויות ווריאציות שונות ל MVC על מנת להתמודד עם האתגרים החדשים שהציבו טכנולוגיות אלו.