ניתן לומר שהגדרת ארכיטקטורה מורכבת מ 4 פעולות בסיסיות:
חלוקת המערכת למודולים / תתי מערכות
ניהול תלויות (בין המודולים)
יצירת הפשטות (Abstractions) והגדרת API
תיעוד הארכיטקטורה.
כמעט כל העקרונות והטכניקות של הגדרת ארכיטקטורה (למשל Quality Attributes או חלוקה ל Views) הן הנחיות כיצד לבצע פעולות בסיסיות אלו בצורה נכונה יותר."ניתוח Quality Attributes" היא טכניקה שכתבתי עליה בפוסט הזה והזה.
תכנון מונחה אובייקטים – OOD
תחום ה Object Oriented Design הוא תולדה של רעיונות שהתפרסמו במספר ספרים / מאמרים משפיעים – אך בניגוד למה שניתן לחשוב, אין הגדרה "חד-משמעית וברורה" מהם "עקרונות ה Object Oriented Design".
חוצמזה ניתן בהחלט להזכיר את תנועת ה Patterns (קרי POSA, PLOP, GOF) שלא ניסחה חוקים אלא "תיעדה" תבניות עיצוב מוצלחות – אבל יש לה השפעה ניכרת על הדרך בה אנו עושים היום Design (ולא תמיד לטובה).
העשור האחרון
זרם ה Agile השפיע גם הוא רבות על OOD וקידם כמה רעיונות:
"כשאתה מקודד – אתה בעצם עושה Design" (מקור: TDD) –> ומכאן רעיונות כמו "Design by Tests/Coding"
ההכרה שביצוע Design או הגדרת ארכיטקטורה הם Waste – שיש לנסות ולייעל אותם ("Just Enough Software Architecture")
ההבנה שחיזוי העתיד הוא דבר בלתי-מציאותי, גם על ידי אנשים נבונים למדי, במיוחד במוצרים חדשים אך גם במוצרים קיימים. ירידת קרנם של "העיקרון הפתוח-סגור" (מתוך SOLID) ו "(Predictable Variations (PVs" (מתוך GRASP) והצבת סימני שאלה בפני כמה מהעקרונות האחרים…
התאוריה המאוחדת
בכל מקרה ישנה השאלה: בהינתן עקרונות ל"ארכיטקטורה", "תכנון" ו"כתיבת קוד" – היכן בדיוק עוברים הגבולות הללו? מתי יש להשתמש בעקרון ארכיטקטוני ומתי בטכניקת Design?
אני רוצה לטעון שהקו הוא דיי מלאכותי – וכדאי להתייחס אליו ככזה.
כמו שיש דמיון בין יחסי גומלין בין אנשים בודדים, קבוצות אנשים ומדינות (קונפליקטים, "כבוד", מעשה טוב שנזכר במשך שנים) וניתן להכליל ביחסים ברמות שונות עקרונות דומים, ניתן למצוא ולהכליל עקרונות דומים ברמות שונות של תוכנה.
ניתן להשתמש בכללי הקוד, התכנון והארכיטקטורה מבלי להתייחס לגבולות – חוץ מאשר בכמה מקרים יוצאי דופן.
ניסיתי למפות את "ארבע הפעולות הבסיסיות של הגדרת ארכיטקטורה" לפעולות תכנון וכתיבת קוד:
אפשר לראות שמות שונים ("גבוהים" ו"נמוכים") לרעיונות דומים – אבל ההקבלה הרעיונית יפה למדי.
האם העקרונות הארכיטקטונים של חלוקה מערכת למודולים או יצירת Abstractions נכונים גם לכתיבת קוד? – אני מאמין שכן.
כלומר: ארכיטקטים יכולים ללמוד על ארכיטקטורת-מערכת מתוך עקרונות ומגמות של כתיבת קוד, כשם שמתכנתים יכולים ללמוד לכתוב קוד טוב יותר ע"י למדיה של עקרונות "ארכיטקטונים". להזכיר: ההפרדה ל"ארכיטקט" ול "מתכנת" היא גם חצי-מלאכותית, אני משתמש בה כי היא נוחה לצורך הדיון.
הנה שתי דוגמאות:
עקרון קוד: המעט בכתיבת הערות – ע"י כתיבת קוד שמסביר את עצמו טוב יותר ("Literary Code").
אני לא מכיר כלל ברור ש"תיעוד ארכיטקטורה הוא סממן לארכיטקטורה לא ברורה" אבל דיי ברור לי שזה נכון. אם צריך לתעד ארוכות את הארכיטקטורה – כנראה שהיא לא ברורה ואפשר לנסות לשפר את המטפורות / הגדרת המודולים בכדי שתהיה ברורה יותר. כך יהיה צורך בפחות תיעוד.
עקרון ארכיטקטוני: Interface Segregation Principle
עקרון זה אומר שמודול לא אמור להיות תלוי ב interfaces רבים שאינם בשימוש. אם נוצר מצב כזה – יש לפצל את ה Interfaces כך שהמודול יהיה תלוי, עד כמה שאפשר, רק ב Interfaces שהוא משתמש בהם בפועל. העיקרון נכון מאוד גם למתודות בתוך interface יחיד (רמת ה Design) או לפרמטרים בחתימה של פונקציה בתוך הקוד (רמת הקוד).
עוד היבט קוד שאני מאמץ מעקרון ה ISP הוא לנסות ולהימנע משימוש בספריות (כגון "Open Source") שיש לי מעט שימוש בהן. אני אשתדל לא להכליל במערכת ספרייה של אלף שורות קוד – אם אני משתמש רק ב 50 מהן. אני מעדיף למצוא ספרייה אחרת או אפילו לכתוב אותן שורות קוד לבד. מדוע? א. סיכוי לבעיות מ 950 שורות קוד לא רלוונטיות, ב. מסר לא ברור האם נכון "להשתדל" להשתמש במתודות אחרות בספריה או לא. ג. אם צריך לשנות / לדבג – ייתכן וצריך להבין הרבה קוד בדרך שלא רלוונטי למקרה שלנו.
אפשר להראות כמה דוגמאות של עקרונות ש"עוברים פחות נקי":
מקבילה ל"ניהול תלויות" ברמת הקוד – לא מצאתי בדיוק. הסתפקתי בעקרון של הימנעות ממשתנים גלובליים.
לרעיון של חלוקת מודולים ע"פ "Unit Of Work" (כך שקבוצות פיתוח שונות יוכלו לעבוד במקביל עם מינימום תלות) – אני לא חושב שיש הקבלה אמיתית ברמת קוד.
העיקרון האלמותי של (DRY (Do Not Repeat Yourself הוא "No Brainer" בקוד, אבל הופך לנושא מורכב ולא חד-משמעי ברמת הארכיטקטורה.
בסופו של דבר – יש הרבה מאוד חפיפה בין עקרונות "ארכיטקטורה" לעקרונות "קוד", כך שאין כ"כ חדש ללמוד. הרעיונות חוזרים על עצמם בשינוי אדרת. חלק מהעקרונות (למשל KISS = Keep It Simple Stupid) הם פשוט אוניברסליים.
עדכון: אף עיקרון בעצם לא דורש ש"נגרום לתוכנה לעבוד". כן, כן! גם זה חלק בעל חשיבות 🙂 גם בקוד, גם בתכנון וגם בארכיטקטורה. עד כמה שזה נשמע משעשע – לעתים אנחנו שוכחים את זה (בעיקר "ארכיטקטים").
עדכון2: ארצה להרחיב מעט על העיקרון שנקרא SLAP, לקוראים שלא מכירים אותו. הוא אומר את הדבר הבא: "על כל פונקציה להיות ברמה יחידה של הפשטה". למשל, אם יש לי פונקציה:
הרי שזו חריגה ברורה מהעיקרון. הפונקציה drawItems עוסקת בפרטים: כיצד לצייר פריט אחר פריט. מה פתאום היא מבצעת שמירה לבסיס הנתונים?! (פעולה ברמת הפשטה גבוהה יותר – OMG!)
השינוי, אם כן, שנדרש בקוד הוא להעביר את השורה dbTable.save לפונקציה שביצעה את הקריאה ל drawItem – בהנחה שזו רמת ההפשטה המתאימה.
קושי מסוים בשימוש ב SLAP הוא שאין "מד רמת-הפשטה", כזה שנכוון אותו לשורה בקוד והוא יגיד לנו "רמה 6!" או "רמה 7!". זה הכל בראש שלנו כמפתחים ובני-אדם אינטליגנטים. לפעמים יהיו "סתירות" כך שיוחלט שפעולה X תהיה פעם אחת ברמה n ופעם אחרת ברמה n+1. אני אומר: זה אנושי. זהו עקרון חשוב – פשוט קחו אותו בפרופורציה ("We figured they were more actual guidelines").
סיכום
כפי שאולי אתם שמים לב, התחלתי לאחרונה לתקוף את נושא הארכיטקטורה וה OOD. זה נושא גדול ומורכב, עם זוויות רבות לכל עניין.
במקום להסתגר כשנה וחצי ולהוציא בסוף פוסט באורך של ספר (גישת ה Waterfall), אני מנסה לתקוף את הנושא בצורה אג'ילית: בעזרת nibbles ("ביסים קטנים"). כמו ערמה של דוקים לפרק אחד אחד – עד אשר אוכל להגיע לגרעין הקשה של העניין.
במערכת כלשהי, באזור לו נקרא "Sub-Project 3", מחלקה A (בג'אווה) קראה למתודה במחלקה B, אשר דרשה כפרמטר איזה ערך. הערך יכול להיות אחד מ 3 ערכים קבועים – ועל כן המפתחים יצרו enum (נקרא לו ENUM_X). עד כאן – יפה וטוב.
מפתח אחר גילה שבדיוק אותו enum (נקרא לו 'ENUM_X) מוגדר במקום אחר בפרויקט, ודרש שמחלקות A ו B ישתמשו ב enum המקורי – כך שלא יתוחזק "קוד כפול".
המפתח אשר כתב את הקוד במקור טען: "חבל לייצר reference לעוד תת פרויקט ב Build בשביל כזה דבר קטן. שיהיו שני enums זהים – לא יקרה שום דבר."- "אבל אם תשנה אחד ותשכח את השני?! – מה אז?"
הוויכוח התלהט והגיע לראש הקבוצה (!).
מה דעתכם? במי אתם הייתם מצדדים?
כיצד לסיים את הוויכוח?
לפני שאספר לכם מה הייתה הצעתי (שנדחתה פה-אחד ע"י 2 הצדדים, כדרך אגב) ארחיב את הדילמה:
מי שקצת בקיא ב"תיאוריה" של הנדסת תוכנה או Object Oriented Design (בקיצור OOD) – יכול לטעון:
"שכפול קוד הוא אם כל רוע". "יש עיקרון חשוב שאומר שאין לשכפל קוד: כל שינוי קונספטואלי צריך להתרגם בדיוק לנקודה אחת בקוד בה עושים שינוי". "עקרון זה נקרא Don't Repeat Yourself Principle (בקיצור DRY) – וזהו עיקרון ידוע."
קל להתחבר לטיעון הזה: אותו מכירים אותו, כנראה, מקורס התכנות הראשון שלנו.
האם זהו הטיעון המנצח שיפתור את הדיון?
הממ… לא בטוח.
הנה טיעון מלומד אחר:
"אסור למודול להיות תלוי בחלקי-ממשק שאין לו בהם שימוש". במקרה שלנו יצרנו תלות לא רצויה בכל "Sub-Project 7" – כלומר בהרבה מחלקות וממשקים שאין לנו בהם שימוש. הממ… נשמע חמור!
עיקרון זה נקרא The Interface Segregation Principle.
האם ייתכן שעקרונות ה OOD סותרים זה את זה?
כמה שאלות
האם יכול אדם, המכיר את 2 העקרונות והוא בעל כושר שכנוע, להחליט באופן רגשי במי הוא מצדד וכל פעם לשלוף את "הטיעון התאורטי המתאים" בכדי להנחית "טיעון מנצח"? האם הוא יכול לעשות זאת מבלי להיות מודע לכך ולהאמין שהוא "רק פועל ע"פ התאוריה"?
בהינתן שחוקי ה OOD סותרים לעתים אחד-את-משנהו, האם ישנם חוקים "חזקים יותר" שיש להעדיף?
נניח ונוותר על אחד החוקים או שניהם – איזה "נזק" יתרחש? מה ההשלכות של "לא לציית לחוקים"? האם המאמץ הנוסף שבציות לחוקי ה OOD – משתלם?
האם OOD היא מתודולוגיה מוצלחת? האם, לאחר כל השינויים בשיטות העבודה שחלו בעשור האחרון – היא עדיין יעילה או רלוונטית?
עסקתי הרבה בחיי ב Object Oriented Design: למדתי, למדתי עוד, ניסיתי, יישמתי, שאפתי ליישום "מושלם", הנחיתי אחרים במתודולוגיה וכו'.
עדיין, ברגע זה, כשאני עומד ושואל את עצמי את השאלות הנ"ל – אין לי תשובה ברורה.
במשך שנים, פעלתי ע"פ כללי הנדסת-תוכנה שלמדתי. פעלתי? – נלחמתי בחירוף נפש, אפשר לומר.
ניסיתי להעמיק כמה שיותר ולעשות את המירב.
כיום אני יודע לומר שנצמדתי במידה רבה, לזרם בתוכנה שנקרא "Defensive Programming". זרם ששפת ג'אווה ו JEE היו אולי רגע השיא שלו. הוא מתבטא ברעיונות כגון:
"על המתכנת צריך להגן על התוכנה בפני המפתחים – כולל הוא עצמו".
עשה כל מה שתוכל כדי להפחית סיכונים לבאגים.
גישה זו יצרה הרבה משמעת (discipline), אך גם הובילה להמלצות כגון כתיבת מחלקה singleton בג'אווה בתוך enum על מנת להבטיח singleton "שפשוט אי אפשר לקלקל" [א].
מאז, נחשפתי לזרמים אחרים, אולי כמעט הפוכים – שגם הם יכולים לעבוד יפה. הבנתי (פעם נוספת) שאין אמת אחת.
עקרונות ה OOD – למבחן!
עתה אני ניצב מול עקרונות הOOD המוכרים, ואני רוצה להעמידם במבחן הזמן והרלוונטיות.
בניתי רשימה של העקרונות שאני זוכר / מודע אליהם וקיבצתי אותם, באופן גס, ל 3 קבוצות:
חלוקה הקוד למחלקות או מודולים
(The Single Responsibility Principle (SRP
(Don't Repeat Yourself Principle (DRY
Encapsulation
High-Cohesion / Low-coupling Principle
The Common Closure / Reuse Principle
הגדרת הפשטות (abstactions) / אינטראקציה בין מחלקות
The Open-Closed Principle
The Liskov Substitution Principle
The Release-Reuse Equivalency Principle
The Stable Abstraction Principle
ניהול תלויות (מחלקות עד מודולים במערכות)
The Interface Segregation Principle + גרסת הקוד שלו
באופן גס ניתן לומר שחלוקת הקוד למודולים, הגדרת הפשטות וניהול תלויות היא רוב העבודה ב"הגדרת ארכיטקטורת תוכנה".
נראה לי שאבחר כמה מהעקרונות הנ"ל – ואתחיל לנתח אותם יותר לעומק.
הערות ומחשבות יתקבלו בשמחה.
ליאור
נ.ב: האא כן – איזה פתרון אני הצעתי לדילמה שהצגתי בתחילת הפוסט? אני הצעתי פשוט להשתמש במחרוזת (string) במקום enum וכך להימנע בכלל מתלויות. מה הייתרון של ENUM על מחרוזת פשוטה? Type Safety? ובכן… ה speller ב IDE ימצא את רוב שגיאות הכתיב. בדיקות-היחידה אמורות לתפוס את השאר…
—-
[א] בספר Effective Java 2nd Edition, פריט מס' 3, ע"מ 18. אפשר להתייחס לכל סדרת ספרי ה … Effective – כספרים של זרם ה "Defensive Programming".
הדפדפן החל כאפליקציה קטנה לצפייה בסוג מסמכים מסוים, שנקרא HTML. עם הזמן הפך הדפדפן לשער הכניסה לחלק מכריע מהתוכנות בעולם – כפי שאנחנו מכירים אותו היום. מטרת פוסט זה היא לסייע להבין ממה מורכב הדפדפן וכיצד הוא עובד.
ידע זה איננו ״מדע טילים״, אך הוא יכול להיות שימושי בכדי לבנות אפליקציות ווב טובות יותר.
כמו כל מערכת, הדפדפן מורכב מכמה מודולים מרכזיים. ישנו מספר רב של דפדפנים, אך רובם מחולקים למודולים באופן הדומה למתואר להלן:
Networking
מודול זה אחראי לניהול ה TCP connections (כמה לפתוח, כמה זמן להחזיק,…), ציות לכללי פרוטוקול ה HTTP הקשורים לתקשורת, וסיפוק שירותים מתקדמים כגון תקן webSockets של HTML5. לדפדפנים שונים יש מימושים שונים למודול ה Networking, המשפיעים על אופי התקשורת. ל Chromium (כלומר דפדפן כרום) יש מודול שנקרא Chromium Network Stack ולספארי יש את CFNetwork.
דפדפנים מודרניים מיישמים אופטימיזציות רבות ברמת הרשת:
יצירת connection מוקדם (preconnection) גם מבלי שהמשתמש לחץ על לינק. סיבות אפשריות: משתמש עבר עם העכבר מעל לינק (סיכוי גבוה שהוא ילחץ) או דפים שהמשתמש כמעט תמיד ופותח כשהוא מפעיל את הדפדפן (לדוגמה ynet).
השארה של TCP connection פתוח עוד כמה שניות…. אתרים רבים מבצעים קריאות Ajax זמן קצר לאחר שהאתר נטען (סוג של Lazy Loading). ה Connection כבר "חם" וחבל לסגור אותו.
ניחוש איזה משאב כדאי לבקש מאיזה TCP connection פתוח. לדוגמה: דחייה של בקשה לטעינת תמונות בכמה מאות מילי-שניות, כי אולי תבוא בקשה לטעינת קובץ javaScript או CSS – שיש יתרונות רבים לטעון אותם מוקדם יותר.
Storage
מודול האכסון מנהל אכסון של Caches, של תקני HTML5 כגון Local Storage או ניהול של Cookies או Plugins. למודול זה יש עבודה רבה עבור הדפדפן, אך השפעה קטנה יחסית על מפתח הווב.
Graphics
מודול זה כולל בתוכו הפעלה של ספריות הגרפיקה של מערכת ההפעלה. הן לצורך רינדור פקדים "טבעיים" של מערכת ההפעלה (למשל DropDown List) או פונטים והן לצורך הפעלה של האצת חומרה [א] שתסייע למנוע הרינדור.
מנוע הרצת הג'אווהסקריפט (javaScript Engine, לשעבר javaScript Interpreter)
המודול האחראי להרצת ג׳אווהסקריפט, נעסוק בו בפוסט נפרד.
מנוע הרינדור (Rendering Engine), ידוע גם כ Layout Engine
המנוע שהופך דפי HTML לתמונה על גבי המסך, נעסוק בו בהמשך פוסט זה.
Browser Management Layer
שכבה המנהלת שירותים שונים של הדפדפן כגון:
Bookmarks
ניהול היסטורית גלישה
סנכרון ה Bookmarks מול שרת מרוחק (פיירפוקס, כרום ולאחרונה גם ספארי)
ניהול Plug-ins
התמודדות עם קריסות
וכו'
Browser Shell
בעצם שכבת ה UI, ה "פנים" של הדפדפן כפי שאנו מכירים אותו:
טאבים
כפתורי "קדימה", "אחורה", Home וכו'
תיבת הכתובת / חיפוש וכיצד הן מתנהגות
למשל: הקלדה של כתובת ב Chrome או לחיצה על כפתור "refresh" תגרום לטעינה לדף בהתעלמות מה cache בעוד שטעינת דף בעקבות לחיצה על link תגרום לטעינת הדף תוך שימוש ב cache. התנהגות זו היא חלק מהגדרת ממשק המשתמש.
Viewers שונים, לדוגמה הצגת תמונות או PDF הן לרוב חלק מה Shell ולא ממנוע הרינדור.
מספר לא מבוטל של דפדפנים (maxthon, iCabMobile ולאחרונה גם אופרה) הם וריאציה של דפדפן אחר ומחדשים בעיקר ב Shell ובשכבת הניהול.
ה Shell של פיירפוקס, כפי שהוא נראה על מכשירים שונים.
מנוע הרינדור
אלמנט מרכזי מאוד בעבודת הדפדפן היא עבודתו של מנוע הרינדור (ידוע כ Rendering Engine או Layout Engine). מנוע הרינדור מקבל קובץ HTML וקובצי javaScript, CSS והופך אותם לגרפיקה על המסך.
יש הרבה מנועי רינדור, הנה הרשימה של כמה מהנפוצים שבהם:
אנסה לתאר את העקרונות הבסיסיים מאחורי עבודתם של מנועי הרינדור, שכנראה ונכונים עבור רוב הדפדפנים. בתקופה בה קיימת תחרות רבה ודפדפנים מסוימים משחררים גרסה כל 6 שבועות, כמעט לא ניתן לעקוב אחר השינויים והאופטימיזציות השונות שנעשות בדפדפנים.
פוסט זה מתבסס בעיקר על ההתנהגות הידועה של Firefox ו Chrome.
ה flow המרכזי של מנוע הרינדור הוא כדלהלן:
הדברים מסתבכים כאשר קוד javaScript מעורב. בפוסט זה אתמקד ברינדור של HTML סטטי – מכיוון שזה הבסיס, ובפוסט המשך אתאר כיצד javaScript נכנס לתמונה ליצירת HTML דינמי – תמונה המשקפת את המציאות של Web Applications כיום.
שלבי הטעינה
שלב 1: פענוח קובץ ה HTML ובניית ייצוג היררכי שלו בזיכרון (מבנה נתונים הנקרא DOM Tree)
הנה תרשים המתאר את פעולת הפענוח של קובץ ה HTML. הלוגיקה כוללת נקודות רבות של "התאוששות מתקלות" (שלא באות לביטוי בתרשים למטה) כגון תגיות שלא נסגרו / לא נסגרו בסדר הנכון או הימצאותם של אלמנטים לא תקניים ב markup:
בעבר לא היה תקן אחיד כיצד יש לפרסר HTML, וכבר בתהליך הפירסור היו נקודות שוני בין דפדפנים שונים. HTML5 הגדיר במדויק כיצד פירסור צריך להתבצע ונראה שהדפדפנים המודרניים כיום אחידים למדי ב DOM Tree שהם בונים.
שלב 2: טעינת קבצי CSS
כאשר נתקלים ב DOM באלמנטים של Sytle (קובצי CSS) – מבקשים מרכיב הרשת לטעון אותם. כשהטעינה מסתיימת – תוכן הקובץ נשלח לפענוח.
שלב 3: פענוח קבצי ה CSS
קבצי ה CSS מפוענחים בצורה דומה ל HTML (אם כי התהליך פשוט יותר) ומהם נבנה מבנה נתונים המחזיק את כל החוקים המתוארים בקבצי ה CSS, מבנה הידוע לרוב כ Style Tree או Style Rules. ה nodes ב Style Tree הם פריטי CSS Selectors כגון "p" או "hover:" ועל כל node יש את סט החוקים שפועלים על selection זה, כגון "border-radius: 4px".
שלב 4: חיבור ה Styles ל DOM ובניית ה Rendering Tree
ה Rendering Tree הוא שלב ביניים בין ה DOM Tree וה styles לציור התוכן בפועל על המסך. העץ מורכב מאלמנטים, שכל אחד מהם מתאר מלבן שמאוחר יותר יצויר על המסך:
אלמנטים ב Render Tree מתייחסים לאלמנטים ב DOM Tree, אך יחס זה הוא לא 1:1. ייתכן מצב בו אלמנט יחיד ב DOM (בדוגמה: אלמנט ה HTML) מיוצג ע"י מספר אלמנטים ב Render Tree. מצד שני, אלמנטים שהם hidden (תגים כמו Script או Head שהם מוחבאים כהגדרה או תגים רגילים שהוחבאו בעזרת הגדרה, כגון תכונת "display:none") פשוט לא יתוארו ע"י ה Render Tree. ה Render Tree מתאר רק את מה שאמור להיות מצויר על המסך.
יוצא דופן היא תכונת ה visibility שאיננה גורמת לכך שלא יווצר Render Tree Node. הסיבה לכך היא שע"פ התקן, בן לאלמנט "visibility:hidden" יכול עדיין להיות בעל "visibility:visible" ולהיות מוצג. כדי לחשב כיצד להציג אותו עדיין צריך ב Render Tree את פרטי האב ה"בלתי נראה".
כאשר רוצים להחיל styles על ה DOM Tree, עוברים node אחר node בתוך ה DOM ומחפשים אלו styles רלוונטיים. את החיפוש מתחילים מתחתית העץ (המקבילה לצד הימני של ה Selector) – מכיוון שכך ניתן לפסול מהר יותר Selectors שאינם רלוונטיים.
CSS Selectors וביצועים
המלצה ידועה למפתחי ווב היא:
"השתמשו כמה שיותר ב ids" – חיפוש שלהם הוא הכי מהיר
"אם אי-אפשר להשתמש ב id, השתמשו ב class" – גם הם מהירים
"אח"כ השתמשו בכל השאר."
המלצה זו היא טובה ככלל אצבע, אך יש עוד לדעת ולעשות על מנת לשפר את ביצועי הדף / אפליקציה שלנו.
ids, למשל, הם מהירים מבחינת ביצועים אך לא כ"כ טובים מבחינת תחזוקת קוד. ישנה דעה רווחת שברגע שהדפדפן מצא את ה id המבוקש – הוא מפסיק לחפש, אולם אם תנסו להכניס 2 ids זהים ב HTML (דבר לא-חוקי בעליל) סביר שתראו שהדפדפן מרנדר את ה style נכון – מה שמצביע שזו לא הדרך שבה עובד הדפדפן פנימית. עובדה זו נכונה אולי לפקודה document.getElementById ועל כן היא קצת יותר מהירה.
נראה שהדפדפנים מחזיקים אינדקסים ל Ids ול classes וכך הם מצליחים להחזיר אותם מהר יותר מאשר לטייל על כל ה DOM.
ההשפעה של Selectors מורכבים
בואו נתאר את מבנה ה DOM ורשימת החוקים הבאים:
הערה: השתמשתי בדוגמה בה ה Selectors מה Style Tree מיוצגים כרשימה, כך שיהיה קל יותר לקרוא אותה.
בואו נבחן את הקושי לדעת אם אלמנט בודד ב DOM Tree מתאים ל Selectors:
את 4 ה Selectors הראשונים הכי קל לאשר / לבטל מידית. בחינת ה node הנוכחי מספיקה בכדי להכריע אם החוק תקף או לא. (O(1.
חוקים כמו "sidebar p#" דורשים טיול, פוטנציאלי, עד לראש העץ בכדי לדעת אם הם תקפים, כלומר (O(n למרות שיש לנו שני אלמנטים שראינו קודם שכ"א מחושב ב (O(1 כשהוא לבד.
ביטוי כמו "ul > p" ניתן לבדוק ב (O(2 – בניגוד לביטוי דומה למדי "ul p" שדורש (O(n.
ביטוי מורכב יותר כמו "item div p." ידרוש בתאוריה (O(n^2 מכיוון שעבור כל div בשרשרת האבות צריך להתחיל מחדש חיפוש אחר אב עם class בשם "item", אולם דפדפנים מודרניים יכולים בקלות לבצע אופטימיזציה ל (O(n, מכיוון ששרשרת האבות היא אחת.
עוד מקרה מיוחד selector כגון "hover:" שדורש בדיקה כל זמן שהעכבר זז, מה שעלול להפוך selector כגון "someClass div p:hover." למטרד עבור הדפדפן.
ניתוח זה נועד להבהיר את המחיר של הכנסת כל node חדש ל DOM Tree. כאשר מדובר שאילתות על ה DOM Tree מתוך javaScript, המכיר יכול להיות גדול עד פי מספר ה nodes ב DOMTree.
אם אתם עובדים עם jQuery או Zepto בכדי לתשאל את ה DOM בזמן ריצה (כדאי שתעבדו!) – כדאי להכיר כמה טיפים ספציפיים לגבי ספריות אלו.
מה אפשר לעשות בכדי לסייע ל DOM Tree להבנות מהר יותר?
כל אלמנט חדש שנכנס ל DOM – נבדק ע"י כל החוקים ב Style Tree. זכרו שדפדפנים קוראים את ה selectors מימין לשמאל ונסו שהאלמנט הימני ביותר ב selections שלכם יהיה ספציפי ככל הניתן, כך שיהיה אפשר לבטל אותו מהר.
נסו להימנע מחלקים "ברורים מאליהם" בחוקים שלכם, לדוגמה: אם "sidebar div ul li.item img." זהה בפועל ל "item img." – נסו להשתמש בשני. אני מודה שתיאור מפורט של המבנה לפעמים מסייע לכתוב קוד קריא יותר.
נסו להימנע, אם אפשר, מיצירת DOM Tree עמוק במיוחד.
נסו להימנע מחוקים מסובכים, נאמר:"{ div:nth-of-type(3) ul:last-child li:nth-of-type(odd) *{ font-weight:bold".ע"י חישוב המצב הספציפי בעצמכם וסימון האלמנטים שעומדים בתנאי ב class – אתם יכולים לחסוך לדפדפן הרבה מאוד עבודה בעת הכנסת אלמנטים ל DOM Tree.
עוד כלל חשוב הוא לטעון את כל קבצי ה CSS מוקדם ככל האפשר, ולפני קבצי ה JavaScript בפרט. מדוע?
הוספה של אלמנט ב DOM (נאמר בעקבות קוד ג'אווהסקריפט) גורמת לבנייה של node חדש ב Render Tree, מה שדורש מעבר על כל ה Style Tree, עם האופטימיזציות שהוזכרו לעיל. נציין עבודה זו כ w.
הוספה של חוק ל Style Tree דורשת לעבור על כל האלמנטים ב DOM Tree ולבדוק כל אחד מהם את החוק החדש. אם החוק תקף, יש לבנות מחדש את ה Render Tree nodes של כל הבנים של האלמנט עליו תקף החוק החדש – בגלל שתכונות ב CSS הן נורשות. רק תוספת אחרונה זו היא עבודה של m * w (כאשר m הוא מספר ה nodes שעליהם תקף החוק והבנים שלהם) – הרבה יותר מהכנסה של אלמנט ל DOM Tree.
על כן נעדיף תמיד "לסגור" את ה Render Tree שלנו לפני שאנו "נוגעים" ב DOM.
לדפדפנים יש גם עוד מנגנון שניתן לתאר כ DOM Tree write Buffer (עליו נדבר בפוסט ההמשך) שהופך סדרה של כתיבות ל DOM לזולות יותר.
שלב 5: Layout / Reflow
אלמנטים ב Render Tree יכללו הן את תכונות ה CSS Box Model: גובה, רוחב, עובי מסגרת, ריווח וכו' והן תכונות מיקום (position, float, left, top וכו').
גם ערכי תכונות ה box model וגם ערכי תכונות המיקום מושפעות מאלמנטים אחרים ב Rendering Tree. בתהליך שנקרא Layout או Reflow עובר מנוע הרינדור על כל האלמנטים ב Render Tree ומחשב את הרוחב והמיקום שלהם. תכונות כגון float:right (תכונת מיקום) או height:auto (תכונות גודל) משפיעות רבות על ה Layout שייווצר בסוף.
בתהליך בניית ה Render Tree מבוצע חישוב של הרוחבים של האלמנטים.
רוב האלמנטים ב Render Tree יהיו אחד מ – 2 סוגים:
Inline: אלמנט שתופס את הרוחב והגובה שלו, אך מאפשר להמשיך את ה inline Flow. כלומר: אם יש כמה אלמנטי Inline אחד אחר השני, הם יסתדרו בשורה כל עוד רוחב העמוד מאפשר זאת (ואז יגלשו לשורה הבאה).
על סוגים נוספים, פחות נפוצים, של התנהגות layout ניתן לקרוא כאן.
שלב 6: Canvas & Paint
השלב האחרון של הדפדפן הוא לקחת את ה Render Tree כסדרה של הגדרות ולצייר אותו בפועל על ה canvas של הדפדפן. שלב זה נעשה בעזרת APIs של מערכת ההפעלה ושימוש ב GPU (האצת חומרה) ,אם ניתן – עבור פעולות רינדור פעולות מורכבות כגון Animate או Transform של CSS.
סיכום
בפוסט זה סקרנו את מבנה הדפדפן וה flow החשוב ביותר שלו "הפיכת HTML למסך מרונדר".
בכדי לפשט את הדיון התעלמנו מאלמנט חשוב: האופי הדינמי של הדף הנובע מקוד javaScript.
אנסה בפוסט המשך להוסיף מורכבות נוספת זו להשלמת התמונה.
שיהיה בהצלחה!
—
[א] האצת חומרה (hardware acceleration) היא הפעלת פקודות גרפיות, בעיקר, שהמעבד הגרפי (GPU: Graphical Processing Unit), יחידת עיבוד חיצונית ל CPU שיש כיום כמעט לכל מחשב – יכולה לבצע יותר מהר מה CPU. שימוש ב GPU יכול לשפר משמעותית את ביצועי הרינדור של כל מיני פילטרים מתקדמים של CSS3 כגון Transform או Animations, עבודה עם Canvas או תלת-מימד.
—
מקורות מעניינים נוספים בנושא:
Browsers architecture How browsers works
כתבה קלאסית ומקיפה של טלי גרסיאל על עבודתם של דפדפנים. פול אייריש (גורו ווב שעובד בגוגל) אהב את הכתבה ופרסם אותה מחדש באתר הפופולרי HTML5 Rocks! http://www.html5rocks.com/en/tutorials/internals/howbrowserswork/
מעניין לציין שטלי ואני עבדנו במשך תקופה ״בערך״ באותה החברה (אינקפסולה ואימפרבה), אך לא הזדמן לי להכיר אותה.
התגובות על הפוסט הקודם סייעו לי להבין שלמרות הסיפור על תולדות Require, עדיין לא ברור בדיוק הקשר בין require ל AMD ו CommonJS ומהן האלטרנטיבות השונות הזמינות. אנסה לספק מידע ישיר יותר בשאיפה שהוא יעזור להסיר את העננה.
ניסיתי לתת להרכבת הצבעים משמעות לתיאור הרכבת היכולות
Require הוא מימוש של AMD, תקן המטפל ב-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, אשר נקבע באופן הבא:
אם צוין property של data-main בקובץ ה HTML – מיקום סקריפט ה main יהיה ה baseURL.
אחרת מיקום קובץ ה html יקבע להיות ה baseUrl.
ניתן לקבוע 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 תיעוד מקיף, שהוא מוצלח עבור מי שכבר מכיר את העקרונות הבסיסיים. מטרת פוסט זה היה להשלים את הפער כך שתוכלו מנקודה זו להיעזר בתיעוד הרשמי, ואף ליהנות מהחוויה.
תבנית העיצוב AMD (ראשי תיבות של Async Module Definition) היא בהחלט לא MVC. מדוע עם כן אני עוסק בה בסדרה על MVC?
AMD היא תבנית עיצוב חדשה-יחסית, המיושמת בעיקר בג'אווהסקריפט ובאה לסייע לכתוב כמות גדולה של קוד ג'אווהסקריפט כך שיהיה קל לתחזוקה.
הדמיון ל MVC הוא במטרה – שמירה של סדר בעבודה עם כמות גדולה של קוד. דרך הפעולה – שונה לגמרי. אפשר (וכדאי) להשתמש גם ב MVC וגם ב AMD במקביל על מנת "לעשות סדר" בקוד הג'אווהסקריפט שלנו.
בשנת 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
האם קרה לכם שהיה לכם באג בקוד שנבע מסדר לא-נכון של תגיות ה ב head של קובץ ה HTML?
קרוב לוודאי שבמערכת גדולה יהיו מספר רב של קובצי javaScript ולכן – מספר רב של תלויות. אין דרך ברורה להגדיר קשרים בין קבצי javaScript (כולם נרשמים במרחב זיכרון משותף), כל שקשרים אלו הם לא-מפורשים ואינם קלים לתיעוד או למעקב.
שימוש ב require פוטר אתכם מדאגה לעניין זה. require תטען את הקבצים בסדר הנכון, ע"פ ההגדרות שסיפקתם.
עבור אלו ששמרו על קובצי javaScript ענקיים, require מאפשרת להרבות בקבצים ללא דאגה לניהול שלהם – כך שיהיה קל יותר לפתח את הקוד.
הערה קטנה: require לא נבנתה לנהל קשרים בהם יש cycles, אולם יש "טכניקה" בה ניתן לטעון קבצים עם תלות מעגלית – אם כי בצורה מעט מסורבלת.
#2: טעינה עצלה ומקבילית של קבצי javaScript [ביצועים]
Require מנצלת את היתרון שהגדרתם כבר את התלויות בין הסקריפטים לא רק בכדי לטעון אותם בצורה נכונה, כי גם בכדי לטעון אותם בצורה אופטימלית מבחינת ביצועים.
Require לא תטען קובץ עד לרגע שצריך אותו בפועל (lazy loading).
Require טוענת קבצים בעזרת תכונת ה async של תגית ה – משהו שכמעט בלתי-אפשרי לנהל באופן ידני בפרוייקט גדול.
טעינה דינמית של קבצים מפגישה 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.
בפועל, רוב הפעמים יהיו לנו קוד שגם:
מגדיר מודול.
וגם תלוי במודולים אחרים.
משהו שנראה כך:
קוד זה הוא מעט מסורבל, ויותר גרוע – טומן בתוכו חשיפה לאופי האסינכרוני בו טוענת 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.