AMD ו-Require.js

מבוא נאיבי ומופשט ל-Require.js, מה הקשר ל-AMD וכיצד אנשי הדפדפנים קשורים לכך?

מקור: flickr, cc-by Robert Banh

מקור: flickr, cc-by Robert Banh

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

הדמיון ל-MVC הוא במטרה – שמירה של סדר בעבודה עם כמות גדולה של קוד. דרך הפעולה – שונה לגמרי. אפשר (וכדאי) להשתמש גם ב-MVC וגם ב-AMD במקביל על מנת “לעשות סדר” בקוד הג’אווהסקריפט שלנו.

הקדמה

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

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

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

  • הגדרה של מודולים (modules) וחבילות (packages) בכדי לארגן את הקוד. חבילות הן קבוצות של מודולים.
  • כתיבת בדיקות-יחידה.
  • כלי עזר לכתיבת קוד אסינכרוני כך שהקוד יישאר מודולרי.
  • עבודה עם מערכות קבצים.
  • טעינה דינמית של קוד.
  • ועוד כמה…

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

נניח שיש לי קוד שזקוק למודולים 1 ו 2 בכדי לפעול, כיצד הוא מתאר תלות זו וגורם להם להטען? פשוט מאוד:

מקור: ליאור בר-און

מקור: ליאור בר-און

הפונקציה בדוגמה היא callback שתפעל רק לאחר שהקוד של מודולים 1 ו-2 נטען ואותחל. m1 הוא reference ל-מודול1 (אותו object literal שהוחזר ב -return וחושף את החלקים הציבוריים) ו -m1 הוא reference למודול2. השיוך נעשה ע”פ סדר הפרמטרים. doStuff היא כבר סתם פונקציה שעושה משהו עם m1 ו-m2.

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

מקור: ליאור בר-און

מקור: ליאור בר-און

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

מקור: ליאור בר-און

מקור: ליאור בר-און

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

  • כאשר אנו רוצים לטעון מודולים רק בהסתעפות מסוימת בקוד (ולכן איננו יודעים בוודאות על צורך זה בשורה הראשונה).
  • עבור הקובץ הראשון בתוכנה שלנו. כלומר: פונקציית ה-“main”.
הדרך המקובלת ביותר לטעון את require ב-HTML היא באופן הבא:
מקור: ליאור בר-און

מקור: ליאור בר-און

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

מקור: ליאור בר-און

מקור: ליאור בר-און

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

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

סיכום

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

—-

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

הפוסט פורסם לראשונה בבלוג Software Archiblog.

ליאור בר-און

ליאור בר-און הוא Chief Architect בחברת סטארטאפ ישראלית גדולה.

הגב

6 תגובות על "AMD ו-Require.js"

avatar
Photo and Image Files
 
 
 
Audio and Video Files
 
 
 
Other File Types
 
 
 

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

סידור לפי:   חדש | ישן | הכי מדורגים
גרסיו
Guest

אפשר לתת לינק לכן: http://requirejs.org/docs/whyamd.html
יותר ברור.

Hen Reu
Guest

קודם כל תודה.

שניתכל אי אפשר להגיד : "config? – היא לא כ"כ חשובה"

כל ההגדרות יושבות שם? אם יש לך ספרייה חיצונית שלא תלויה ב- amd (שאגב לא הזכרת את זה וחבל…)
אז חייבים להגדיר אותה ב- config. זה חלק מאוד חשוב בבנית תוכנה עם AMD.

Adiel Ozana
Guest
יש לציין שיש ביקורת רבה לשימוש בשיטות ה AMD בעולם החדש של ה web מן הטיעונים שהוזכרו במאמר בצורה פעוטה של טעינה מרובה שיוצרת overhead עצום על השרת. כיוון שבכל קריאת http נשלחים גם ה headers אנחנו מעוניינים לצמצם כמה שיותר את הקריאות לשרת (ככה למשל נולדה השיטה image sprite שמחברת כמה תמונות לתמונה אחת בדיוק בגלל הסיבה הזו) כיום לדוגמא גם אם נשתמש בספריית הפח jquery+jquery UI יחד באותו פרוייקט – אם נחבר את כל הקבצים לקובץ אחד ונעשה minify ונגדיר בשרת להחזיר תוכן מכווץ באמצעות gzip (כל האחרונות אגב מבוצעות ע"י מספר פעולות פשוטות ביותר) אנחנו נגיע לקובץ… Read more »
Hen Reu
Guest
א. אם יש לך פתרון יותר טוב לסידור קבצי js אז תציע… ב. אתה מסתכל על פרוייקט כמשהו קטן, תסתכל על התמונה הגדולה, ברגע שיש לך X מפתחים שכל אחד מתעסק עם משהו אחר, הסיכוי לזהם את ה- namespace גדול מאוד, הסיכוי לא לרשום בצורה מודולרית גדול, הסיכוי שברגע שתצטרךלהכניס תיקייה חדשה ולא תדע איפה י זה יצור תלויות גדול… ולכן אתה חייב משהו שיסדר את כל הפרוייקט ויתווה לאנשים את הדרך הנכונה לעבודה. ג. מבחינת performence יש לך את r.js שעושה אחלה של עבודה, אני אל מכיר את זה יותר מדי לעומק, אבל קראתי והיו הרבה ביקורות טובות. ד.… Read more »
Adiel Ozana
Guest
קודם כל ברור שאין אמת אחת – אמרתי את דעתי בתור מפתח web וכמובן שאני בטוח שיש אנשים שבאמת אולי נעזרים בשטות הזו שנקראת require.js – מה שמפריע לי היא הגישה האבסולוטית להתמודדות עם בעיות מאוד מורכבות בפיתוח באמצעות גישת קסם כמו "פשוט תשתמש ב[הכנס כאן את ה buzzword החדש בתעשייה] והבעיות שלך יפתרו. עכשיו לשאלותיך: א. יש הרבה שיטות טובות בהרבה – השיטה המתוחכמת והחדשנית: שימוש במודולים של commonjs ל browser – כגון browserify או prime (אתה מוזמן לחפש ב github אם אתה לא מכיר) – ככה יוצא שאתה כותב כמו node.js (כלומר עם מודולים סגורים שרצים בפונקציות אנונימיות… Read more »
Hen Reu
Guest

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

wpDiscuz

תגיות לכתבה: