
אם אתם חדשים ב- JavaScript, ז'רגון כמו "חבילות מודולים לעומת מעמיסי מודולים", "Webpack לעומת Browserify" ו- "AMD לעומת CommonJS" יכולים להפוך במהירות למכריע.
מערכת מודול ה- JavaScript עשויה להיות מאיימת, אך הבנתה חיונית עבור מפתחי אתרים.
בפוסט זה אפרק עבורך את מילות הבאזז באנגלית רגילה (וכמה דוגמאות קוד). אני מקווה שתמצא את זה מועיל!
הערה: לשם הפשטות זה יחולק לשני חלקים: חלק 1 יצלול להסבר מהם מודולים ומדוע אנו משתמשים בהם. חלק 2 (פורסם בשבוע הבא) יעבור על המשמעות של חבילה של מודולים ודרכים שונות לעשות זאת.
חלק 1: מישהו יכול בבקשה להסביר שוב מה הם מודולים?
מחברים טובים מחלקים את ספריהם לפרקים ומקטעים; מתכנתים טובים מחלקים את התוכניות שלהם למודולים.
כמו פרק ספרים, המודולים הם רק מקבצי מילים (או קוד, לפי העניין).
עם זאת, מודולים טובים הם עצמאיים מאוד עם פונקציונליות מובהקת, המאפשרים לדשדש אותם, להסירם או להוסיף אותם לפי הצורך, מבלי לשבש את המערכת כולה.
מדוע להשתמש במודולים?
יש הרבה יתרונות לשימוש במודולים לטובת בסיס קוד רחב ידיים ותלוי זה בזה. החשובים ביותר, לדעתי, הם:
1) יכולת תחזוקה: בהגדרה, מודול הוא עצמאי. מודול מתוכנן היטב מטרתו להפחית ככל האפשר את התלות בחלקים של בסיס הקוד, כך שהוא יוכל לצמוח ולהשתפר באופן עצמאי. עדכון של מודול יחיד קל בהרבה כאשר המודול מנותק מחלקי קוד אחרים.
אם נחזור לדוגמת הספר שלנו, אם אתה רוצה לעדכן פרק בספר שלך, זה יהיה סיוט אם שינוי קטן בפרק אחד ידרוש ממך גם לצבוט כל פרק אחר. במקום זאת, תרצה לכתוב כל פרק באופן שניתן יהיה לבצע שיפורים מבלי להשפיע על פרקים אחרים.
2) מרחב שמות: ב- JavaScript, משתנים מחוץ לתחום של פונקציה ברמה העליונה הם גלובליים (כלומר, כולם יכולים לגשת אליהם). בגלל זה, מקובל שיש "זיהום מרחב שמות", שבו קוד שאינו קשור לחלוטין משתף משתנים גלובליים.
שיתוף משתנים גלובליים בין קוד שאינו קשור הוא לא גדול בפיתוח.
כפי שנראה בהמשך פוסט זה, מודולים מאפשרים לנו להימנע מזיהום מרחבי השמות על ידי יצירת שטח פרטי עבור המשתנים שלנו.
3) שימוש חוזר: בואו נהיה כנים כאן: כולנו העתקנו קוד שכתבנו בעבר לפרויקטים חדשים בשלב זה או אחר. לדוגמה, בואו נדמיין שהעתקתם כמה שיטות שימוש שכתבתם מפרויקט קודם לפרויקט הנוכחי שלכם.
הכל טוב ויפה, אבל אם אתה מוצא דרך טובה יותר לכתוב חלק כלשהו מהקוד הזה תצטרך לחזור ולזכור לעדכן אותו בכל מקום אחר שכתבת אותו.
זה ללא ספק בזבוז זמן עצום. האם זה לא יהיה הרבה יותר קל אם היה - חכה לזה - מודול שנוכל להשתמש בו שוב ושוב?
כיצד ניתן לשלב מודולים?
ישנן דרכים רבות לשלב מודולים בתוכניות שלך. בואו נעבור כמה מהם:
דפוס מודול
תבנית המודול משמשת לחיקוי מושג הכיתות (מכיוון ש- JavaScript אינו תומך בכיתות באופן מקורי) כך שנוכל לאחסן שיטות ציבוריות ופרטיות ומשתנים בתוך אובייקט יחיד - בדומה לאופן שבו משתמשים בשיעורים בשפות תכנות אחרות כמו Java. או פייתון. זה מאפשר לנו ליצור ממשק API הפונה לציבור עבור השיטות שאנו רוצים לחשוף בפני העולם, תוך סגירה של משתנים ושיטות פרטיים.
ישנן מספר דרכים להשיג את דפוס המודול. בדוגמה ראשונה זו, אשתמש בסגירה אנונימית. זה יעזור לנו להשיג את מטרתנו על ידי הצבת כל הקוד שלנו לפונקציה אנונימית. (זכור: ב- JavaScript, פונקציות הן הדרך היחידה ליצור היקף חדש.)
דוגמה 1: סגירה אנונימית
(function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); }()); // ‘You failed 2 times.’
עם מבנה זה, לפונקציה האנונימית שלנו יש סביבת הערכה משלה או "סגירה", ואז אנו מעריכים אותה מיד. זה מאפשר לנו להסתיר משתנים ממרחב השמות האב (הגלובלי).
מה שנחמד בגישה זו הוא שניתן להשתמש במשתנים מקומיים בתוך פונקציה זו מבלי להחליף בטעות משתנים גלובליים קיימים, ובכל זאת לגשת למשתנים הגלובליים, כך:
var global = 'Hello, I am a global variable :)'; (function () { // We keep these variables private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item}, 0); return 'Your average grade is ' + total / myGrades.length + '.'; } var failing = function(){ var failingGrades = myGrades.filter(function(item) { return item < 70;}); return 'You failed ' + failingGrades.length + ' times.'; } console.log(failing()); console.log(global); }()); // 'You failed 2 times.' // 'Hello, I am a global variable :)'
שים לב שהסוגריים סביב הפונקציה האנונימית נדרשים, מכיוון שהצהרות שמתחילות בפונקציית מילת המפתח נחשבות תמיד כהצהרות פונקציה (זכור, לא תוכל לקבל הצהרות פונקציות ללא שם ב- JavaScript.) כתוצאה, הסוגריים הסובבים יוצרים ביטוי פונקציה במקום זאת. אם אתה סקרן, תוכל לקרוא עוד כאן.
דוגמה 2: ייבוא גלובלי
גישה פופולרית נוספת בה משתמשים ספריות כמו jQuery היא ייבוא גלובלי. זה דומה לסגירה האנונימית שזה עתה ראינו, אלא שעכשיו אנחנו עוברים בגלובליות כפרמטרים:
(function (globalVariable) { // Keep this variables private inside this closure scope var privateFunction = function() { console.log('Shhhh, this is private!'); } // Expose the below methods via the globalVariable interface while // hiding the implementation of the method within the // function() block globalVariable.each = function(collection, iterator) { if (Array.isArray(collection)) { for (var i = 0; i < collection.length; i++) { iterator(collection[i], i, collection); } } else { for (var key in collection) { iterator(collection[key], key, collection); } } }; globalVariable.filter = function(collection, test) { var filtered = []; globalVariable.each(collection, function(item) { if (test(item)) { filtered.push(item); } }); return filtered; }; globalVariable.map = function(collection, iterator) { var mapped = []; globalUtils.each(collection, function(value, key, collection) { mapped.push(iterator(value)); }); return mapped; }; globalVariable.reduce = function(collection, iterator, accumulator) { var startingValueMissing = accumulator === undefined; globalVariable.each(collection, function(item) { if(startingValueMissing) { accumulator = item; startingValueMissing = false; } else { accumulator = iterator(accumulator, item); } }); return accumulator; }; }(globalVariable));
בדוגמה זו, GlobalVariable הוא המשתנה היחיד הגלובלי. היתרון של גישה זו על פני סגירות אנונימיות הוא שאתה מצהיר על המשתנים הגלובליים מראש, מה שהופך אותו לברור קריסטל לאנשים שקוראים את הקוד שלך.
דוגמה 3: ממשק אובייקט
גישה נוספת היא ליצור מודולים באמצעות ממשק עצמים עצמאי, כך:
var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; // Expose these functions via an interface while hiding // the implementation of the module within the function() block return { average: function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }, failing: function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; } } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
כפי שאתה יכול לראות, גישה זו מאפשרת לנו להחליט מה משתנה / שיטות שאנחנו רוצים לשמור על פרטיות (למשל myGrades ) ומה משתנה / שיטות שאנחנו רוצים לחשוף ידי צבת אותם בדוח ההחזרה (למשל ממוצע & נכשל ).
דוגמה 4: גילוי מודל המודול
הדבר דומה מאוד לגישה שלעיל, אלא שהוא מבטיח שכל השיטות והמשתנים נשמרים פרטיים עד לחשיפה מפורשת:
var myGradesCalculate = (function () { // Keep this variable private inside this closure scope var myGrades = [93, 95, 88, 0, 55, 91]; var average = function() { var total = myGrades.reduce(function(accumulator, item) { return accumulator + item; }, 0); return'Your average grade is ' + total / myGrades.length + '.'; }; var failing = function() { var failingGrades = myGrades.filter(function(item) { return item < 70; }); return 'You failed ' + failingGrades.length + ' times.'; }; // Explicitly reveal public pointers to the private functions // that we want to reveal publicly return { average: average, failing: failing } })(); myGradesCalculate.failing(); // 'You failed 2 times.' myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
זה אולי נראה הרבה לקחת, אבל זה רק קצה הקרחון כשמדובר בדפוסי המודולים. להלן כמה מהמשאבים שמצאתי שימושיים בחיפושים שלי:
- לימוד דפוסי עיצוב JavaScript מאת אדי אוסמני: אוצר פרטים בקריאה תמציתית מרשימה
- טוב מספיק על ידי בן שרי: סקירה שימושית עם דוגמאות לשימוש מתקדם בתבנית המודולים
- בלוג של קרל דנלי: סקירה על תבניות המודולים ומשאבים לדפוסי JavaScript אחרים.
CommonJS ו- AMD
לגישות מעל לכל יש דבר משותף: השימוש במשתנה גלובלי בודד לעטוף הקוד שלו בפונקציה, ובכך ליצור לעצמו מרחב שמות פרטי באמצעות היקף סגירה.
אמנם כל גישה יעילה בדרכה שלה, אך יש להן חסרונות.
ראשית, כמפתח, עליך לדעת את סדר התלות הנכון לטעון את הקבצים שלך. לדוגמה, נניח שאתה משתמש ב- Backbone בפרויקט שלך, כך שתכלול את תג הסקריפט של קוד המקור של Backbone בקובץ שלך.
עם זאת, מכיוון שלעמוד השדרה יש תלות קשה ב- Underscore.js, לא ניתן למקם את תג הסקריפט של קובץ ה- Backbone לפני הקובץ Underscore.js.
כמפתח, ניהול תלות והשגת דברים אלה יכולים לפעמים להיות כאב ראש.
חסרון נוסף הוא שהם עדיין יכולים להוביל להתנגשויות במרחב השמות. לדוגמא, מה אם לשניים מהמודולים שלך אותו שם? או מה אם יש לך שתי גרסאות של מודול, ואתה זקוק לשתיהן?
אז אתה בטח תוהה: האם נוכל לעצב דרך לבקש ממשק של מודול מבלי לעבור על ההיקף הגלובלי?
למרבה המזל, התשובה חיובית.
ישנן שתי גישות פופולריות ומיושמות היטב: CommonJS ו- AMD.
CommonJS
CommonJS היא קבוצת עבודה מתנדבת המתכננת ומטמיעה ממשקי API של JavaScript לצורך הצהרת מודולים.
מודול CommonJS הוא למעשה קטע JavaScript רב פעמי שמייצא אובייקטים ספציפיים, מה שהופך אותם לזמינים עבור מודולים אחרים שדורשים בתוכניות שלהם. אם תכנתת ב- Node.js, אתה מכיר מאוד את הפורמט הזה.
עם CommonJS, כל קובץ JavaScript מאחסן מודולים בהקשר המודול הייחודי לו (בדיוק כמו לעטוף אותו בסגירה). בהיקף זה, אנו משתמשים באובייקט module.exports כדי לחשוף מודולים ודורשים לייבא אותם.
כאשר אתה מגדיר מודול CommonJS, זה עשוי להראות בערך כך:
function myModule() { this.hello = function() { return 'hello!'; } this.goodbye = function() { return 'goodbye!'; } } module.exports = myModule;
We use the special object module and place a reference of our function into module.exports. This lets the CommonJS module system know what we want to expose so that other files can consume it.
Then when someone wants to use myModule, they can require it in their file, like so:
var myModule = require('myModule'); var myModuleInstance = new myModule(); myModuleInstance.hello(); // 'hello!' myModuleInstance.goodbye(); // 'goodbye!'
There are two obvious benefits to this approach over the module patterns we discussed before:
1. Avoiding global namespace pollution
2. Making our dependencies explicit
Moreover, the syntax is very compact, which I personally love.
Another thing to note is that CommonJS takes a server-first approach and synchronously loads modules. This matters because if we have three other modules we need to require, it’ll load them one by one.
Now, that works great on the server but, unfortunately, makes it harder to use when writing JavaScript for the browser. Suffice it to say that reading a module from the web takes a lot longer than reading from disk. For as long as the script to load a module is running, it blocks the browser from running anything else until it finishes loading. It behaves this way because the JavaScript thread stops until the code has been loaded. (I’ll cover how we can work around this issue in Part 2 when we discuss module bundling. For now, that’s all we need to know).
AMD
CommonJS is all well and good, but what if we want to load modules asynchronously? The answer is called Asynchronous Module Definition, or AMD for short.
Loading modules using AMD looks something like this:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) { console.log(myModule.hello()); });
What’s happening here is that the define function takes as its first argument an array of each of the module’s dependencies. These dependencies are loaded in the background (in a non-blocking manner), and once loaded define calls the callback function it was given.
Next, the callback function takes, as arguments, the dependencies that were loaded — in our case, myModule and myOtherModule — allowing the function to use these dependencies. Finally, the dependencies themselves must also be defined using the define keyword.
For example, myModule might look like this:
define([], function() { return { hello: function() { console.log('hello'); }, goodbye: function() { console.log('goodbye'); } }; });
So again, unlike CommonJS, AMD takes a browser-first approach alongside asynchronous behavior to get the job done. (Note, there are a lot of people who strongly believe that dynamically loading files piecemeal as you start to run code isn’t favorable, which we’ll explore more when in the next section on module-building).
Aside from asynchronicity, another benefit of AMD is that your modules can be objects, functions, constructors, strings, JSON and many other types, while CommonJS only supports objects as modules.
That being said, AMD isn’t compatible with io, filesystem, and other server-oriented features available via CommonJS, and the function wrapping syntax is a bit more verbose compared to a simple require statement.
UMD
For projects that require you to support both AMD and CommonJS features, there’s yet another format: Universal Module Definition (UMD).
UMD essentially creates a way to use either of the two, while also supporting the global variable definition. As a result, UMD modules are capable of working on both client and server.
Here’s a quick taste of how UMD goes about its business:
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD define(['myModule', 'myOtherModule'], factory); } else if (typeof exports === 'object') { // CommonJS module.exports = factory(require('myModule'), require('myOtherModule')); } else { // Browser globals (Note: root is window) root.returnExports = factory(root.myModule, root.myOtherModule); } }(this, function (myModule, myOtherModule) { // Methods function notHelloOrGoodbye(){}; // A private method function hello(){}; // A public method because it's returned (see below) function goodbye(){}; // A public method because it's returned (see below) // Exposed public methods return { hello: hello, goodbye: goodbye } }));
For more examples of UMD formats, check out this enlightening repo on GitHub.
Native JS
Phew! Are you still around? I haven’t lost you in the woods here? Good! Because we have *one more* type of module to define before we’re done.
As you probably noticed, none of the modules above were native to JavaScript. Instead, we’ve created ways to emulate a modules system by using either the module pattern, CommonJS or AMD.
Fortunately, the smart folks at TC39 (the standards body that defines the syntax and semantics of ECMAScript) have introduced built-in modules with ECMAScript 6 (ES6).
ES6 offers up a variety of possibilities for importing and exporting modules which others have done a great job explaining — here are a few of those resources:
- jsmodules.io
- exploringjs.com
What’s great about ES6 modules relative to CommonJS or AMD is how it manages to offer the best of both worlds: compact and declarative syntax and asynchronous loading, plus added benefits like better support for cyclic dependencies.
Probably my favorite feature of ES6 modules is that imports are live read-only views of the exports. (Compare this to CommonJS, where imports are copies of exports and consequently not alive).
Here’s an example of how that works:
// lib/counter.js var counter = 1; function increment() { counter++; } function decrement() { counter--; } module.exports = { counter: counter, increment: increment, decrement: decrement }; // src/main.js var counter = require('../../lib/counter'); counter.increment(); console.log(counter.counter); // 1
In this example, we basically make two copies of the module: one when we export it, and one when we require it.
Moreover, the copy in main.js is now disconnected from the original module. That’s why even when we increment our counter it still returns 1 — because the counter variable that we imported is a disconnected copy of the counter variable from the module.
So, incrementing the counter will increment it in the module, but won’t increment your copied version. The only way to modify the copied version of the counter variable is to do so manually:
counter.counter++; console.log(counter.counter); // 2
On the other hand, ES6 creates a live read-only view of the modules we import:
// lib/counter.js export let counter = 1; export function increment() { counter++; } export function decrement() { counter--; } // src/main.js import * as counter from '../../counter'; console.log(counter.counter); // 1 counter.increment(); console.log(counter.counter); // 2
Cool stuff, huh? What I find really compelling about live read-only views is how they allow you to split your modules into smaller pieces without losing functionality.
Then you can turn around and merge them again, no problem. It just “works.”
Looking forward: bundling modules
Wow! Where does the time go? That was a wild ride, but I sincerely hope it gave you a better understanding of modules in JavaScript.
In the next section I’ll walk through module bundling, covering core topics including:
- Why we bundle modules
- Different approaches to bundling
- ECMAScript’s module loader API
- …and more. :)
NOTE: To keep things simple, I skipped over some of the nitty-gritty details (think: cyclic dependencies) in this post. If I left out anything important and/or fascinating, please let me know in the comments!