אבני הבניין של האינטרנט: הדפדפן

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

המלצה ידועה למפתחי ווב היא:

  1. "השתמשו כמה שיותר ב ids" – חיפוש שלהם הוא הכי מהיר
  2. "אם אי-אפשר להשתמש ב id, השתמשו ב class" – גם הם מהירים
  3. "אח"כ השתמשו בכל השאר."

המלצה זו היא טובה ככלל אצבע, אך יש עוד לדעת ולעשות על מנת לשפר את ביצועי הדף / אפליקציה שלנו.

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 וכו').

What is Box model in CSS? | Lil Engine

גם ערכי תכונות ה box model וגם ערכי תכונות המיקום מושפעות מאלמנטים אחרים ב Rendering Tree. בתהליך שנקרא Layout או Reflow עובר מנוע הרינדור על כל האלמנטים ב Render Tree ומחשב את הרוחב והמיקום שלהם. תכונות כגון float:right (תכונת מיקום) או height:auto (תכונות גודל) משפיעות רבות על ה Layout שייווצר בסוף.
בתהליך בניית ה Render Tree מבוצע חישוב של הרוחבים של האלמנטים.
רוב האלמנטים ב Render Tree יהיו אחד מ – 2 סוגים:

  • Inline: אלמנט שתופס את הרוחב והגובה שלו, אך מאפשר להמשיך את ה inline Flow. כלומר: אם יש כמה אלמנטי Inline אחד אחר השני, הם יסתדרו בשורה כל עוד רוחב העמוד מאפשר זאת (ואז יגלשו לשורה הבאה).
  • Block: אלמנטים הדורשים שורה משלהם.
מקור (הכולל מידע נוסף): https://www.webdesignerdepot.com/2012/09/when-pages-are-not-paper-the-designers-guide-to-layout-code/

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

