ניסוי בטכנולוגיות צד-לקוח (React.js)

פוסט זה נכתב בשיתוף פעולה של ניר בנימין, איתמר שגב ושלי.
הוא מסכם ניסיון של 3 ימים בכתיבה בראקט, שהתרחשה במסגרת \"האקתון\" במעבדות SAP.
הפוסט פורסם לראשונה ב geektime, אך ללא הפיסקה \"כמה נושאים מעט יותר מתקדמים\" (בעצתו של ניר – בכדי להקל מעט על הקוראים). אני רוצה להאמין שקוראי בלוג זה הם בעלי קיבולת גבוהה יותר מ 1600 מלים / 7 דקות קריאה – המומלצות לבלוגים.

אמרו לי אם לא. אני לא מזלזל בטענה \"זה קצת ארוך מדי בשבילי\". אנחנו בני-אדם.

—-

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

בצד ההגמוניה ההולכת ונבנית ע\"י אנגולר בצד-הלקוח, נשמעים בד בדד, גם קולות הטוענים שאנגולר מורכבת מדי – שאפשר לעשות את הדברים בצורה פשוטה יותר. פשטות היא ערך משמעותי בעולם הקוד הפתוח ובצד הלקוח בפרט. אחד הקולות הללו מגיע מצד Facebook ו Instagram. הפתרון שלהם: ספרייה ששוחררה לפני כשנה בשם react.js.

React.js (להלן \"ראקט\"), לא תתחרה באנגולר ראש-בראש בעתיד הנראה לעין: אנגולר היא מקיפה הרבה יותר (הן פונקציונלית והן תהליכית), ומתאימה הרבה יותר ל mainstream.
ראקט פותרת רק חלק קטן ממה שאנגולר פותרת – ועושה זו בצורה לא שגרתית, מה שעלול להרחיק ממנה מפתחים רבים מה mainstream. היא אנדרדוג. כמה מהרעיונות שלה מקוממים במבט ראשון. אולי גם במבט שני :).

יש כבר כמה דוגמאות לשימושים ב React.js במוצרים מרכזיים[א]: פייסבוק (חלקים), אינסטגרם (עם ה router של backbone) ו Khan Academy (עם Backbone). ניתן למצוא עליה דיונים ב StackOverflow ואפילו יש 4 גרסאות שונות שלה לשימוש ב jsFiddle.

אפילו אם ראקט לא \"תתפוס\" – הרעיונות שלה הם מעניינים מספיק בכדי שיהיה שווה להכיר אותם ולחשוב עליהם. האם היא באה להעיר את תשומת לבנו לאבדן הדרך שהולך ומתרחש? או אולי סתם להציג עוד נקודת מבט אפשרית?

יצאנו לבדוק במה מדובר…

מה ראקט מציעה?

הדבר הראשון הניכר משימוש בראקט הוא מ Client Side Rendering מהיר ביותר! ראקט יכולה לשמש לכתיבת אפליקציות שמרגישות מאוד \"חלקות\" ולא נתקעות. היא מתאימה לאפליקציות בעלות רינדור אינטנסיבי.

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

האלמנט השני שמאפיין את ראקט הוא פישוט צד-הלקוח בכל הנוגע לעדכונים של ה DOM.
בד\"כ באפליקציה נראה 2 סוגי עבודה עם ה DOM: בנייה ראשונית של האפליקציה (על בסיס state מסוים) ועדכון ה DOM הקיים בנתונים חדשים. באפליקציות SPA, עדכון ה DOM הוא חלק משמעותי מהאפליקציה.

ראקט מציעה לנו לבטל את עדכון ה DOM: כאשר יש צורך בעדכון כלשהו, פשוט נהרוס את ה DOM של האפליקציה כולו – ונבנה את כולו מחדש בעזרת לוגיקת \"הבנייה הראשונית\".
פשוט, לא?

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

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

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

רבים ממשתמשי ראקט לא מאמצים / מאמצים חלקית בלבד את הרעיון השלישי, ומשתמשים בראקט בתור שכבת UI (קרי View) לארכיטקטורת MVC סטנדרטית. MVC היא אולי לא מושלמת – אך היא מוכרת ומובנת (כלומר: כל קבוצה מכירה אותה בדרך משלה).
שימוש בראקט עם Angular או Ember אפשרי, אך הוא לא נפוץ. השימוש בראקט מייתר חלק מיכולות Angular/Ember ומחליש את הסדר המובנה שהן מציעות. שימוש בראקט עם Backbone.js, הוא דווקא טבעי בהרבה – כאשר ראקט מחליפה את ספריית ה Template ליצירת Views.

יש כאלו הממליצים להתחיל לעבוד עם ראקט כ View של Backbone, ורק לאחר שמשיגים בצורה זו שליטה – להמשיך הלאה.

סיכום ביניים:

  • ראקט לא מציעה MVC, או תחליף ישיר ל MVC.
  • ראקט מהווה סוג של תחליף ל Templates (וקצת יותר מכך). במקום שפת template \"מנוונת\" – ראקט מאפשרת להשתמש (פחות או יותר) בג\'אווהסקריפט לבניית ה markup.
  • ראקט מערבבת בין ג\'אווהסקריפט לבין \"HTML) Markup)\", אך מפרידה בין רכיבים על הדף – כל אחד אחראי על הפונקציונליות שלו.
  • ראקט יעילה מאוד ברינדור, ומתאימה לאפליקציות אינטנסיביות ברינדור (בעיקר כאלו שעובדות על ה DOM).
    אציין שגם Ember יעילה למדי בתחום זה (בעזרת Handlebars) – אולם נראה שהקוד יהיה פחות קריא.

כיצד ראקט עובדת

React.js היא ספרייה פשוטה, יחסית, בעלת אלמנטים ספורים.

הגדרת רכיבים (Components) נעשית ע\"י המבנה הבא:

var = React.createClass({
  method1: function(…){..};
  method2: function(…){..};
});
לא שונה בהרבה מ Backbone, Angular, או ספריות נפוצות אחרות.

בראקט ישנה הבחנה בין 2 סוגי רכיבים:
  • רכיבים שהם Immutable
  • רכיבים בעלי state
שני הרכיבים נוצרים ע\"י createClass, אך הם שונים בשימוש שלהם ב State: האם כותבים משהו על ה state של הרכיב או לא. ראקט מעודדת לנסות ולכתוב כמה שיותר רכיבים Immutable – עבור יתרון בביצועים. כדי לחסוך ב Garbage Collections ראקט מבצעת pooling של אובייקטים (כגון Events) ועושה בהם שימוש-חוזר.

הרכיבים מושפעים בעת הרינדור שלהם מ2 מקורות: ה properties – שנקבעים בעת יצירת המופע של הרכיב (ולא ניתן לשנותם מרגע זה) וה state – שנשמר על הרכיב, וניתן לשנותו בכל רגע. בד\"כ ה properties ישמשו גורם חיצוני לקבוע את התנהגות הרכיב, בעוד ה state הוא פנימי לשימושו של הרכיב.

אפליקציית ראקט טיפוסית מתחילה בקריאת:

 React.RenderComponent();
