2013-04-30

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


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




2013-04-26

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
האם קרה לכם שהיה לכם באג בקוד שנבע מסדר לא-נכון של תגיות ה <script> ב head של קובץ ה HTML?
קרוב לוודאי שבמערכת גדולה יהיו מספר רב של קובצי javaScript ולכן - מספר רב של תלויות. אין דרך ברורה להגדיר קשרים בין קבצי javaScript (כולם נרשמים במרחב זיכרון משותף), כל שקשרים אלו הם לא-מפורשים ואינם קלים לתיעוד או למעקב.

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

הערה קטנה: require לא נבנתה לנהל קשרים בהם יש cycles, אולם יש "טכניקה" בה ניתן לטעון קבצים עם תלות מעגלית - אם כי בצורה מעט מסורבלת.


#2: טעינה עצלה ומקבילית של קבצי javaScript [ביצועים]
Require מנצלת את היתרון שהגדרתם כבר את התלויות בין הסקריפטים לא רק בכדי לטעון אותם בצורה נכונה, כי גם בכדי לטעון אותם בצורה אופטימלית מבחינת ביצועים.
  • Require לא תטען קובץ עד לרגע שצריך אותו בפועל (lazy loading).
  • Require טוענת קבצים בעזרת תכונת ה async של תגית ה <script> - משהו שכמעט בלתי-אפשרי לנהל באופן ידני בפרוייקט גדול.

טעינה דינמית של קבצים מפגישה 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.


2013-04-08

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

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

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





בהצלחה!



2013-04-07

אבני הבניין של האינטרנט: 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
Host: www.lyrics.com
User-Agent: My-Home-Made-Browser/0.1a
Content-Type: text/plain
Referer: http://softwarearchiblog.com

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

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


----

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

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

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