(How Webkit works (ppt
קצת פרטים ספציפיים על Webkit
https://docs.google.com/presentation/pub?id=1ZRIQbUKw9Tf077odCh66OrrwRIVNLvI_nhLm2Gi__F0#slide=id.p

About Layers
תיאור מפורט כיצד Webkit מבצע את הרינדור ל Canvas – נושא שדילגתי עליו בפוסט זה.
http://www.html5rocks.com/en/tutorials/speed/layers/

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

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

AMD ו Require.js

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

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

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

הקדמה

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

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

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

עם אלו בעיות ServerJS מנסה להתמודד?

  • הגדרה של מודולים (modules) וחבילות (packages) בכדי לארגן את הקוד. חבילות הן קבוצות של מודולים.
  • כתיבת בדיקות-יחידה.
  • כלי עזר לכתיבת קוד אסינכרוני כך שהקוד יישאר מודולרי  – אותו כיסיתי בפוסט מקביליות עם jQuery (ובכלל) (תקן ה Promises/A, היחסית-מפורסם)
  • עבודה עם מערכות קבצים.
  • טעינה דינמית של קוד.
  • ועוד כמה…
אחד התקנים בעל ההשפעה הרבה ביותר הוא תקן בשם Modules/1.1, תקן המתאר כיצד להגדיר מודולים. צורך זה הוא בסיסי מאוד והרבה frameworks משתמשים ב "CommonJS Sytle" (כלומר – בתחביר של התקן, או כזו שדומה לו מאוד) על מנת להגדיר מודולים של קוד ג'אווהסקריפט.
אנשי הדפדפנים, התאימו את Modules/1.1 לעולם הדפדפן (ישנם כמה הבדלי התנהגות חשובים) וקראו לו: Async Module Definition, או בקיצור: AMD [א].
רק להסביר: AMD היא הגדרה המתארת API לטעינה דינמית של מודולים – אך אין מאחורי AMD קוד. ל AMD יש מימושים רבים בדמות ספריות כמו: lsjs, curl, require, dojo ועוד.
המימוש הבולט ביותר ל AMD היא ספרייה בשם require.js.
כיום require.js היא הספרייה הנפוצה ביותר, בפער גדול, על שאר האלטרנטיבות. המצב מזכיר במעט את המצב של jQuery מול MooTools או Prototype – תקן "דה-פאקטו".

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

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

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

בפועל, רוב הפעמים יהיו לנו קוד שגם:

  1. מגדיר מודול.
  2. וגם תלוי במודולים אחרים.

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

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

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

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

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

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

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

זהו, סיימנו!

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

סיכום

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

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

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

סדרה: אבני הבניין של האינטרנט

פיתוח לאינטרנט נחשב לפני כעשור לנישה: אם "בוני אתרים" ואם "מפתחי אתרים דינמיים".

כיום נראה שחלק נכבד מהמפתחים בעולם יוצרים מערכות שנחשפות דרך טכנולוגיות אינטרנט אם כ UI ואם כ Service, גם ברשת הפנימית האירגונית (אינראנט) וגם בחשיפה עולמית (אינטרנט).

בשנים האחרונות יש חזרה רבתי לפיתוח UI ב HTML, CSS וג'אווהסקריפט. מפתחים ממהרים ללמוד טכנולוגיות כגון ג'אווהסקריפט, MVC צד-לקוח, ספריות לפיתוח UI / מובייל, REST ועוד. טכנולוגיות שהתחדשו בשנים האחרונות.

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

הבסיס של טכנולוגיות האינטרנט הם כמה מהטכנולוגיות שאסקור בסדרה זו.

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

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

ה URL הוא אולי ברור מאליו כאשר מקלידים בדפדפן כתובת של אתר אהוב, אך כאשר מפתחים קוד שקורא או מרכיב URLs – ישנם כמה חוקים ועקרונות שחשוב להכיר!

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

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

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

רינדור בצד הדפדפן – המשך לפוסט הקודם בסדרה המסביר כיצד מנוע הרינדור וה DOM עובדים כאשר רץ קוד ג'אווהסקריפט.
פוסט זה כולל תובנות רבות לגבי Performance.

סביבת הריצה של ג'אווהסקריפט – מבט לתוך מנועי הג'אווהסקריפט והבנה כיצד הם עובדים. מהו ה Event Loops וכיצד interval של 100ms יכול לחזור בשנייה רק פעמיים?!
הבנת סביבת הריצה של ג'אווהסקריפט יכולה למנוע מכם כמה חוסרי-הבנה עם הדפדפן ולסייע לכתוב אפליקציות רספונסיביות יותר

The Single Origin Policy הידוע בקיצור SOP הוא מנגנון אבטחה חשוב המגן על אפליקציות הווב שלנו. מצד שני – הוא מגביל ומקשה על תקשורת Cross-Domain.
מה המשמעות של SOP? כיצד מתמודדים עם המגבלות שלו?
הכל בפוסט SOP ותקשורת Cross-Domain

TBF – על HTTP מתקדם, caches ו cookiez

בהצלחה!

אבני הבניין של האינטרנט: HTTP בסיסי

פרוטוקול ה HTTP הוא הפרוטוקול האחראי באינטרנט על שיתוף דפי HTML.
רשת האינטרנט נבנתה בחשיבה על מספר רב של שירותים: דואר אלקטרוני, צ׳אט (IRC), שיתוף קבצים (FTP), תקשורת טקסט (Telnet) ועוד. אני לא חושב שבימים הראשונים של האינטרנט היה ברור איזה תפקיד משמעותי יהיה ל World Wide Web, לדפי ה HTML.למען האמת, בימיו הראשונים היה ל HTTP מתחרה משמעותי בשם Gopher. לפרוטוקול זה הייתה מטרה מקבילה (שיתוף "מסמכים") והוא לא היה מבוסס על HTML, אם כי עם טקסט ללא עיצוב. היו לו דפדפנים משלו, ולא הייתה סיבה משמעותית להניח שדווקא HTTP יצא מהמאבק כשידו על העליונה.

בראשית ימיו (HTTP 0.9, שנת 1991) פרוטוקול ה HTTP היה בסיסי ביותר ונועד רק להעביר קבצי HTML (ללא קבצי CSS או js) בצורה סטנדרטית על גבי פרוטוקול TCP. מסמך ה Specification שלו היה באורך עמוד וחצי.פחות מעשור לאחר מכן (1999), הוגדר פרוטוקול HTTP 1.1, שמסמך ה Specification שלו הוא באורך 360 עמודים – עליה תלולה במורכבות ובאחידות של הפרוטוקול.

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

פוסט זה הוא חלק מהסדרה: אבני הבניין של האינטרנט.

On the Wire

בואו נתבונן כיצד נראה פרוטוקול HTTP בפעולה.
אתייחס ל HTTP גרסה 1.1 (לא נראה לי שיצא לכם להיתקל במשהו אחר), אולם אנסה להדגיש את הפונקציה הבסיסית ביותר של הפרוטוקול שהוגדרה עוד בגרסה 0.9.
פרוטוקול HTTP כולל מחזור קבוע: בקשה ותשובה, שתיהן נשלחות כטקסט פשוט על גבי TCP Connection.
בקשה נראית כך:

POST /lyrics_server/my_song.php HTTP/1.1
User-Agent: My-Home-Made-Browser/0.1a
Content-Type: text/plain
text="Att ahla Hamuda!"&action=post

אתם יכולים לכתוב קוד פשוט בשפת התכנות החביבה עליכם שיבצע קריאות HTTP. כל שעליכם לעשות הוא לפתוח TCP socket, לשלוח מחרוזת טקסט בפורמט הבקשה, ולפרסר את פורמט התשובה (שנראה בהמשך).
שימו לב שהבקשה בנויה בפורמט הבא:

<http method> <url> <http version>
<headers>
<empty line>
<body>

השורה הראשונה נקראת Start Line ומכילה פרטים בסיסיים לגבי הבקשה. בדוגמה זו יש קריאת POST ("שליחת מידע למשאב") שכוללת טקסט בדמות ה body. קריאות GET, למשל, הן בקשות לקריאת נתונים מהשרת, ולרוב לא יכללו body (אך עדיין יכללו את השורה הריקה).

HTTP Methods

פרוטוקול HTTP מתאר משאבים (resources, שם נרדף אחר הוא מסמכים documents), המתוארים ע"י URL ופעולות (method או verb) שניתן לבצע עליהם. המשאב יכול להיות קובץ "'קיים" / "סטטי" (למשל קובץ HTML, CSS) או קובץ שנוצר באופן דינמי עבור כל בקשה (למשל דו"ח מצב העובדים). אין מניעה שקבצי HTML או CSS ייווצרו גם הם באופן דינאמי, וזה בדיוק מה שעושים שרתי אינטרנט כמו Tomcat, IIS בעזרת טכנולוגיות כמו ASP.NET או Servlet בהתאמה.

HTTP/1.0 הגדיר את המתודות הבאות:

  • GET – קריאת ה resource.
  • POST – שליחת מידע ל resource לצורך עדכון / ביצוע פעולה כלשהי (הגדרה דיי כללית).
  • HEAD – כמו GET רק ללא קבלת ה body, שזו סוג של "סימולציה" מה היה קורה לו היינו קוראים ל GET. טוב לבדיקה אם URL הוא תקין, למשל.

תקן HTTP/1.1 הגדיר מתודות נוספות:

  • OPTIONS – בקשת מידע על הפעולות האפשריות על המשאב.
  • PUT – שמירה של מידע כמשאב ב URL המסופק.
  • DELETE – מחיקה של המשאב. כנראה שאתם צריכים הרשאות מתאימות בכדי לעשות זאת.
  • TRACE – תאורטית משמשת ל debug, אולם בפועל לא כ"כ בשימוש בגלל סכנות אבטחה שבשימוש בה.
  • CONNECT – מתודה טכנית שנועדה לתמוך ב HTTP Tunneling.
בפועל, השימוש העיקרי באתרים או באפליקציות ווב הוא במתודות GET ו POST. אני זוכר ש Flex גרסה 3 בכלל לא אפשרה לשלוח קריאה לשרת עם מתודה אחרת, והיא כנראה לא הייתה הטכנולוגיה היחידה.
מתודת HEAD יכולה להיות שימושית לפעמים, ומתודות PUT ו DELETE הן שימושיות למדי כאשר מדובר בארכיטקטורת REST. בדרך כלל יהיה מדובר ב web service שהאפליקציה שלנו ניגשת אליו.

אינני רוצה לגלוש ל REST, לכך הקדשתי 2 פוסטים אחרים.

אלמנט חשוב מאוד במתודות ה HTTP הוא הקטלוג שלהם ל2 משפחות:

  • Safe Methods: שהן GET, HEAD ו OPTIONS.
  • Unsafe Methods: שהן: POST, DELETE ו PUT.
כחלק מהחוזה ש HTTP מספק נקבע שהפעלת ״מתודות בטוחות" לא אמורה לשנות את ה state בצד-השרת, בעוד הפעלה של מתודות "לא-בטוחות" עשויות לעשות זאת (אבל לא חייבות).

למשל, קרוב לוודאי שיצא לכם להיתקל במצב בו לחצתם על "Refresh", אך הדפדפן פתח לכם popup שהודיע שייתכן וישלח מידע פעם נוספת לשרת ושאל האם אתם רוצים להמשיך. קרוב לוודאי שלחצתם "yes", אלא אם בדיוק בשלב של אישור עסקה כספית גדולה.

מה שקרה הוא שדף אינטרנט שהוצג לכם, בעת שלחצתם על refresh, הוא תוצאה של פעולת HTTP "לא בטוחה" (כנראה POST). מכיוון ש Refresh שולח את המידע לשרת שוב, הדפדפן מתריע על הסכנה לשינוי שלא בכוונה של ה state של המשאב בשרת.

פתרון מקובל לבעיה היא "תבנית עיצוב" שנקראת (Post/Redirect/Get (PRG בה לאחר פעולת POST השרת מבצע פעולת redirect (קוד 302 שמתואר בהמשך) לעמוד GET שאין שום מניעה לעשות Refresh.

הבדל חשוב לא פחות הוא שתוצאות של פעולות "בטוחות" ניתן לשמור כ Cache. רכיבי רשת שונים (gateway, proxy, cdn) עשויים לשמור עותק מקומי של תוצאות של פעולות בטוחות ולהחזיר את התשובה עבור בקשות עתידיות. כמובן שחשוב שיהיה תהליך invalidation בו לאחר זמן, התוצאות שנשמרו ב cache יזרקו לטובת תוצאות מעודכנות יותר.

אם אתם כותבים קוד צד-לקוח, שמתשאל את השרת בעזרת מתודת POST (בגלל שאתם צריכים לשלוח פרמטרים) – התוצאה של השאילתה לא תישמר "ברשת", עובדה שיכולה להיות משמעותית מאוד עבור ביצועים (במיוחד ברשתות WAN) ועבור ה Scalability של השרת שצריך לחשב ולנפק את אותה התשובה פעם אחר פעם.

הדרך הנכונה היא לשלוח פקודות GET ולהשתמש ב Query String על מנת לשלוח פרמטרים. לא אמורה להיות לכם כיום מגבלה מעשית בצורת עבודה זו.

שורה ריקה ושורות חדשות

תקן HTTP 0.9 קבע שכל שורה צריכה להסתיים ב LF (בשפות תכנות רבות: "n\\") או ב CRLF (בשפות תכנות רבות: "r\\n\\"). תקן 1.1 קבע שגם CR (בשפות רבות: "r\\") הוא סוף שורה תקני. כיצד יודעים אם CRLF הוא סיום שורה רגיל או "השורה הריקה" המיוחלת? אין לכך תשובה חד משמעית.

בנוסף יש את החוק המבלבל הבא: שורה שהתחילה ב white space (טאב או רווח) נחשבת כהמשך לשורה הקודמת, למרות שהיה line break (אחד מהשלושה) קודם לכן.

מעניין לציין ששני שרתי הווב הנפוצים בעולם (Apache ו IIS) לא מכבדים את התקן ולא מאפשרים לשלוח CR בלבד, בכדי להגן מהבעיות שיכולות להיווצר בגישה הזו. ישנה משפחה של התקפות אבטחה שנקראות header injection שמנסות לגרום לדחיפה של סימני LF, CR ו CRLF מקומות שונים בתוך ה headers ולגרום לקוד שמפרסר אותם להתבלבל ולשגות.

Request Headers

כפי שציינו, הבקשה כוללת רשימה של Headers, כל אחד הוא שורה. ה Header היחידי שהוא חובה הוא header ה Host – כל השאר הם אופציונליים.

הסיבה ששולחים את Host על ה URL היא מצב שנקרא Shared Hosting או Virtual Hosting: דמיינו מצב בו שרת אחד מארח 2 או יותר אתרי אינטרנט (מצב מקובל מאוד בימי הענן), ששני ה hostnames שלהם מקונפגים לאותו ה IP Address על שרתי ה DNS. מכיוון שהשרת המארח מקבל רק את הכתובת היחסית של האתר, שלרוב תהייה פשוט "/" או "index.html" – הוא לא היה יודע לאיזה אתר מתייחסים. Header ה Host מאפשר לו לדעת לאיזה אתר התכוונו ולהפנות את הבקשה לאתר הנכון.
ה Host Header לא היה קיים ב HTTP 1.0 וגרם לבעיות, החל מ HTTP 1.1 הוא חובה.

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

בקריאה לפרוקסי תכלול שורת הפתיחה URL מלא הכולל את ה Host. עבור הדוגמה למעלה, שורת הפתיחה תראה כך:

POST http://www.lyrics.com/lyrics_server/queen_song.php HTTP/1.1

באופן זה ידע שרת הפרוקסי לנתב נכון את הקריאה.
אמנם יש כאן כפילות עם ה "Host Header" שנוסף ב HTTP 1.1. מצופה משרתי Proxy לוודא שה host בשורת הפתיחה וב Host Header הם זהים.

תהליך חשוב שמתרחש בעת החלפת ה headers ב request וה response הוא תהליך שנקרא "Content Negotiation". בתהליך זה ה client (כנראה דפדפן, אבל לא בהכרח) מצהיר באילו סוגי תוכן ובאיזה פורמטים הוא תומך. השרת ינסה להיענות לדרישה כמיטב יכולתו. הנה קבוצת headers שכזו לדוגמה:

Accept: text/html, application/xhtml+xml, application/xml; q=0.9, *.*;q=0.8
Accept-Language: he, en-us;q=0.8, en;q=0.6
Accept-Charset: ISO-112, utf-8;q=0.7,*;q=0.3
Accept-Encoding: gzip, deflate

שימו לב ש "Accept-Charset" הוא ה header שמצביע על סוג הקידוד (encoding) בעוד "Accept-Encoding" מתאר דווקא את סוג הדחיסה (compression). סוג של חוסר-דיוק מינוחי שנעשה בתקן.

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

Accept-Language: he, en-us;q=0.8, en;q=0.6

q הוא מדד בין 0 ל 1 שמבטא את חוזק ההעדפה (quantity?) עבור ה content negotiation.
כלומר ב header זה הדפדפן אומר: "אחי, אני הכי רוצה עברית (q לא צויין = 1) אבל אני מוכן לקבל אנגלית אמריקאית כהעדפה קצת יותר נמוכה או אנגלית-סתם כהעדפה שלישית.

Headers שכדאי לציין הם:

  • Accept (נקרא גם Accept-Media) – מציין את רשימת ה media types (נקראים גם MIME types) שה client תומך בהם. * משמעה: "תן לי הכל, אני אסתדר כמיטב יכולתי".רשימת ה MIME types נראית קצת מסורבלת, וכנראה שאם תשלחו MIME type שגוי הדפדפן יידע להסתדר (הוא מפענח את התחילה של הקובץ כדי לדעת איזה סוג קובץ זה באמת, בלי קשר ל MIME type שנשלח) – אבל זהו התקן ורוב השרתים מצייתים לחלק זה.
  • Accept-Encoding – סוגי דחיסה שונים שהדפדפן יודע לפתוח. קבצים נדחסים על מנת להעביר פחות מידע על גבי הרשת. הדחיסה המועדפת היא gzip (שזה וריאציה קלה-לפענוח של אלגוריתם הדחיסה ZIP). אין טעם לציין כאן q מכיוון שלדפדפן באמת אין העדפה.
  • User-Agent – זהו header קצת חריג בנוף של ה content negotiation: במקום לתאר יכולת של ה Agent כגון encoding או supported media types, הוא מתאר פרטים מזהים על ה Agent כמו מנוע הרינדור שה agent משתמש בו או מערכת ההפעלה שהוא רץ עליה. בעזרת פרטים אלו יכול השרת לנחש ולנסות להתאים את התוכן טוב יותר ל agent.

אומר זאת בבירור: User Agent הוא סוג של Anti-Pattern בו עושים שימוש נפוץ בכדי להתאים תוכן של אתרים / אפליקציות לדפדפן ספציפי / מערכת הפעלה, על בסיס הידיעה כיצד מתנהג כל דפדפן (באיזו גרסה?).
זיהוי דפדפן (למשל IE7) הוא לא משימה קלה מכיוון שבעוד שבמחשב שלכם IE7 ייצר User Agent String (בקיצור UA) אחד, למשל:

Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.2; Win64; x64; Trident/6.0; .NET4.0E)
במחשב אחר (לא עוד disk image מאותו IT) הוא עלול לייצר UA שנראה כך:
Mozilla/4.0 (compatible; MSIE 7.0b; Windows NT 6.0)
"אין בעיה!", תאמרו לעצמכם. "MSIE 7" תמיד יופיע ועליו נסתמך. מה שכן, הנה UA שהגיע מדפדפן IE8:
Mozilla/5.0 (compatible; MSIE 7.0; Windows NT 5.0; Trident/4.0; FBSMTWB; msn OptimizedIE8; ENUS)
גם הוא מכיל משום מה "תאימות ל MSIE 7".

לפני שאתם משחררים קוד המתבסס על UA, כדאי שתבדקו את ה regular expressions שלכם מול בנק של UAs כגון לינק1 או לינק2. הדרך הנכונה לעבוד היא להשתמש בספריות כגון modernizr, שבודקות בזמן ריצה האם יכולות ספציפיות נתמכות בדפדפן כגון SVG או Touch ולא לנסות לנחש זאת דרך ה UA.

עדיין שאלות חשובות כגון ״האם מדובר ב Smartphone או ב Tablet?״ לא יכולות להיענות בצורה טובה ללא גישה ל UA – אבל כדאי לעשות זאת בזהירות.

ניתן לגשת ל UA גם מתוך ג'אווהסקריפט בעזרת פקודת navigation.useragent.

גם ג'ון רזיג, היוצר של jQuery, לא "מת" על השימוש ב UA. מקור: twitter

Headers חשובים אחרים ב HTTP Request הם:

  • If-Modified-Since – בעזרת header זה הדפדפן מצהיר: "יש לי את הקובץ הזה (זכרו שקריאת HTTP היא גם לתמונות או מסמכים, לא רק קבצי HTML) מתאריך abc, שלח לי אותו חזרה רק אם הוא השתנה מאז". אם הקובץ לא השתנה, ה response יחזור עם body ריק אבל עם קוד 304 (not-modified) שיאמר לדפדפן להשתמש במשאב שכבר ברשותו, מכיוון שהוא מעודכן.
  • Referer – זיהוי האתר שהפנה את הבקשה (בהנחה שבקשת ה HTTP היא תוצאה של לחיצה על לינק). מידע זה הוא שימושי למדי לכלי Web Analytics. בעלי אתרים רוצים לדעת אילו מפרסמים יעילים יותר עבור האתר שלהם. שימו לב ששגיאת הכתיב (באנגלית יש לכתוב Referrer) היא שגיאה שנעשתה במקור בתקן – ונשתמרה.
  • Cookie ו Caches – שני נושאים בעלי נפח שראוי להקדיש להם דיון עצמאי.
  • (X-forward-for (XFF – פעמים רבות, לפני השרת שלנו יישב Reverse Proxy או Load Balancer המנטרים את התעבורה. מכיוון שכלפי-חוץ (האינטרנט) אותו רכיב רשת הוא היעד הסופי של התעבורה, ה packet של פרוטוקול ה IP יציין את רכיב הרשת בתור היעד ולא את השרת שלנו. רכיב הרשת ישלח שוב את אותה הבקשה אלינו ושם הוא יציין את המקור (ברמת פרוטוקול ה IP) ככתובת שלו, ולא כתובת השולח המקורי. XFF נועד "to patch" מגבלה זו של פרוטוקול IP ולתאר לנו את המקור האמיתי של התעבורה.

אם אתם רוצים לבחון את ה Headers של המערכת שלכם, ניתן לעשות זאת בקלות:

הקישו ב F12 בדפדפן בכדי לפתוח את כלי הפיתוח. בכרום, למשל, לכו לטאב ה Network ולחצו F5 כדי לטעון שוב את הדף. עכשיו תראו את רשימת כל קריאות ה HTTP, כאשר בכל אחת ניתן לראות את ה Headers שנשלחו. בדפדפנים אחרים הדרך למצוא את ה Haders אינה שונה בהרבה.

HTTP Response

חפרנו דיי הרבה בבקשת ה HTTP, בואו נעבור על כמה נקודות מעניינות בתשובה. פורמט התשובה הוא דומה למדי:

<http version> <status code> <reason = status text>
<headers>
<empty line>
<body>

ההבדל הניכר הוא בשורת הפתיחה, ובסט של Headers אפשריים שהוא שונה מאלו שנשלחים ב Request.

HTTP/1.1 200 OK
Server: Some-Server/0.9.2
Content-Type: text/plain
Connection: close
<DOCTYPE html><html><head></head><body><p>
This is currently an <b>html</b> document, but http response body can include images, css files, binary files etc.<p>
</body><html>
מקור: http://www.gog.com/error/404
שורת הפתיחה
שורת הפתיחה כוללת את גרסת הפרוטוקול, קוד סטטוס (מספרי) וטקסט שמתאר את הסטטוס.
אמנם כמעט תמיד נראה את התיאור "OK" עם קוד 200 ("הכל בסדר"), אבל אין מניעה לשלוח טקסט אחר. שדה זה נועד בכדי לשלוח הודעות שגיאה טקסטואליות כאשר עובדים, למשל, עם REST API. הודעת שגיאה כמו ״Bad Request״ היא לא מועילה וכדאי להחליף אותה.

מבין כ-50 הקודים שמוגדרים בפרוטוקול ה HTTP יש אולי 15 שבאמת שימושיים בפועל, בואו נעבור עליהם בקצרה:
  • 200 – "הכל בסדר": התוכן שנתבקש אמור להגיע ב body
  • 204 – No Content: כאשר אין ערך החזרה. הדפדפן לא ינווט לכתובת החדשה אלא יישאר בכתובת האחרונה שהיה.
  • 206 – Partial Content: כמו 200, רק משמש עבור בקשות של range בתוך המסמך, המבוצעות על בסיס ה header שנקרא Content-Range.
  • 301, 302 או 303 – הדפדפן מציין שה URL למשאב זה הוחלף, וה URL החדש מצויים ב header בשם Location. דפדפנים יפנו אוטומטית לכתובת החדשה, מבלי שהמשתמש מודע לכך, מה שנקרא "Client-Side ReDirect". אם הפעולה הייתה פעולת POST, הדפדפן יגש ל URL החדש בפעולת GET (כדי למנוע שליחה כפולה של הנתונים). דפים שמציגים למשתמש הודעה "…You are being redirected" מחזירים בעצם קוד 200, ופעולת ה Redirect היא בעצם אפליקטיבית (בעזרת קוד ג'אווהסקריפט, לרוב).
  • 304 – not modified: ראו את ההסבר אודות סטטוס זה תחת ההסבר על ה header בשם If-Modified-Since למעלה.
  • 307 – Temporary Redirect: כמו 301-303 רק ללא ביטול פעולת ה POST.
  • 400 – bad request: השרת מציין שמשהו לא בסדר בבקשה שנשלחה, אולי הפורמט ואולי תוכן הבקשה. הפירוט אמור להגיע בהודעת השגיאה.
  • 401 – Unauthorized: השרת לא מזהה את המשתמש ולכן אינו יכול לאפשר לו לגשת למשאב שהגישה אליו מוגבלת. קוד זה יזניק חלון "שם/ססמה" או יהיה נקודת הפתיחה לסט קריאות מורכב כחלק מהפעלת פרוטוקולים לאימות המשתמש כגון Kerberos או SAML.
  • 403 – Forbidden: השרת מזהה את המשתמש, אך למשתמש אין הרשאות לגשת למשאב.
  • 404 – לא קיים משאב בשם שתואר ב URL. תשאלו כל ילד בן 12 ומטה, והוא יסביר לכם 🙂
  • 405 – Method not allowed: פעולת ה HTTP שביקשנו (למשל Delete) לא זמינה למשאב הספציפי. ייתכן והיא תהיה זמינה עבור משאב (URL) אחר.
  • 500 – Internal Error: תקלה פנימית בשרת, שיכולה להיות בעצם כל דבר. התקלה לרוב תכתב לקובצי הלוג של השרת ולא תשלח (מטעמי בטיחות) למשתמש. אל תצפו להודעת שגיאה מפורטת או מועילה על קודים אלו.
  • 503 – Service Unavailable: כמו 500, אך בא להדגיש שמדובר בתקלה זמנית בגלל שאיזה service בדיוק נמצא ב maintainable או restart, למשל.

כמובן שקודים אלו הם קודים טכניים, שלא רבים מהמשתמשים מכירים או מבינים. אתרי אינטרנט יעדיפו להחזיר תמיד קוד 200 ולהחזיר דף HTML המתאר בצורה קלה ונוחה (חחחח, לפעמים זו הודעה מעוצבת בה כתוב רק "404"), מהי התקלה. קודי HTTP, אם כן, רלוונטיים יותר לשירותי REST.
בעצם, גם שירותים שקוראים לעצמם "REST" לא תמיד משתמשים בקודים בצורה נכונה. אפשר לומר שיש כאן כוונה טובה והרבה מקום לשיפור. בלינק זה תוכלו למצוא את ההגדרה התקנית לכל הקודים. שימו לב ששרתים או פרוטוקולים מסוימים אמצו לעצמם קודים נוספים, את הנפוצים שבהם תוכלו למצוא כאן.

Headers מעניינים בתשובה

הנה כמה מה Headers המעניינים שיכולים לחזור בתשובה:

  • Content-Encoding, Content-Language, Content-Type ועוד – המקבילים ל Headers של ה Content Negotiation בבקשה, אולם הפעם מצוינת ההחלטה. לא יהיו יותר q לבטא את מידת הרצון כי אם החלטה: "התוכן הוא HTML בצרפתית והוא דחוס ב gzip", לדוגמה.
    ה Content Type יתואר בעזרת אחד מה MIME types, כמו שהזכרנו קודם.
  • Server – ממש כמו user agent – רק בצד השרת. מתאר איזה שרת זה ואיזו גרסה.
  • מעולם לא ראיתי קוד שעשה שימוש במידע זה, אך למי שמדבג פלטפורמה מרוחקת מידע זה עשוי להיות שימושי.
  • Content-Length – אורך ה body. יכול לסייע ל client להיערך בהקצאת זיכרון.
  • Cache-Control, Pragma, ETag – אלו headers המתארים את היכולת לשמור את התוכן ב cache בצד הלקוח, ופרטים אודות התקפות שלו. נושא בפני עצמו.

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

סיכום

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

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

מי שרוצה להתעניין קצת יותר, ממליץ להמשיך לפוסטים:

מבנה ה URL (אולי תופתעו שלא הכל ידוע)
HTTPS (חלק א', ב', וג')

—-

קישורים רלוונטיים

מדריך מוצלח לבחירת Status Code ל API שלכם:
http://racksburg.com/choosing-an-http-status-code/

Status Codes בהם משתמשים APIs מפורסמים:
https://gist.github.com/vkostyukov/32c84c0c01789425c29a