יום שני, 26 בנובמבר 2012

Backbone.js (ספריית MVC) - אוספים

בפוסט הקודם, הצצה ל Backbone.js (בקיצור BB, ובלי לדבר על פוליטיקה) ראינו את שני הטיפוסים המרכזיים ביותר של ספריית BB. בפוסט זה ארצה להרחיב מעט את היריעה לטיפוס מרכזי נוסף שנקרא Collection שמסייע לארגן את המודלים במערכת.
להזכיר: מודל (model) מייצג אובייקט המתאר "יישות בעולם בו עוסקת המערכת", קרי Domain Object. לעתים קרובות נרצה להציג / לטפל בסדרה של מודלים. BB מאפשר ותומך בצורך זה בעזרת Collection שהוא אוסף של מודלים, המספק יכולות מתקדמות יותר מרשימה פשוטה.

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


דוגמה בסיסית

בואו נפתח בדוגמה פשוטה:

אתם יכולים לזהות מודל מינימלי בשם Person ואת האוסף (Collection) בשם People - אוסף של Person.

1 - ההגדרה של model בבנאי של האוסף אינה נדרשת בשלב זה. היא משמשת רק כאשר משתמשים ביכולות ה REST המובנות ב Backbone.Model ו Backbone.Collection על מנת לסנכרן שינויים לשרת. איננו עוסקים ביכולת זו עדיין, אך אני מעדיף להגדיר את המודל לצורך הקריאות. הקונבנציה המקובלת ב BB היא לקרוא לאוסף בשם הרבים (s בסוף) של המודל, למשל Invoice ו Invoices.

2 - אנו יוצרים מודל (בצורה נאיבית, אציג דרכים יעילות יותר בהמשך) של מג'יק ג'ונסון ומוסיפים אותו לאוסף.

3 - לכל מודל (בעצם לכל אובייקט של BB) שנוצר ניתן מזהה גלובלי ייחודי, בפורמט "<מספר רץ>c". במקרה זה אני יודע שאובייקט של מג'יק ג'ונסון הוא השלישי שנוצר במערכת ועל כן אני יכול לאמת את ה cid שלו. זו הנחה שלא הייתי רוצה להסתמך עליה בקוד אמיתי.
cid הוא קיצור של Client Identifier, שנועד לסייע לזהות מופע של מודל בצורה ייחודית. ב BB מניחים שצד-השרת, בעקבות השימוש בבסיס נתונים כלשהו, יהיה לכל אובייקט זיהוי ייחודי. בטווח הזמן בו האובייקט נוצר בצד-הלקוח ועדיין לא נשמר בבסיס הנתונים וקיבל Id ייחודי מבסיס הנתונים, ה cid אמור להיות כלי העזר על מנת לזהות ייחודית את המודלים.

4 - הוספנו עוד מודל אחד לאוסף.

5 - אוסף של BB, באופן מובנה, יכול להשתמש utilities של underscore.js - שהם דיי שימושיים. הנה דוגמה של פונקציה בשם plunk שעוברת על כל אחד מהמודלים באוסף, ומכניסה לתוך מערך התשובה ערך של שדה שהוגדר, במקרה זה - כל שמות המשפחה. כמה נוח.
כן, underscore.js "מתעסק לנו" באובייקטים בסיסיים של השפה כמו "Object" או "Array"', במקרה זה הוא הוסיף את plunk ומתודות אחרות למערך. נכון, זה משהו שממליצים לא לעשות. לא לעשות באפליקציה - אך זה "מותר" לספרייה. תתרגלו.


Collection ו Views

הנה דוגמה מורכבת מעט יותר המציגה חיבור של Collection ל View:
View אחד עבור כל Person, ועוד View עוטף של האוסף People.

התוצאה, עבור אוסף מינימלי של 2 אנשים, תראה משהו כזה:
הכותרת "שני אנשים" שייכת לPeopleView, והשאר נוצר ע"י שני מופעים של PersonView. בלי שום Styling כמובן - בכדי לצמצם את גודל הדוגמה.

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

ב1 - אנו יוצרים אוסף בשם people ומזינים אותו בערכים. המשמעות של reset היא "החלף את כל הערכים הנוכחיים ברשימת הערכים הבאה" והיא האופן בו נרצה להזין ערכים כאשר אנו טוענים מצד-השרת רשימה מלאה של ערכים. אנו מזינים את people במערך של אובייקטים שכל אחד מתאר את המודל של הרשימה - Person.

ב2 - אנו יוצרים את ה PeopleView, ה View שיודע להציג את הרשימה וליצור את ה HTML הנ"ל. נראה את הקוד שלו מייד.

ב3 - כך מבצעים בדיקת-יחידה יציבה (לא-שבירה) עבור תוצאת HTML. טעות נפוצה היא להשוות את כל תוצאת ה HTML כ String לערך שהיה נכון בסוף רגע הפיתוח ובחנו שהוא תקין. הבעיה: כל שינוי ב markup - אפילו ריווח או תוספת תג לא משמעותית (למשל label) תשבור את הבדיקה - גם כאשר התוצאה עדיין תקינה. הדרך הנכונה לבדוק היא לאתר "עוגנים חשובים" ב HTML ולבדוק רק אותם. האומנות היא למצוא את רמת הפירוט האופטימלית.
אנו מאמתים את טקסט הכותרת
אנו מאמתים את מס' האלמנטים מסוג h2
אנו מאמתים שהטקסט של הראשון הוא "ג'ונסון, מגיק" - כלומר את הפורמט. eq היא המקבילה של jQuery ל index.
אנו מאמתים שאחרי ("~" ב css selector) הכותרת h2 הראשונה ישנו אלמנט p שמכיל את כתובת האימייל של זיגי. איננו רוצים "להפיל" את הבדיקה אם מחליפים את התחילית "email" באייקון קטן, למשל. אנו מתמקדים בנתונים ההכרחיים.

ב4 - אנו מבצעים reset לנתונים - עם רשימה באורך 3, רק כדי לוודא שהכותרת הראשית (h1) אכן תלויה באורך הרשימה.

הנה הקוד עצמו:
ההתחלה אמורה להיות מוכרת. פרקטיקה טובה היא להוסיף ערכי defaults למודל עבור כל שדה שאנו הולכים להשתמש בו ב View - אחרת אנו עשויים להיתקל ב Runtime Errors ללא fallback (למשל כשיש מודל עם age בלבד).

1 - לא התאפקתי והשתמשתי הפעם ב template engine - שהיא הדרך המומלצת ב BB כדי לייצר את ה markup עצמו. השתמשתי בצורה בסיסית ביותר בספרייה המובנה בתוך underscore.js (אתם יכולים לזהות ע"פ תחביר ה ERB/JSP), אבל אין שום מניעה לעבוד עם ספרייה אחרת כמו handlebars.js, mustache.js או dust.js.
אתם יכולים לשים לב שב template.people יש קו על התגית <u>. הסיבה: התגית <u> היא deprecated ב HTML5. הדרך ה"נכונה" ע"פ HTML5 לייצר קו תחתון הוא לעטוף ב span עם class שלי ואז להחיל עליו קו-תחתון בעזרת CSS. לצורך דוגמה זו אתעלם מזוטות שכאלה.
עוד נקודה מעניינת היא השימוש ב class (נקודה) ולא id (סולמית) על מנת לזהות את החלק שבו "יוזרקו" חתיכות ה markup של ה Person הבודד. על Id ב HTML להיות ייחודי בכל הדף ואיני יודע בוודאות אם ה PeopleView יוצג יותר מפעם אחת על הדף או לא. סיכוי אחר: אולי מישהו אחר ישתמש בid בשם people-list במקום אחר. על מנת להישאר סטנדרטיים יש להימנע משימוש ב id אלא אם אפשר לוודא שהוא אכן ייחודי - מה שלרוב קשה לדעת. הפתרון: שימוש ב  css class לזיהוי.
בכל מקרה, אחפש את ה css class בצורה יחסית - כך שלא אשפיע על אזורים אחרים ב markup, גם אם הם השתמשו באותו css class בדיוק.

גישה פופולרית היום היא להקליד את ה templates עצמם בתוך ה HTML עצמו עטופים בתג <'script type='text/template> ועם id ידוע מראש ואז להשתמש ב jQuery על מנת לטעון את ה template בזמן ריצה. ה template כמובן אינו משפיע על ה HTML - הוא רק "יושב שם מבלי להפריע". מדוע לעשות זאת?
  • כדי ליהנות בעת כתיבת ה template מה HTML Editor עם Syntax Highlighting ובדיקת שגיאות.
  • הצמדות לכלל שאומר: makrup נמצא בקובץ ה HTML - לוגיקה ב javaScript. כל ספריות ה template engine שציינתי למעלה הן "logic-less" ואינן מאפשרות כתיבת javaScript בתוך ה template, אלא רק לוגיקה רק בשפה הפנימית שלהן. היה נכון יותר לקרוא להן "javaScript-less", בעצם.

מדוע, אם כן, אני מעדיף בכל זאת לשמור את ה templates בתוך ה javaScript code?
  1. אני משתמש בIDE בשם WebStorm שמספק לי את היתרונות של HTML Editor בתוך קובצי הג'אווהסקריפט. החיסרון היחיד: חוסר החופש לייצר שורה חדשה ללא +.
  2. כתיבת ה templates בתוך ה javaScript מאפשרים לי להריץ את בדיקות-היחידה ללא קובצי HTML - מה שמאפשר להם לרוץ הרבה יותר מהר. התוצאה: אני מריץ בדיקות בצורה תכופה הרבה יותר - כי כולן רצות בשתי שניות בתוך ה IDE.

2 - ביצירה של ה PersonView אני מייצר את ה"אוטומט" של ה template. על מנת להשיג יעילות מרבית, ה template engine נוהגים לפרק את ה string של ה template לצורה מהירה יותר לשימוש. לרוב מקובל לקרוא למשתנה שמחזיק את התבנית פשוט "template", השתמשתי ב myTemplate על מנת לנסות ולהקל על ההבנה במקרה זה.

3 - כאן מבצועת ההפעלה של ה"אוטומט" של ה template engine על נתונים מתאימים, במקרה זה - המודל המדובר. ה"אוטומט" myTemplate הוא בעצם פונקציה שמרנדרת html markup ברגע שמזינים לה נתונים. אנו טוענים את התוצאה לתוך this.el שהוא, כאמור, העטיפה ב DOM ל View שלנו.

בואו נעבור עכשיו ל PeopleView המייצג את התמונה הכוללת:
4 - לאחר שרינדרתי את החלק של People ב HTML, הרי היא ה"מסגרת" (במקרה שלנו - רק כותרת), אני רוצה להזין את הנתונים של המודלים הבודדים. אני קורא ל $.this על מנת לבצע חיפוש בתוך ה markup של this.el בלבד, אחר המקום בו "אזריק" את ה Views של המודלים הבודדים. זוהי גרסה מקוצרת (ומומלצת) לכתיבת "(this.el).find('.people-list')$" (אני מניח שאתם מכירים jQuery).

5 - each של collection מאפשרת לי להריץ פונקציה עבור כל מודל באוסף - משהו כמו foreach.

6 - או יוצרים מופע של PersonView עם המודל ה person הנוכחי. אולי זה מרגיש קצת "כבד" לייצר View כל פעם ולא לייצר אחד ולעשות בו שימוש חוזר - אך זה בסדר.

7 - אנו משתמשים ב view על מנת לרנדר את ה markup ומוסיפים את ה "glueing markup", שבמקרה זה הוא רק תגית br. אנו רוצים לשמור את ה template של Person "נקייה" כך שנוכל להשתמש בה גם ללא ה PeopleView.


סיכום

בפוסט זה צללנו לדוגמה ריאלית קצת-יותר (מהפוסט הקודם) של קוד ב backbone - על מנת להבין כיצד קוד ב BB "מרגיש". אני מקווה שהצלחתי להעביר את התחושה, אם כי היא עדיין מינימלית למדי. כפי שראינו, BB לא עושה "קסמים" (כמו Ember או Angular) - אנו עדיין כותבים את כל הקוד, אך BB נותן לנו מסגרת, guidance ו utilities על מנת להגיע לשם.
על הדרך השלמנו את הפער אודות השימוש ב template engine, שהוא היבט מרכזי בספריות "javaScript MVC", והדגמתי כיצד לבצע בדיקות-יחידה ל View וראינו Views מקוננים. כל זה דחוס בפוסט לא כל-כך ארוך. אני מקווה ששמרתי על איזון נכון של "לא משמעמם" ו "ניתן להבנה".

הערות יתקבלו בשמחה!


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



2 comments: