Ενότητες JavaScript: Ένας οδηγός για αρχάριους

Εάν είστε αρχάριος στη JavaScript, η ορολογία όπως "bundlers λειτουργικών μονάδων έναντι φορτωτών λειτουργικών μονάδων", "Webpack εναντίον Browserify" και "AMD εναντίον CommonJS" μπορεί γρήγορα να γίνει συντριπτική.

Το σύστημα λειτουργικής μονάδας JavaScript μπορεί να είναι εκφοβιστικό, αλλά η κατανόησή του είναι ζωτικής σημασίας για τους προγραμματιστές ιστού.

Σε αυτήν την ανάρτηση, θα αποσυμπιέσω αυτά τα λέξεις-κλειδιά για εσάς στα απλά αγγλικά (και μερικά δείγματα κώδικα). Ελπίζω να το βρείτε χρήσιμο!

Σημείωση: για λόγους απλότητας, αυτό θα χωριστεί σε δύο ενότητες: Το Μέρος 1 θα βυθιστεί για να εξηγήσει ποιες ενότητες είναι και γιατί τις χρησιμοποιούμε. Το μέρος 2 (δημοσιεύτηκε την επόμενη εβδομάδα) θα αναλύσει τι σημαίνει να συνδυάσετε τις ενότητες και τους διαφορετικούς τρόπους για να το κάνετε.

Μέρος 1: Μπορεί κάποιος να εξηγήσει ποιες είναι οι ενότητες ξανά;

Οι καλοί συγγραφείς χωρίζουν τα βιβλία τους σε κεφάλαια και ενότητες. οι καλοί προγραμματιστές χωρίζουν τα προγράμματά τους σε ενότητες.

Όπως ένα κεφάλαιο βιβλίου, οι ενότητες είναι μόνο ομάδες λέξεων (ή κωδικός, ανάλογα με την περίπτωση).

Οι καλές μονάδες, ωστόσο, είναι πολύ αυτόνομες με ξεχωριστή λειτουργικότητα, επιτρέποντάς τους να ανακατεύονται, να αφαιρούνται ή να προστίθενται όπως απαιτείται, χωρίς να διαταράσσεται το σύστημα στο σύνολό του.

Γιατί να χρησιμοποιήσετε τις ενότητες;

Υπάρχουν πολλά οφέλη από τη χρήση ενοτήτων υπέρ μιας εκτεταμένης, αλληλεξαρτώμενης βάσης κώδικα. Τα πιο σημαντικά, κατά τη γνώμη μου, είναι:

1) Συντηρησιμότητα: Εξ ορισμού, μια ενότητα είναι αυτόνομη. Μια καλά σχεδιασμένη μονάδα στοχεύει στη μείωση όσο το δυνατόν περισσότερο των εξαρτήσεων από τμήματα της βάσης κώδικα, έτσι ώστε να μπορεί να αναπτυχθεί και να βελτιωθεί ανεξάρτητα. Η ενημέρωση μιας μονάδας είναι πολύ πιο εύκολη όταν η μονάδα αποσυνδέεται από άλλα κομμάτια κώδικα.

Επιστρέφοντας στο παράδειγμα του βιβλίου μας, εάν θέλετε να ενημερώσετε ένα κεφάλαιο στο βιβλίο σας, θα ήταν εφιάλτης εάν μια μικρή αλλαγή σε ένα κεφάλαιο απαιτούσε να τροποποιήσετε και κάθε άλλο κεφάλαιο. Αντ 'αυτού, θα θέλατε να γράψετε κάθε κεφάλαιο με τέτοιο τρόπο ώστε να μπορούσαν να γίνουν βελτιώσεις χωρίς να επηρεαστούν άλλα κεφάλαια.

2) Χώρος ονομάτων: Στο JavaScript, οι μεταβλητές που δεν εμπίπτουν στο πεδίο μιας συνάρτησης ανώτατου επιπέδου είναι καθολικές (που σημαίνει ότι όλοι μπορούν να έχουν πρόσβαση σε αυτές). Εξαιτίας αυτού, είναι σύνηθες να υπάρχει «ρύπανση χώρου ονομάτων», όπου εντελώς άσχετος κώδικας μοιράζεται παγκόσμιες μεταβλητές.

Η κοινή χρήση καθολικών μεταβλητών μεταξύ άσχετου κώδικα είναι ένα μεγάλο όχι-όχι στην ανάπτυξη.

Όπως θα δούμε αργότερα σε αυτήν την ανάρτηση, οι ενότητες μας επιτρέπουν να αποφύγουμε τη ρύπανση του χώρου ονομάτων δημιουργώντας έναν ιδιωτικό χώρο για τις μεταβλητές μας.

3) Επαναχρησιμοποίηση: Ας είμαστε ειλικρινείς εδώ: όλοι αντιγράψαμε κώδικα που είχαμε γράψει προηγουμένως σε νέα έργα σε ένα σημείο ή άλλο. Για παράδειγμα, ας υποθέσουμε ότι αντιγράψατε ορισμένες βοηθητικές μεθόδους που έχετε γράψει από ένα προηγούμενο έργο στο τρέχον έργο σας.

Όλα αυτά είναι καλά και καλά, αλλά αν βρείτε έναν καλύτερο τρόπο για να γράψετε κάποιο μέρος αυτού του κώδικα, θα πρέπει να επιστρέψετε και να θυμάστε να το ενημερώνετε οπουδήποτε αλλού το έχετε γράψει.

Αυτό είναι προφανώς τεράστιο χάσιμο χρόνου. Δεν θα ήταν πολύ πιο εύκολο αν υπήρχε - περιμένετε - μια ενότητα που μπορούμε να επαναχρησιμοποιήσουμε ξανά και ξανά;

Πώς μπορείτε να ενσωματώσετε ενότητες;

Υπάρχουν πολλοί τρόποι ενσωμάτωσης ενοτήτων στα προγράμματά σας. Ας δούμε μερικά από αυτά:

Σχέδιο ενότητας

Το μοτίβο Module χρησιμοποιείται για να μιμείται την έννοια των τάξεων (δεδομένου ότι η JavaScript δεν υποστηρίζει εγγενώς τάξεις) έτσι ώστε να μπορούμε να αποθηκεύουμε δημόσιες και ιδιωτικές μεθόδους και μεταβλητές μέσα σε ένα μόνο αντικείμενο - παρόμοιο με το πώς χρησιμοποιούνται οι κλάσεις σε άλλες γλώσσες προγραμματισμού όπως η Java ή Python. Αυτό μας επιτρέπει να δημιουργήσουμε ένα 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: Παγκόσμια εισαγωγή

Another popular approach used by libraries like jQuery is global import. It’s similar to the anonymous closure we just saw, except now we pass in globals as parameters:

(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)); 

In this example, globalVariable is the only variable that’s global. The benefit of this approach over anonymous closures is that you declare the global variables upfront, making it crystal clear to people reading your code.

Example 3: Object interface

Yet another approach is to create modules using a self-contained object interface, like so:

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.'

As you can see, this approach lets us decide what variables/methods we want to keep private (e.g. myGrades) and what variables/methods we want to expose by putting them in the return statement (e.g. average & failing).

Example 4: Revealing module pattern

This is very similar to the above approach, except that it ensures all methods and variables are kept private until explicitly exposed:

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.'

That may seem like a lot to take in, but it’s just the tip of the iceberg when it comes to module patterns. Here are a few of the resources I found useful in my own explorations:

  • Learning JavaScript Design Patterns by Addy Osmani: a treasure trove of details in an impressively succinct read
  • Adequately Good by Ben Cherry: a useful overview with examples of advanced usage of the module pattern
  • Blog of Carl Danley: module pattern overview and resources for other JavaScript patterns.

CommonJS and AMD

The approaches above all have one thing in common: the use of a single global variable to wrap its code in a function, thereby creating a private namespace for itself using a closure scope.

While each approach is effective in its own way, they have their downsides.

For one, as a developer, you need to know the right dependency order to load your files in. For instance, let’s say you’re using Backbone in your project, so you include the script tag for Backbone’s source code in your file.

However, since Backbone has a hard dependency on Underscore.js, the script tag for the Backbone file can’t be placed before the Underscore.js file.

As a developer, managing dependencies and getting these things right can sometimes be a headache.

Another downside is that they can still lead to namespace collisions. For example, what if two of your modules have the same name? Or what if you have two versions of a module, and you need both?

So you’re probably wondering: can we design a way to ask for a module’s interface without going through the global scope?

Fortunately, the answer is yes.

There are two popular and well-implemented approaches: CommonJS and AMD.

CommonJS

CommonJS is a volunteer working group that designs and implements JavaScript APIs for declaring modules.

A CommonJS module is essentially a reusable piece of JavaScript which exports specific objects, making them available for other modules to require in their programs. If you’ve programmed in Node.js, you’ll be very familiar with this format.

With CommonJS, each JavaScript file stores modules in its own unique module context (just like wrapping it in a closure). In this scope, we use the module.exports object to expose modules, and require to import them.

When you’re defining a CommonJS module, it might look something like this:

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.

Τότε μπορείτε να γυρίσετε και να τα συγχωνεύσετε ξανά, χωρίς πρόβλημα. Απλώς «λειτουργεί».

Ανυπομονώ: ενοποίηση ενοτήτων

Ουάου! Πού πηγαίνει ο χρόνος; Ήταν μια άγρια ​​βόλτα, αλλά ελπίζω ειλικρινά να σας δώσει μια καλύτερη κατανόηση των ενοτήτων σε JavaScript.

Στην επόμενη ενότητα θα αναλύσω τη δέσμη στοιχείων, καλύπτοντας βασικά θέματα, όπως:

  • Γιατί ομαδοποιούμε τις ενότητες
  • Διαφορετικές προσεγγίσεις για ομαδοποίηση
  • API φόρτωσης λειτουργικών μονάδων ECMAScript
  • …κι αλλα. :)

ΣΗΜΕΙΩΣΗ: Για να διατηρήσω τα πράγματα απλά, παραλείψαμε μερικές από τις λεπτομέρειες του σκελετού (σκεφτείτε: κυκλικές εξαρτήσεις) σε αυτήν την ανάρτηση Αν άφησα κάτι σημαντικό και / ή συναρπαστικό, ενημερώστε με στα σχόλια!