JavaScript-moduler: En begyndervejledning

Hvis du er en nybegynder i JavaScript, kan jargon som "modulbundlere vs. modulindlæsere", "Webpack vs. Browserify" og "AMD vs. CommonJS" hurtigt blive overvældende.

JavaScript-modulsystemet kan være skræmmende, men det er vigtigt for webudviklere at forstå det.

I dette indlæg pakker jeg disse buzzwords ud til dig på almindelig engelsk (og et par kodeeksempler). Jeg håber, du finder det nyttigt!

Bemærk: For enkelheds skyld deles dette i to sektioner: Del 1 vil dykke ned i at forklare, hvad moduler er, og hvorfor vi bruger dem. Del 2 (udgivet i næste uge) gennemgår, hvad det betyder at samle moduler og de forskellige måder at gøre det på.

Del 1: Kan nogen venligst forklare, hvad moduler er igen?

Gode ​​forfattere opdeler deres bøger i kapitler og sektioner; gode programmører deler deres programmer i moduler.

Ligesom et bogkapitel er moduler kun klynger af ord (eller kode, alt efter tilfældet).

Gode ​​moduler er dog meget selvstændige med tydelig funktionalitet, så de kan blandes, fjernes eller tilføjes efter behov uden at forstyrre systemet som helhed.

Hvorfor bruge moduler?

Der er mange fordele ved at bruge moduler til fordel for en spredt, indbyrdes afhængig kodebase. De vigtigste er efter min mening:

1) Vedligeholdelse: Per definition er et modul selvstændigt. Et veldesignet modul sigter mod at mindske afhængighederne på dele af kodebasen så meget som muligt, så det kan vokse og forbedre sig uafhængigt. Opdatering af et enkelt modul er meget lettere, når modulet frakobles fra andre kodestykker.

Hvis vi vender tilbage til vores bogeksempel, hvis du vil opdatere et kapitel i din bog, ville det være et mareridt, hvis en lille ændring til et kapitel krævede, at du også tilpasser hvert andet kapitel. I stedet for vil du skrive hvert kapitel på en sådan måde, at forbedringer kan foretages uden at påvirke andre kapitler.

2) Navneområde: I JavaScript er variabler uden for rækkevidden af ​​en topfunktions global (hvilket betyder, at alle kan få adgang til dem). På grund af dette er det almindeligt at have "namespace-forurening", hvor helt uafhængig kode deler globale variabler.

Deling af globale variabler mellem ikke-relateret kode er et stort nej-nej i udvikling.

Som vi vil se senere i dette indlæg, tillader moduler os at undgå navneområdet forurening ved at skabe et privat rum til vores variabler.

3) Genanvendelighed: Lad os være ærlige her: Vi har alle kopieret kode, som vi tidligere har skrevet til nye projekter på et eller andet tidspunkt. Lad os for eksempel forestille dig, at du har kopieret nogle hjælpemetoder, du skrev fra et tidligere projekt til dit nuværende projekt.

Det er godt og godt, men hvis du finder en bedre måde at skrive en del af den kode på, skal du gå tilbage og huske at opdatere den overalt, hvor du skrev den.

Dette er naturligvis et enormt spild af tid. Ville det ikke være meget lettere, hvis der - vent på det - var et modul, som vi kan genbruge igen og igen?

Hvordan kan du indarbejde moduler?

Der er mange måder at integrere moduler i dine programmer på. Lad os gå gennem et par af dem:

Modulmønster

Modulmønsteret bruges til at efterligne konceptet med klasser (da JavaScript ikke understøtter klasser), så vi kan gemme både offentlige og private metoder og variabler i et enkelt objekt - svarende til hvordan klasser bruges i andre programmeringssprog som Java eller Python. Det giver os mulighed for at oprette et offentligt vendt API til de metoder, vi vil eksponere for verden, mens vi stadig indkapsler private variabler og metoder i et lukningsomfang.

Der er flere måder at opnå modulmønsteret på. I dette første eksempel bruger jeg en anonym lukning. Det hjælper os med at nå vores mål ved at placere al vores kode i en anonym funktion. (Husk: i JavaScript er funktioner den eneste måde at oprette nyt omfang på.)

Eksempel 1: Anonym lukning

(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.’

Med denne konstruktion har vores anonyme funktion sit eget evalueringsmiljø eller "lukning", og derefter vurderer vi det straks. Dette giver os mulighed for at skjule variabler fra det overordnede (globale) navneområde.

Hvad der er rart ved denne tilgang er, at du kan bruge lokale variabler inde i denne funktion uden ved et uheld at overskrive eksisterende globale variabler, men alligevel stadig få adgang til de globale variabler som sådan:

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 :)'

Bemærk, at parentesen omkring den anonyme funktion er påkrævet, fordi udsagn, der begynder med nøgleordsfunktionen , altid betragtes som funktionserklæringer (husk, du kan ikke have unavngivne funktionserklæringer i JavaScript.) Derfor skaber de omgivende parenteser et funktionsudtryk. i stedet. Hvis du er nysgerrig, kan du læse mere her.

Eksempel 2: Global import

En anden populær tilgang, der bruges af biblioteker som jQuery, er global import. Det ligner den anonyme lukning, vi lige så, undtagen nu passerer vi i globaler som parametre:

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

I dette eksempel er globalVariable den eneste variabel, der er global. Fordelen ved denne tilgang i forhold til anonyme lukninger er, at du erklærer de globale variabler på forhånd, hvilket gør det krystalklart for folk, der læser din kode.

Eksempel 3: Objektgrænseflade

Endnu en anden tilgang er at oprette moduler ved hjælp af en selvstændig objektgrænseflade som sådan:

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

Som du kan se, giver denne tilgang os mulighed for at bestemme, hvilke variabler / metoder vi vil holde private (f.eks. MyGrades ), og hvilke variabler / metoder vi vil eksponere ved at placere dem i returopgørelsen (f.eks. Gennemsnit og ikke ).

Eksempel 4: Afsløring af modulmønster

Dette svarer meget til ovenstående tilgang, bortset fra at det sikrer, at alle metoder og variabler holdes private, indtil de udtrykkeligt udsættes for:

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

Det kan virke som meget at tage i, men det er bare toppen af ​​isbjerget, når det kommer til modulmønstre. Her er et par af de ressourcer, jeg fandt nyttige i mine egne udforskninger:

  • Læring af JavaScript-designmønstre af Addy Osmani: en skat af detaljer i en imponerende kortfattet læsning
  • Tilstrækkeligt godt af Ben Cherry: en nyttig oversigt med eksempler på avanceret brug af modulmønsteret
  • Blog af Carl Danley: modulmønsteroversigt og ressourcer til andre JavaScript-mønstre.

CommonJS og AMD

Fremgangsmåderne frem for alt har én ting til fælles: brugen af ​​en enkelt global variabel til at indpakke koden i en funktion og derved skabe et privat navneområde for sig selv ved hjælp af et lukningsomfang.

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.

Så kan du vende om og flette dem igen, ikke noget problem. Det “virker” bare.

Ser frem: bundling af moduler

Wow! Hvor går tiden hen? Det var en vild tur, men jeg håber inderligt, at det gav dig en bedre forståelse af moduler i JavaScript.

I det næste afsnit gennemgår jeg modulbundning og dækker kerneemner, herunder:

  • Hvorfor samler vi moduler
  • Forskellige tilgange til bundling
  • ECMAScript's modul loader API
  • …og mere. :)

BEMÆRK: For at holde tingene enkle sprang jeg over nogle af de detaljerede detaljer (tænk: cykliske afhængigheder) i dette indlæg. Hvis jeg har udeladt noget vigtigt og / eller fascinerende, så lad mig det vide i kommentarerne!