היוצרת את רכיב האב וקוראת ל ()render שלו. הוא מצידו יקרא ייצור מופעים של רכיבים בנים (משולבים ב HTML markup) – ויקרא ל render שלהם וכך חוזר חלילה לעומק עץ של רכיבים ו markup.

כאשר יש שינוי ב state של אחד הרכיבים – פונקציית ה ()render של אותו רכיב תקרא ותגרום לעדכון של תת העץ שלו.
הרכיבים בראקט מגדירים את המראה שלהם בעזרת Markup ורכיבים בנים, כאשר הם לא כותבים ישירות ל DOM של הדפדפן לא רק ל React.DOM – ה DOM הוירטואלי שראקט מנהלת. 

כפי שציינו, כתיבה ל DOM היא פעולה יקרה. יתרה מכך: הדפדפן מחזיק Buffer קטן של כתיבות ל DOM אותן הוא מנסה לאגד לאצווה, אולם כל קריאה מה DOM עשויה להכריח אותו לכתוב ל DOM בנקודת זמן לא רצויה (מדוע? הסבר בפוסט זה, \"ההשפעה של פעולות DOM מתוך קוד ג\'אווהסקריפט\").
מכיוון שראקט מנהלת עותק שלה של ה DOM, היא יכולה לקרוא ולכתוב ל DOM הווירטואלי שלה מבלי לעדכן את ה DOM של הדפדפן. כמו כן היא יכולה לסדר פעולות ל DOM בצורה אופטימלית מבחינת ביצועים (סדרות של קריאות וסדרות של כתיבות).
כאשר היא רוצה לעדכן את ה DOM של הדפדפן, היא עושה diff יעיל (להלן הסבר האלגוריתם) של ה DOM הווירטואלי וה DOM האמיתי ומעדכנת ב DOM האמיתי רק אלמנטים שבאמת השתנו. כמובן, שכדי שזה יעבוד, אין לערבב על אותו אזור ב DOM את ראקט עם ספריה אחרת שכותבת ל DOM ישירות.

ראקט מחלקת את הטקסט לרכיבים אטומיים קטנים, כך שיהיה אפשר לעדכן רק את ה nodes של הערכים (בירוק), מבלי לעדכן את הערכים של ה Labels (בצהוב).
שימו לב למספור שראקט מייצרת על ה DOM (תכונת data-reactid) – בעזרתו היא מבצעת השאוות יעילות של ה DOM.
הרעיון של Virtual DOM אינו חדש, אגב: Backbase הציגה מין Virtual DOM משלה לפני שנים (היא כנראה לא היחידה), וטכניקה דומה נמצאת בשימוש במנועים גרפיים של משחקי מחשב.
הניתוק מה DOM מאפשר לראקט גם לרוץ בצד השרת (עבור מכשירים חלשים / sub-second load time) – ממש כמו rendr עבור Backbone.js.
השוואה לא מדוייקת בין האתר Hacker News (שורה עליונה) ו clone שלו שכתבנו בראקט (שורה תחתונה). אדום – טעינה ראשונית. בכחול – פעולת דפדפוף בין הכתבות (האתר שלנו היה הרבה-הרבה יותר מהיר).
פרופיל ה timeline שקבלנו מקוד הראקט מאוד לא אופייני לאתר / אפליקציה שלא עברה עדיין שום אופטימיזציית ביצועים.

דוגמת קוד

אין כמו לראות קצת קוד.
הנה אפליקציה פשוטה למדי עם 2 כפתורים (++X ו ++Y) המעלים את הערכים של ה counters המתאימים במרכז המסך.

התוצאה הסופית של האפליקציה

קוד ב react ניתן לכתוב באחד מ2 סגננונות: JSX ו\"ג\'אווהסקריפט נקי\".
JSX הוא הרחבה לג\'אווהסקריפט עם XML (מבוסס E4X?), המאפשרת לכתוב snippets של XML בתוך הג\'אווהסקריפט ע\"פ חוקים מסויימים.

JSX אמור להיות פשוט יותר ונקי יותר לכתיבה – ועל כן מועדף ע\"י החלק הגדול של מפתחי ראקט. ה JSX מקומפל לג\'אווהסקריפט רגיל לפני שהוא מורץ על הדפדפן. ניתן לקמפל אותו בתוך הדפדפן ע\"י הוספת קובץ ג\'אווהסקריפט בשם JSXTransformer (לפיתוח מהיר יותר) או בצד השרת ע\"י רכיב מתאים של Node (לביצועים טובים יותר).

אנו עבדנו ב IDE בשם WebStorm (מומלץ) שקיבל יפה מאוד את הרחבות ה XML בתוך קובצי ה javaScript, והמשיך לתפקד היטב עם קוד הג\'אווהסקריפט (hinting, ניווט, וכו\').

עוד וריאציה נפוצה יחסית לשימוש בראקט היא כתיבה ב CoffeeScript על מנת לכתוב קוד מינימליסטי של ראקט ללא JSX.

בדוגמה זו נראה כתיבה בצורת JSX ולאחר מכן, את הגרסה המקומפלת שלה ל\"ג\'אווהסקריפט נקי\".

  1. כאשר משתמשים ב JSX Transformer בכדי לקמפל JSX \"תוך כדי שימוש\" (\"on the fly\") – חייבים לכלות הערה זו בתחילת הקובץ.
  2. אנו מגדירים רכיב (component) פשוט של ראקט, בשם Simple.
  3. את הרצת התוכנה בראקט מבצעים בעזרת פקודת renderCompoent על הרכיב הראשי. שימו לב לתגים ה Simple – שאיננה סטנדרטית ב HTML, אלא מבטאת את הרכיב שלנו. אנו שולחים לרכיב ה Simple שלנו Property (בלתי ניתן לשינוי immutable) – בשם message ואומרים לראקט להוסיף את הרכיב בתוך document.body (קרי InnerHTML).
  4. אחד השלבים הראשונים במחזור החיים של רכיב react הוא קריאה למתודה ()getInitialState המאתחלת את ה state של הרכיב. בשלב זה, ניתן להבין שזהו stateful component.
  5. שלב מעט מאוחר יותר במחזור החיים של הרכיב, הוא הקריאה למתודה ()render המייצרת את ה markup של הרכיב – ומחזירה אותו לרכיב האב (במקרה זה – זהו רכיב האב).
  6. יצירת ה markup נעשית על ידי כתיבה inline של markup ב JSX. עטפנו הכל בסוגריים כדי שנוכל למקם את ה div הראשון בשורה חדשה (אחרת JavaScript יחשוב שרצינו לכתוב ;return, ואח\"כ אולי עוד משהו לא רלוונטי).
  7. סוגריים מסולסלות הוא ה escaping לשילוב ביטויי ג\'אווהסקריפט בתוך ה XML (חייב לחזור ערך). במקרה זה אנו קוראים לאובייקט ה properties של הרכיב (שהוא immutable  – בלתי ניתן לשינוי). את הערך של התכונה message קבענו בשלב 3.
  8. באופן דומה אנו רושמים מטפלים לאירועים (incX, incY).
  9. כאשר האירועים נקראים (בעקבות לחיצה של העכבר על אחד הכפתורים), מספיק שנשנה את ה state של הרכיב – כדי ש react ידאג לקרוא ל render, ובצורה יעילה (למשל: לא יותר מפעם אחת ב Animation Frame של הדפדפן).
  10. לתחביר ה XML יש כמה תכונות מיוחדות – למשל הטיפול ב inline style attribute.

ניתן למצוא online JSX compiler בקישור הבא
כפי שאפשר להבחין, ה markup שאנו כותבים ב JSX הוא איננו HTML. זהו XML שרק דומה ל HTML. הסיבה לשימוש ב XML הוא הצורך ב markup קל לפענוח – HTML הוא מאוד סלחן ובעייתי לפענוח.
הדרך הכי בולטת לשים לב להבדל בין ה JSX XML ו HTML היא לקחת דוגמת HTML מהאינטרנט (שעובדת!) ולהדביק אותה לתוך האפליקציה. כמה הבדלים מרכזיים הם:
  1. אלמנטים חייבים להסגר. לא עוד
    אלא רק
    .
  2. מכיוון שה markup מקומפל לג\'אווהסקריפט, לא ניתן להשתמש במלים שמורות של שפת ג\'אווהסקריפט. למשל, במקום תכונת class משתמשים ב className.
  3. inline style attribute לא נכתב כמו ב HTML, אלא כאובייקט – כמו בדוגמת הקוד למעלה. אפשר גם לכתוב אותה inline בעזרת סוגריים מסולסלים כפולים {{…}}=style.

בסה\"כ, בניגוד לציפיותנו, התרגלנו לכתיבת ה XML דיי מהר – והיא לא היוותה מטרד מיוחד.
מעניין לציין ש react עושה escaping ל markup בכדי להגן בפני חולשות XSS.

הנה הקוד לאחר שקומפל ל JavaScript בעזרת ה JSX Transformer:

הודאה קטנה: זה לא בדיוק הקוד. הקוד שנוצר הוא מעט \"עמוס\" בגלל השימוש ב \"React.DOM\" בכל מקום – ולכן עשינו לביטוי זה extract למשתנה שפחות תופס את העין: קו תחתון (בירוק).
הכנסנו את הביטוי במקום שורת רווח כדי לא \"לקלקל\" את מספרי השורות. ה JSX Transformer דואג לשמר את מספרי השורות בין ה source JSX לתוצר ה javaScript שנוצר.

כמה נושאים מעט יותר מתקדמים 

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

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

אפשר לנהל את מודל כזה בצורה הקלאסי (חיבור למודל backbone, למשל) או לתת לרכיב להביא את המידע לו הוא זקוק ישירות מהשרת בעזרת קריאת Ajax (גישה של micro-services – כל רכיב דואג לעצמו מא\' ועד ת\').

על מנת ליצר רשימה כזו בראקט הגדרנו שני רכיבים:

  1. List רכיב שמיצג את הרשימה כולה – רכיב זה ייצור ListItem עבור כל אחת מהרשומות במודל שלנו, וגם ינהל את הרשימה במידה ויש צורך.
  2. ListItem רכיב שמייצג כל רשומה – רכיב זה יצטרך להכיר את המאפיינים של כל רשומה וידע להציג אותם כראוי. 

כזכור, בראקט כל אחד מהרכיבים מחזיר בפונקציות ה () renderאת מבנה ה DOM שעליו הוא אחראי.
בדוגמה שלפנינו, כל רשומה היא בעצם DIV שמכיל את מבנה ה DOM של הרשומה. רכיב ה ListItem יחזיר אך ורק את ה DOM שנדרש בשביל להציג את הרשומה הבודדת.
רכיב ה List שאמור להציג את כלל הרשומות, יחזיר רק את העטיפה הנדרשת לכלל הרשומות, במקרה שלנו רק DIV עוטף אחד.

(לחצו להגדלה)

  1. זוהי הגדרת רכיב האב, ה List. בעת יצירתו הרכיב מקבל startIndex – מאיזה מספר רשומה להציג את ה listItems ו data – הנתונים עצמם המגיעים מהשרת מבנה json. 
  2. דרך יעילה לרנדר רשימה של אברים היא היא לעבור על נתוני המקור (this.props.data) בעזרת פונקציית map (של מערך בג\'אווהסקריפט) וליצור מכל פריט נתונים – מופע של הרכיב (ListItem).
    לא הייתה מניעה, אגב ש ListItem יהיה מורכב גם הוא מתתי-רכיבים. במקרה שלנו הפרדנו רק אלמנט אחד לרכיב-בן: Comment (האלמנט האחרון).
  3. הרכיב ListItem הוא עצמאי ואינו מודע ל List. אחד הרעיונות בראקט הוא לייצר רכיבים הניתנים לשימוש חוזר.
  4. ניתן לראות כיצד ListItem משתמש בטבעיות כ properties שהושמו לו ביצירה (שלב 2) – אלו בעצם הנתונים כפי שקיבלנו אותם מהשרת.
  5. שראקט יודע לטפל ב markup (למשל: אלמנט div), רכיבים (למשל ) או במערך של רכיבים / אלמנטי markup – כמו במקרה זה.
בעקבות שלב 5 בקוד, ראקט יוציא לנו הערה ב console של הדפדפן בזמן ריצה:
לא כדאי להתעלם מהערות אלו (ואחרות שהוא מוציא): כאשר יש רשימה, ראקט מבקש מאתנו מפתח ייחודי שבעזרתו יזהה שינוי state באובייקטי הרשימה. אחרת, האופטימיזציות של ראקט עלולות לגרום לו לדלג על שינויים שנעשו באובייקט – מבלי לעדכן אותם על המסך.
הנה דוגמה ל key כזה שהוספנו לאובייקט הרשימה בפרויקט שלנו:



האינדקס ממנו מתחילה הצגת הרשימה ברגע הנתון – הוא מזהה טוב לצורך זה.

יש עוד כמה נושאים ששווים ציון, אך היריעה קצרה להכיל. הנה מספר נקודות התייחסות אליהם:
מחזור החיים של הרכיב
כדאי להכיר: Component Lifecycle Event
כשתכתבו רכיבים מורכבים יותר – כנראה שתוכלו להיעזר בשלבים שונים ב lifecycle.
ממשק חיצוני של רכיב
כפי שאמרנו, רכיבים נבנו לשימוש חוזר / תחוף. ראקט מספקת מספר כלים כדי לכתוב רכיבים עמידים (robust):
  • default props – ערכי default במידה והמשתמש לא הגדיר את Property נדרש.
  • propTypes – מנגנון אימות על properties: האם יש property שחייבים להגדיר (isRequired), האם יש Property שסט הערכים התקניים שלו הוא סגור ( ([…])isRequired, ) וכו\'.
שימוש חוזר בקוד בין רכיבים
במקום להסתבך עם prototype chains, ראקט מציעה מנגנון נוח יותר של mixins, לשיתוף התנהגות בין כמה components שונים.
שינויים נקודתיים
על רכיב ניתן לשמור קישורים (תכונת refs) לאובייקטים מסוימים ב DOM בכדי לבדוק / לעדכן אותם בצורה נקודתית.

סיכום ורשמים אישיים

ראקט דורשת קצת זמן הסתגלות: אנו ניסינו אותה במהלך סוג של \"האקתון\" שערך שלושה ימי כתיבה, במסגרת מקום העבודה. רק ביום השלישי התחלנו להתרגל לסגנון הכתיבה בראקט (וגם אז – לא כולנו☺).

התיעוד שלה – לא טוב. את רוב התשובות מצאנו ב stackoverflow וכמעט לא בתיעוד הרשמי. נציין שכל נושא שלא עניין אותנו, דווקא היה מתועד בצורה מופתית! (\"חוק מרפי של התיעוד\").

נתקענו כמה פעמים על בעיות, לזמן לא קצר – אך בסוף הצלחנו לפתור את הבעיות ולהבין את מקורן. למשל גילינו, בדרך הקשה, שקביעת ערך לאובייקט ה state (כלומר (…)setState) לא מתרחשת מיידית, אלא היא מנוהלת בתור ומבוצעת רק לפני ציור ה Activation Frame (כלומר האירוע בו הדפדפן מעדכן את תצוגת המסך, שאמור להתרחש 60 פעמים בשנייה). קביעת ערך בפונציה א\' לא מבטיח שזה הערך שיקרא מאובייקט ה state בפונקציה ב\' שמתרחשת מיד לאחר מכן. עדיף להעביר את הערך כפרטמר לפונקציה ולא להסתמך על ה state שיהיה מעודכן.

האפליקציה שכתבנו התרנדרה בצורה מהירה מאוד, גם ללא מאמץ שהושקע מצדנו. החלוקה לרכיבים הסתדרה בצורה טובה, אם כי זו אפליקציה קטנה. סה\"כ הקוד בדיעבד נראה סביר ומסודר.
עוד נושא שלא בדקנו ונשאר כשאלה פתוחה היא הקלות לכתוב בדיקות יחידה: המצאותו של ה Virtual DOM עשויה בהחלט לעזור – אך לא ניסינו זאת עדיין.

היינו מנסים את react.js בעתיד באפליקציות לא-גדולות ועתירות רינדור (הייתה לנו אחת כזו לפני מספר חודשים).
מכיוון שרקאט היא לא כ\"כ דומה ל frameworks אחרים, כדאי כנראה להשתמש בה רק בצוות שירגיש נוח איתה – לרוב מפתחי client-side hardcore הרגילים לכתוב קרוב ל DOM.
שאר המפתחים – יוכלו להמתין שראקט תתבגר עוד קצת ותגיע לגרסה…נאמר, 0.5.

כותבים שותפים:




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



איתמר שגב הוא מפתח Client-Side ב SAP מאוד אוהב טכנולוגיות חדשות ובעיקר את כל מה שגוגל עושה.

קישורים

[א] בהשוואה לאנגולר. ניתן להזכיר שגוגל לא משתמשת באנגולר במוצרים המרכזיים שלה.


שלום, אנגולר! (Angular.js)

לפני כשנה, כתבתי סדרת הפוסטים על MVC בצד הלקוח, והתמקדתי ב 3 ספריות:

  • Backbone
  • Knockout
  • Ember
הזכרתי, כדרך אגב, את Angular.js של גוגל, שנראתה לי אז עוד אחת מיני רבות.
במהלך השנה האחרונה, Angular הפכה לסופר-פופולרית: שנייה לאחר Backbone ע\"פ Github, וכנראה ראשונה בקצב הצמיחה והחשיפה התקשורתית. ייתכן והיא הייתה מספיק משמעותית גם לפני שנה – ופספסתי זאת.

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

בפוסט זה אני רוצה לבצע הכרות זריזה עם אנגולר (Angular), הספרייה שכולם מדברים עליה, ולהציג תמונה מציאותית (אפילו אם חלקית) על אופייה.
משמעות המילה Angular = בעל פינות או זוויות חדות. אולי השם בא לבטא של Angular יש תפיסות חד-משמעיות?!

אז מה היא אנגולר (AngularJS)?

בכדי לענות על השאלה לעומק, נדרשים כנראה 3-4 פוסטים, אבל הנה כמה נקודות מעניינות:
  • אנגולר היא ספריית MVC בצד הלקוח. היא כוללת מודל, View, Controller ו Services.
  • בניגוד ל Backbone וכמו אמבר – זוהי ספרייה בעלת דעה ברורה, שלקחה החלטות עבור המשתמש. המשמעות: פחות שורות קוד שיש להקליד – אך גם פחות גמישות.
  • בניגוד לספריות אחרות, ובצורה דיי ייחודית, אנגולר משתלבת בצורה \"טבעית\" למדי בתוך ה HTML, במקום לייצר (כמו ספריות אחרות) הפשטות מעל ה HTML.
  • אנגולר נכתבה בראייה מרכזית של בדיקתיות (Testability). ניתן לכתוב לה בקלות בדיקות יחידה[א], ניתן לכתוב לה בקלות בדיקות אינטגרציה/מערכת. זו לא טענה שקל לטעון בעד אמבר (EmberJS) – למשל.
    אנגולר היא ספריית-האם של Karma (כלי מצוין להרצת בדיקות בג\'אווהסקריפט, שאנו משתמשים בו בלי-קשר).
    בכדי לתמוך בבדיקות יחידה, אנגולר כוללת מנגנון Dependency Injection [ב] מובנה.
    כל אלה = כבר סיבה טובה להעריך את אנגולר!
  • אנגולר אינה תלויה בשום ספריה אחרת, והקהילה שלה בעצם מעודדת אי-שימוש ב jQuery. היא כוללת תת-ספריה שנקראת jQuery Light המספקת כמה יכולות בסיסיות מקבליות ליכולותיה של jQuery ובתחביר דומה מאוד.
  • אנגולר תואמת AMD, אם כי לא ל require.js.
  • לאנגולר יש אינטגרציה עם ספרייה \"לוהטת\" אחרת: Twitter Bootstrap.
    אנגולר לא מייצרת UI עשיר ויפה (אלא רק Markup – ממש כמו ספריות MVC אחרות). Bootstrap משלימה גם את היופי וגם מתאימה לפילוסופיה של אנגולר. השלמה זו צריכה לעבוד יפה גם עם אלטרנטיבות ל Bootstrap כמו Foundation, Neat, Pure או Semantic.
  • לאנגולר מנגנון Two-Way Data-Binding יעיל (פרטים בהמשך).
  • לאנגולר יש מנגנון Templating ייחודי משלה (פרטים בהמשך).
  • ה \"Sweet Spot\" של אנגולר היא אפליקציות עסקיות, ואפליקציות CRUD בפרט (הזנת ושליפת נתונים). אנגולר לא מתאימה, למשל, למשחקים או ל UI לא-שגרתי.
  • אנגולר פופולרית: אנגולר היא ה \"A\" ב \"MEAN\" – סט טכנולוגיות פופולרי שמתתעד להיות התחליף המודרני ל LAMP.
  • אנגולר איננה פשוטה כמו Backbone, לוקח זמן ללמוד אותה ולהתמצא בפרטים.
  • אנגולר טוענת שהיא \"Forward Looking\" ומיישמת היום תקני ווב עתידיים / נראית כמו שהווב יראה עוד כמה שנים.
    יש דיבורים על כך שספרייה חדשנית אחרת של גוגל, Polymer, תשתלב לבסוף באנגולר.
    אני לא מתייחס לטיעונים האלו ברצינות, אנו יודעים למי ניתנה הנבואה…
  • אנגולר היא מבית גוגל, מה שאומר שיש מאחוריה מפתחי ווב מוכשרים אבל גם מנגנון יחסי-ציבור חסר-תקדים. פרויקטים של גוגל נוטים לעשות המון באזז עוד לפני שהם באמת הצליחו, מה שמותיר ספק אם לאנגולר צפוי מחזור חיים דומה לזה שהיה ל GWT.
הנה דוגמה המשבחת את אנגולר ומראה כיצד מעבר מ BB לאנגולר צמצם בצורה משמעותית את כמות הקוד. הגיוני.

שלום עולם!

בואו נראה דוגמת קוד קטנה באנגולר:

  1. שימו לב שדף ה HTML באנגולר הוא משמעותי. ראשית הוספנו סקריפטים – כמו כל דף HTML.
    נכון, היה יעיל יותר להוסיף את תגיות ה script לאחר ה , דילגתי על שיפורי ביצועים לצורך פשטות הקוד.
  2. בתוך ה body הוספתי H1 והגדרתי את HelloWorldCtrl כקונטרולר האחראי על אזור זה (תג ה H1). הסיומת Ctrl מקובלת באנגולר לסימון קונטרולרים. צירוף האותיות ng משמש לכל ה properties / תגיות שאנגולר מוסיף ל HTML.
    אנגולר, במקום לבצע הפשטה של ה DOM/HTML – מרחיב אותו. קידומת ה data היא רשות אם אתם רוצים לשמור את קובץ ה HTML שלכם תקני. ניתן (ומקובל) להשתמש פשוט בקיצור, כגון ng-controller, וכך לחסוך קצת הקלדה [ג].
  3. בתוך תגית ה HTML הכנסנו ביטוי בשם message. אם עבדתם פעם עם mustache – אתם מכירים את השפה: סוגריים מסולסלים כפולים עוטפים ביטוי שיחושב.
  4.  באופן דומה למדי, גם הביטוי {{1 + 1}} יחושב. הבדל קטן: הוא לא דורש context (באנגולר נקרא: scope) בעוד הביטוי {{ message }} דורש context – והוא יקבל אותו מה controller האחראי על האזור.
  5. קוד הג\'אווהסקריפט שלנו פשוט למדי: הגדרנו קונטרולר (התאמה ע\"פ שם) שברגע ההפעלה שלו רושם על ה context/scope ערך בשם message. קידומת $ למשתנים (קרי scope$) מסמלת שאלו משתנים מיוחדים של אנגולר.
  6. כשספריית אנגולר נטענת, אנגולר מפענח את ה DOM ומחפש אחר תוית data-ng-app/ng-app. תוית זו מסמנת את אזור האפליקציה של אנגולר. אפליקציות אנגולר יכולות להתארח או לחיות זו לצד זו.
    לאחר מכן אנגולר ימשיך לפענח את ה DOM וימצא את data-ng-controller.
    בשלב הבא הוא יאתחל את ה controller ואז בעזרתו יחליף את {{ message }} בביטוי הרצוי.
הנה התוצאה המדהימה של האפליקציה שלנו:
כפי שאתם רואים, אנגולר \"התעסקה\" לנו ב HTML markup:

זו חלק מהשיטה.

סיכום ביניים:

  • ה HTML הוא מרכזי בפתרון. לא כמו Backbone שמעדיפה HTML עם DIV אחד ריק.
  • Controller הוא מעין \"מנהל אזורי\"
  • חלק משמעותי מהכתיבה באנגולר (ה View / HTML) – הוא דקלרטיבי.

שלום עולם 2: Data-Binding דו-כיווני

קשה להגדיר את הדוגמה הקודמת כדוגמה מייצגת אפליקציית אנגולר – רק בגלל שאנגולר היא מקיפה ולה כלים רבים. הדוגמה הבאה תשלים אזור חשוב מאוד באנגולר: ה 2WDB (קיצור של Two-Way Data Binding).

2WDB אומר בקיצור:

  • ברגע שמעדכנים את המודל – ה View מתעדכן אוטומטית.
  • ברגע שמעדכנים את ה View – המודל מתעדכן אוטומטית.
  • (אין מה לדאוג: אין לופ אינסופי)
מנגנון זה הוא אחד מ\"חוסכי הקלדת ה boilerplate code\" הגדולים של אנגולר באפליקציות CRUD: רישום לאירועים ועדכון מודלים או Views – קוד טכני שאין בו הרבה חוכמה.

בואו נתבונן בדוגמה הבאה:

  1. קישרנו את שדה ה input ל מודל בשם message. היכן \"מחלקת המודל\" בקוד ג\'אווהסקריפט? – לא קיימת, במקרה זה היא implicit.
  2.  הביטוי {{message}} נמצא בתחום השיפוט של השליט האזורי – הרי הוא ה HelloWorldCtrl. המודל שהגדרנו בשלב 1, מתיישב על ה scope$ של הקונרטולר כפרמטר ובו הערך של שדה ה input. הגדרת HTML זו בלבד (ללא קוד הג\'אווהסקריפט) מספיקה בכדי לייצר databinding עובד. הנה לינק לדוגמה עובדת, ללא כל קוד ג\'אווהסקריפט.
    כאשר לא מוגדר controller, אזי ה binding ייעשה על ה scope$ הגלובאלי.
  3. ביצעו אתחול של הטקסט לערך התחלתי.
  4. הוספנו האזנה (watch$) על המודל של ה message. כל פעם שיש שינוי נמיר מופעים של האות e ל UpperCase. שימו לב שבעקבות ה 2WDB, הערך ההתחלתי שקבענו (\"!Hello World\") כבר עובר טרנספורמציה. הנה דוגמת קוד חיה. נסו להקליד e קטנה בתיבת הטקסט ותראו מה קורה.

כל שינוי שנעשה ב View (תיבת הטקסט) משפיע על המודל (אובייקט ג\'אווהסקריפט בזיכרון, במקרה שלנו הוא בלתי-מפורש). בקונטרולר אנו מנטרים את השינוי במודל (watch$) ומבצעים בו שינוי נוסף – שחוזר ומעדכן את ה View (תיבת הטקסט). זהו ה 2WDB של אנגולר.

ה Directive

אחד המרכיבים המסתוריים של אנגולר הם ה Directives, בואו נציץ בהם ונראה במה מדובר.
בוודאי שמתם לב שהשתמשנו בתגיות HTML ו Properties שאינם קיימים ב HTML5, כל ה *-ng.
אנגולר מרחיבה את שפת ה HTML [ד] בשפה משלה, ובעצם מעניקה למפתח את היכולת להמשיך ולהרחיב את שפת ה HTML.

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

לאנגולר יש Compiler (רכיב מרכזי) שסורק את ה HTML (הקיים והמתעדכן) ומבצע בו שינויים. אם הוא מזהה תגית לא סטנדרטית, למשל \"lb-superlink\", הוא יחפש רכיב (מעין \"plugin\" לקומפיילר) שמתאר את האלנמט הזה ויאמר לו מה לעשות. רכיב כזה נקרא Directive.

מספר directives מסופקים, מהקופסא, עם אנגולר: למשל ng-repeat – רכיב שיכפיל קטע markup לכל רכיב ברשימה שמופיעה במודל (למשל: lineItems).

מתכנתים יכולים להרחיב ולכתוב directives בעצמם. הנה קוד של directive פשוט למדי, ה (Superlink (tm – תגית המספקת טקסט אסתטי שניתן ללחוץ עליו :

  1. lb-superlink הוא הדירקטיב (directive) שמיד נגדיר. בעת לחיצה על הלינק – תופעל הפונקציה ()foo.
    כפי שניתן לראות, ה IDE מזהיר אותי שזה אלמנט לא סטנדרטי ב HTML.
  2. כפי שציינו בהקדמה, אנגולר תואמת למערכת המודולים של AMD. בחרתי בדוגמה זו להראות קוד קצת יותר \"מציאותי\", אשר מארגון את הקוד סביב המודול myModule.
  3. על דירקטיב (directive) להיות מוגדר עם שם camelCase, כאשר אנגולר מפרק את השם ליחידות ותאפשר מספר צורות של תחביר ב HTML לגשת לדירקטיב שהוגדר. למשל, שמו של ng-repeat הוא בעצם \"ngRepeat\" (בצורת camelCase) וניתן גם לקרוא לו בצורות כגון ng_repeat, ng:repeat או data-ng-repeat.
    באופן דומה יהיה ניתן להפעיל את הדירקטיב שלנו בצורת lb-superlink, lb_superlink וכו\'.
  4. התחביר ליצירת אובייקטים שהם instantiated באנגולר הם ע\"י פונקציה המחזירה אובייקט Prototype (כלומר: תבנית העיצוב בשם Prototype), המוגדר בתוך ה return. זהו תחביר קיים (אם כי לא מאוד נפוץ) להגדרת מחלקות בג\'אווהסקריפט.
  5. restrict מגדיר כיצד יהיה ניתן להשתמש בתוך ה HTML בדירקטיב שלנו. \"E\" הוא קיצור של אלמנט, קרי . אם היינו כותבים \"AE\" היה ניתן להגדיר את הדירקטיב גם כ attribute על אלמנט קיים קרי .
  6. ה template מתאר את ה markup שהדרקטיב שלנו ייצר. בחרנו ב div פשוט עם label עבור accessibility. עוד פרטים – בהמשך.
  7. כאשר replace הוא אמת – התג המקורי יוסר מתוך ה HTML ע\"י הקומפיילר. ניתן להשאיר את התג הקיים או להחליף אותו.
  8. transclude אומר שאנו רוצים לקחת ערכים מתוך התג המקורי ולהציב אותם בתוך התג כ ng-transclude ב template. במקרה שלנו: תג ה label.
  9. link הוא ערך חשוב. הוא בעצם פונקציית ה processing שתופעל בנוסף לפעולות הסטנדרטיות שהוגדרו ע\"י ה properties הנ\"ל. כאן אנו יכולים לכתוב קוד ג\'אווהסקריפט חופשי משלנו. במקרה שלנו אנו מבצעים bind לארוע לחיצה על האלמנט שנוצר. השתמשנו כאן ב jQuery Light שמגיע עם אנגולר (\"bind\"). כפי שניתן לראות – התחביר דומה מאוד.
  10. קוד זה (eval=evil) נראה קצת פחות אסתטי, אך הוא מקובל באנגולר. אנו קוראים את ערך ה attribute בשם click (במקרה שלנו: \"()foo\"), ומבצעים לו eval, במסגרת ה scope הרלוונטי לאלמנט. בת\'כלס אנו מפעילים את פונקציית foo שהוגדרה על ה scope$ של הקונטרולר.
הנה ה markup שנוצר (כפי שאתם זוכרים, אנגולר \"מעשיר\" את ה markup בביטויים כגון ng-scope):

סיכום

עשינו סקירה + צלילה-מהירה לתוך אנגולר, ספרייה שתופסת לאחרונה תשומת לב וקהל מעריצים רב. ברור שנותר רב נסתר על הגלוי, אל אני מקווה שהצלחתי בקריאה של 5 עד 10 דקות להסיר חלק מהמסתורין מסביב לאנגולר לאלו שאינם מכירים. נראה לי חשוב ומעניין להכיר, אפילו במעט, ספרייה שמשפיעה במידה לא-מבוטלת על עולם ה FrontEnd.
שיהיה בהצלחה!

—-

[א] אם אתם רוצים לקרוא מעט יותר על בדיקות יחידה: על בדיקות יחידה.

[ב] אם אתם רוצים לקרוא מעט יותר על Dependency Injection: להבין Dependency Injection.

[ג] בעצם, אנגולר הפרה את תקן ה HTML5 לאורך זמן, ורק לאחרונה הוסיפה את האופציה להוסיף קידומת -data ולציית לתקן.

[ד] אנגולר מתבססת תקן עתידי של Custom Elements, שאם יתוקנן כמתוכנן – יהפוך את התחביר של אנגולר ל HTML5 תקני.

[ה] תוכלו להתעמק בנושא לא-פשוט זה בעזרת מדריך ה Directives של אנגולר: http://docs.angularjs.org/guide/directive

לינקים אחרים

הרבה תוכן מעמיק על אנגולר, גם בעברית, בבלוג של אייל ורדי

סיכום של שנתיים עבודה עם אנגולר, ע\"י Alexey Migutsky

Weekly על אנגולר, שכולל בערך אינסוף לינקים (לא סתמיים) בנושא: http://syntaxspectrum.com/

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 – ספגטי או רביולי?

״קוד ספגטי״ הוא תיאור של קוד סבוך, מלא תלויות – ועל כן קשה לתחזוקה. בכל פעם שעושים שינוי באזור אחד בקוד, קיים סיכוי גבוה ״לשבור״ אזורים אחרים בקוד. ״קוד רביולי״ הוא המושג ההפוך המתאר קוד מודולרי וקל לתחזוקה: רביולי הם נפרדים אחד מהשני ויש להם הכמסה טובה – רביולי אחד לא יכול לגעת ב״מילוי״ של השני.
ג׳אווהסקריפט מעלה את נושא ״קוד הספגטי״ מחדש למרכז הבמה: מתכנתים רבים נכנסים עכשיו לעולם הג׳אווהסקריפט, עולם עם מעט הגנות והרבה אפשרויות ליצור \"קוד ספגטי\". ג\'אווהסקריפט לא תוכננה בכדי לתמוך במערכות גדולות, והאופי \"מונחה-האירועים\" מעודד פיזור אחריויות ויצירת תלויות רבות. מי שחושב ששפה איננה אחראית לאיכות הקוד – שייזכר בפקודת ה GOTO בשפת בייסיק.

ספריות JavaScript MVC באו לנסות ולעזור למתכנתי ג\'אווהסקריפט לעשות סדר – אך האם הן מספיקות?
בפוסט זה אני רוצה לעזוב את מסלול הTutorial שהתחלנו בו לגבי Backbone.js (בקיצור: BB): מסלול שמציג את יכולות הספרייה בצורה אופטימיסטית בה הכל מסתדר יפה. אתם בוודאי מכירים את המצב הבא: אתם לומדים טכנולוגיה, עושים כמה tutorials ומרגישים שהכל ברור ואז אתם מנסים את הטכנולוגיה בעבודה היומיומית שלכם ו…\"הופ\" – נתקעים אחרי שעות בודדות רק כדי לגלות איזו בעיה עקרונית, מוכרת, שהרבה פורומים דנים בה – אך יש לה מעט פתרונות יפים. מכירים?!
ספריות JavaScript MVC הן לא שונות, ועל כן אני רוצה להציג גם את הצדדים הללו. 

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

קוד \"ספגטי\"?

בחרתי להתמקד בדוגמה \"קטנונית\" לכאורה. הדוגמה מבוססת על מודל ה Person וה View בשם PersonView מהפוסטים הקודמים. בחלק זה הוספתי לה את היכולת לעשות expand/collapse לפרטים של האדם (במקרה שלנו: האימייל) + אייקון יפה שמציג את המצב, \"פתוח\" או \"סגור\".

הנה הקוד:

הקוד אמור להיות ברובו מוכר.

1- העשרתי מעט את ה template. הוספתי תמונה המתארת את מצב הפריט ברשימה (סגור/פתוח) והוספתי class בשם hidden שבעזרתו אני מסתיר חלקים ב markup. תיאור ה class יהיה משהו כמו:
.hidden {
display : none
}

טכניקה זו מאפשרת לי להסתיר / להציג את ה markup ע\"י הוספה / הסרה של CSS class – פעולה קלה בjQuery.

2 – שימוש במנגנון האירועים של BB. הפורמט הוא key: value, כאשר:
Key = מחרוזת: \".
Value = מחרוזת עם שם הפונקציה באובייקט ה View שתופעל כתגובה לאירוע.

BB בעצם מפעילה כאן את jQuery: היא רושמת את הפונקציה שהוגדרה ב value, לשם האירוע (למשל \"click\" או \"keypress\"), על האלמנט(ים) שחוזרים מהשאילתה.

את השאילתה היא מבצעת על אלמנט ה el, ואם לא הוגדרה שאילתה – היא מפעילה jQuery.delegate על el, כך שהאירוע יופעל עבור כל אלמנט-בן של el.

בנוסף, BB גם עושה עבורנו Function Context Binding (לטיפול ב this) – כך שאין צורך לבצע bind/bindAll לפונקציית הטיפול באירוע.

את האובייקט הנכון של ה View היא מזהה בעזרת ה cid שעל ה el – מה שחוסך לנו הרבה עבודה.

3 – זו הפונקציה שתקרא לאחר שהמשתמש לחץ על ה person-frame שלנו. היא מזהה את המצב הנוכחי ומבצעת את השינויים הדרושים ב DOM. קריאת toggleClass של jQuery מסירה / מוסיפה CSS class בדיוק עבור שימושים כאלו.

הנה ההרצה של הקוד:

1 – אנו מוודאים שה default הוא הנכון.

2 – אנו מדמים לחיצה של משתמש על ה person frame ומוודאים שהמצב השתנה.
טיפ קטן: אני משתמש ב (\'trigger(\'click, הקצת פחות קריאה, ולא ב ()click הקצת פחות אמינה. כמה דפדפנים מונעים שימוש ב click ישירות.

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

הקוד נראה בסה\"כ קריא, ולא כ\"כ מסובך. מדוע אם כן אני קורא לו \"קוד ספגטי\"?

  1. אנו שומרים state של האפליקציה (isExpanded) על ה DOM.
    נכון, זו טכניקה מקובלת בג\'אווהסקריפט – אבל זה \"ספגטי\": כל פעם שאנו רוצים לעשות שינויים ויזואליים אנו צריכים \"לבחון\" את ה DOM ולהסיק מה המצב שלו. כשהקוד גדל ונהיה מסובך – זה יכול להיות פשוט סיוט של קוד.
  2. אנו מציבים לעצמנו מגבלה שה View לא יתרנדר מחדש. אם מרנדרים אותו – אנו מאבדים את ה state שלנו.
  3. ה markup שלנו נובע בעצם מ 2 מקורות שונים: מה template וממנהלי האירועים. כדי להבין מה / כיצד נוצר – יש לבחון את שניהם, מה שיגרום לנו לשאול את עצמנו: \"מאיפה \'זה\' הגיע…?\"

אפילו בקוד הבדיקות, \"נגררתי\" (עלק) לבדוק את מצב ב DOM: האם יש css class מסוים או לא. מבנה ה DOM הפך לציבורי.

מודל רזה או שמן?

ב JavaScript MVC יש בגדול 2 אפשרויות כיצד לחלק את האחריויות בין ה View וה Model:
  • \"מודל רזה\" (נקרא גם anemic model) – המודל הוא בעיקרו DataStructure / DTO שמחזיק בצד הלקוח עותק קונסיסטנטי של מצב האובייקט בצד-השרת / בבסיס-הנתונים. כל הלוגיקה ה\"עסקית\" מתרחשת ב View.
  • \"מודל שמן\" (נקרא גם rich model) – המודל הוא המקום בו מתרחשת הלוגיקה העסקית, בעוד ה View הוא רק שיקוף של UI למצב המודל.
BB, באופן רשמי, לא נוקטת עמדה לגבי הגישה המועדפת. \"אפשר גם וגם\".

אם אתם משתמשים ב\"מודל רזה\", BB מספקת מנגנון בשם Backbone.sync שעוזר לשמור / לטעון את המודל משרת בעל RESTful APIs: אם תגדירו על מודל / אוסף (collection) את ה url של השרת / REST servuce ותקראו לפעולת ()fetch, ספריית BB תקרא לאותו ה URL ב get (קונבנציות REST) ותטען מחדש את המודלים מהשרת. פעולת create על האוסף תיצור בשרת (ולאחר תשובת HTTP 201 – גם באוסף שלכם) את המודל. כנ\"ל יש גם update ו delete וכו\'.

אם אתם משתמשים ב\"מודל שמן\" – אזי BB מספקת פחות תשתיות מוכנות. אין בעיה להוסיף פונקציות על המודל שמחשבות ערכים, כגון ()isVIP שמכילה את הלוגיקה האם Person מסוים הוא אדם \"חשוב מאוד\".

הקושי נובע מהמצב בו אתם רוצים לשמור על המודל client-side state או ui state – כלומר properties שישמשו אתכם בצד הלקוח אך לא תרצו לשמור בשרת.
כיצד תוכלו לומר ל BB אילו תכונות של המודל נשמרות בשרת ואילו לא? BB לא מספקת יכולת מובנה לדילמה זו.
ההורשה ב BB, כלומר extend._ איננה הורשה אמיתית, היא בעצם ממזגת עותק של אובייקט האבא (כלומר Prototype Design Pattern) עם אובייקט חדש שאתם מגדירים.
BB גם לא תומכת בארגון מודלים בהיררכיה – רק ברשימות (כמו טבלאות ב DB רלציוני). כלומר: איננו יכולים להשתמש בהיררכיה על מנת להפריד בין ה״מודל לשמירה בשרת\" ל״ערכי המודל של צד-הלקוח״.

  1. לפלטר את ה ui state בצד השרת? אאוץ.
  2. אפשר להשתמש בplug-in ל BB שמאפשר ניהול היררכי של מודל (במקום הורשה), משהו כמו BB deep model. קצת מסורבל.
  3. אפשר לוותר על שימוש ב Backbone.sync ולפלטר לבד את ה ui state.
בואו נבחר בדרך מס\' 3.

בלי קשר לבעיה זו – אינני אוהב את הדרך בה עובד מנגנון הסנכרון לשרת של BB :

  • הוא בעיקר עושה דלגציה ל ajax.$ ומוסיף עליה מעט מאוד. אני מרגיש שבנקודה זו BB לא השקיעה בי מספיק.
  • נקודת הגישה לשרת נעשית דרך המודל ולא ישירות – דבר שלא מרגיש לי נכון. טיפול בשגיאות והתנהגויות חריגות הוא מסורבל, הקשר ההדוק הזה מקשה על היכולת לבצע בדיקות-יחידה, ואי אפשר להשתמש ב Backbone.sync עבור צורות תקשורת אחרות (למשל Push/WebSockets).

\"מודל שמן\" ב Backbone.js

הנה המודל שלנו ב\"גרסה השמנה\" (Rich Model):

  • הוספתי למודל תכונה בשם isCollapsed שהיא חלק מה UI State שלו – שלא אמור להישמר בבסיס הנתונים.
  • יצרתי מתודה בשם getJSONToPersist שתחזיר לי את ה JSON של מה שצריך להישמר בשרת. ניתן ליצור superclass חדש של BB.Model על מנת למחזר קוד זה.
  • את השינוי במצב (גלוי/חבוי) שייכתי לפונקציה במודל בשם toggle. כרגע היא מזערית, אך זה הרגל טוב לכתוב גם פונקציות לוגיות מזעריות – במקום הנכון. הן נוטות להתרחב.
  • הוספתי עוד דוגמה לפונקציה \"לוגית\" קצת יותר עשירה בשם isVIP. היא איננה בשימוש בדוגמה זו (ולכן מסומנת באפור).

הנה ה View:

אפשר לראות שיש 2 templates עבור כל מצב: Collapsed או Expanded – גישה זו טובה כאשר יש 2-3 מצבים. אם יש יותר – אז כדאי להחזיק template בסיסי ולבצע ב ()render שינויים ב markup.

המחזור של תגובה-לאירוע שונה משמעותית מזה של הדוגמה הקודמת:
אם קודם המחזור היה: רינדור View ל DOM, לחיצה של משתמש, אירוע -> שינוי ה DOM,
עכשיו המחזור הוא: רינדור View ל DOM, לחיצה של משתמש, אירוע -> שינוי המודל -> אירוע שינוי המודל -> רינדור ה View ל DOM.

השינוי מתרחש במודל – וה View משקף אותו. ב View אין חוכמה לוגית / עסקית – הוא מטפל ב UI נטו.
שימו לב שעל מנת לשנות את המחזור, הוספתי binding לאירוע ה change של המודל בבנאי – כלי נוח ש BB מספק לי.
קוד ה render הוא לא קצר הרבה יותר מהדוגמה הקודמת – אך הוא פשוט יותר ו\"שביר\" פחות. הוא בוחן את המודל (קוד js פרטי ולא DOM – שיכול להיות מושפע מהרבה מקורות) ורק על פיו הוא מחליט מה לצייר.
אין לי צורך \"לבצע delta\" (במקרה זה: הסרה של ה CSS Class בשם hidden) כי בכל מחזור אני מתחיל על בסיס ה template מחדש – דבר שמפשט את הקוד משמעותית.

שיקולי ביצועים

אחד הדברים שהכי בולטים בקוד ה\"רביולי\", מלבד שהוא קצת ארוך יותר, הוא שהוא עושה יותר עבודה.
במקום לשנות ב DOM משהו קטן – רינדרנו חתיכת markup מחדש והחלפנו את כולה. \"שיטת המוסכים המורשים\" – אני קורא לה: \"שריטה בדלת? – נחליף דלת\", \"רשרוש במנוע? – לא מתעסקים. נחליף את המנוע\".

זה עשוי להראות דיי מרתיע – אך לרוב אין פה בעיית ביצועים אמיתית: רוב הפעמים ההחלפה תהיה של \"מודל בודד\" שלא יכלול הרבה markup – ולכן החלפתו לא תהיה יקרה.

אם שמתם לב, הוצאתי את \"קימפול\" של ה templates (פקודת template._) מחוץ ל instance הבודד של ה view – זה דבר משמעותי הרבה יותר מבחינת ביצועים!

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

  • אתם יכולים לעשות binding לשינוי של שדה ספציפי במודל, בפורמט \'<change:<field name' על מנת להיות מסוגלים להגיב לשינויים נקודתיים מאוד.
  • אתם יכולים לבקש מ BB להשתמש ב el קיים ב markup ולא לייצר אותו, כך שתוכלו להשתמש בספריית templating סלקטיבית (לדוגמה pure.js או handlebars) – אשר עושה שינויים ב markup מבלי לרנדר אותו כל פעם מחדש.
  • אתם יכולים לא להשתמש בספריית templating ולבצע רינדור סלקטיבי בעצמכם. עשו סדרה של שינויים ב DOM – אך שאבו את המידע מהמודל ולא מה DOM.

אני בטוח שיש עוד כמה דרכים אפשריות…

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

סיכום

כל חובב בישול יאמר לכם שרביולי הוא טוב מספגטי. ספגטי הוא דיי … פושט.
בתור מישהו שעוסק הרבה ב״הנדסת תוכנה״, אני מתפתה להצטרף למקהלה ולהעדיף אוטומטית את \"מודל הרביולי\" – אך לא כך.

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

הבעיה: כאשר מתרגלים לסגנון אחד – קשה לעבור לשני.
לאחדים (כמוני) שהתרגלו לכתוב רק \"קוד רביולי\" – קשה מנטלית לכתוב \"קוד ספגטי\" גם כשהדבר נכון. זה גורם להרגשה רעה.
לאחרים, שהתרגלו לכתוב \"קוד ספגטי\" – קשה מנטלית לכתוב \"קוד רביולי\" – זה מרגיש מיותר ומעייף.

אני מניח שאם הגעתם להתעסק ב Backbone / JavaScript MVC – כנראה שעלה צורך ל\"קוד רביולי\".
אני, הייתי הולך עם זה עוד צעד קדימה – ועובד עם \"מודלים שמנים, Views רזים\".

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


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

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

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