JavaScript-moduler: En nybegynnerveiledning

Hvis du er en nykommer i JavaScript, kan sjargong som "modulbundlere vs modullastere", "Webpack vs. Browserify" og "AMD vs. CommonJS" raskt bli overveldende.

JavaScript-modulsystemet kan være skremmende, men å forstå det er viktig for webutviklere.

I dette innlegget skal jeg pakke ut disse moteordene for deg på vanlig engelsk (og noen få kodeeksempler). Jeg håper du synes det er nyttig!

Merk: For enkelhets skyld vil dette bli delt inn i to seksjoner: Del 1 vil dykke ned i å forklare hva moduler er og hvorfor vi bruker dem. Del 2 (lagt ut neste uke) vil gå gjennom hva det betyr å pakke moduler og de forskjellige måtene å gjøre det på.

Del 1: Kan noen forklare hva moduler er igjen?

Gode ​​forfattere deler inn bøkene sine i kapitler og seksjoner; gode programmerere deler programmene sine i moduler.

I likhet med et bokkapittel er moduler bare klynger av ord (eller kode, alt etter omstendighetene).

Gode ​​moduler er imidlertid svært selvstendige med tydelig funksjonalitet, slik at de kan blandes, fjernes eller legges til etter behov, uten å forstyrre systemet som helhet.

Hvorfor bruke moduler?

Det er mange fordeler ved å bruke moduler til fordel for en viltvoksende, gjensidig avhengig kodebase. De viktigste, etter min mening, er:

1) Vedlikehold: Per definisjon er en modul selvstendig. En godt designet modul tar sikte på å redusere avhengigheten av deler av kodebasen så mye som mulig, slik at den kan vokse og forbedre seg uavhengig. Oppdatering av en enkelt modul er mye enklere når modulen kobles fra andre kodestykker.

Hvis du ønsker å oppdatere et kapittel i boken vår, vil det være et mareritt hvis en liten endring i ett kapittel krevde at du også tilpasser hvert annet kapittel. I stedet vil du skrive hvert kapittel på en slik måte at forbedringer kan gjøres uten å påvirke andre kapitler.

2) Navneavstand: I JavaScript er variabler utenfor omfanget av en toppnivåfunksjon globale (det vil si at alle har tilgang til dem). På grunn av dette er det vanlig å ha "namespace-forurensning", der helt urelatert kode deler globale variabler.

Deling av globale variabler mellom urelatert kode er et stort nei-nei i utvikling.

Som vi vil se senere i dette innlegget, lar modulene oss unngå forurensning av navneområdet ved å lage et privat rom for variablene våre.

3) Gjenbrukbarhet: La oss være ærlige her: Vi har alle kopiert koden vi tidligere skrev til nye prosjekter på et eller annet tidspunkt. La oss for eksempel forestille deg at du kopierte noen verktøymetoder du skrev fra et forrige prosjekt til ditt nåværende prosjekt.

Det er vel og bra, men hvis du finner en bedre måte å skrive en del av den koden på, må du gå tilbake og huske å oppdatere den overalt ellers du skrev den.

Dette er åpenbart et enormt bortkastet tid. Ville det ikke vært mye enklere hvis det var - vent på det - en modul som vi kan bruke om og om igjen?

Hvordan kan du innlemme moduler?

Det er mange måter å innlemme moduler i programmene dine. La oss gå gjennom noen få av dem:

Modulmønster

Modulmønsteret brukes til å etterligne klassekonseptet (siden JavaScript ikke støtter klasser), slik at vi kan lagre både offentlige og private metoder og variabler i et enkelt objekt - i likhet med hvordan klasser brukes i andre programmeringsspråk som Java eller Python. Det gjør at vi kan lage et offentlig vendt API for metodene vi vil eksponere for verden, mens vi fortsatt innkapsler private variabler og metoder i et lukkingsomfang.

Det er flere måter å oppnå modulmønsteret på. I dette første eksemplet vil jeg bruke en anonym nedleggelse. Det vil hjelpe oss med å nå vårt mål ved å sette all koden vår i en anonym funksjon. (Husk: i JavaScript er funksjoner den eneste måten å skape nytt omfang.)

Eksempel 1: Anonym nedleggelse

(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 konstruksjonen har vår anonyme funksjon sitt eget evalueringsmiljø eller "nedleggelse", og deretter vurderer vi den umiddelbart. Dette lar oss skjule variabler fra det overordnede (globale) navneområdet.

Det som er hyggelig med denne tilnærmingen er at du kan bruke lokale variabler i denne funksjonen uten å overskrive eksisterende globale variabler ved et uhell, men likevel få tilgang til de globale variablene, slik:

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

Merk at parentesen rundt anonym funksjon er nødvendig, fordi uttalelser som begynner med nøkkelordet funksjon er alltid anses å være funksjons erklæringer (husk, du kan ikke ha navngitte funksjons erklæringer i Javascript.) Følgelig, de omkringliggende parentes skape et funksjonsuttrykk i stedet. Hvis du er nysgjerrig, kan du lese mer her.

Eksempel 2: Global import

En annen populær tilnærming som brukes av biblioteker som jQuery er global import. Det ligner på den anonyme nedleggelsen vi nettopp så, bortsett fra nå passerer vi globaler som parametere:

(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 eksemplet er globalVariable den eneste variabelen som er global. Fordelen med denne tilnærmingen over anonyme nedleggelser er at du erklærer de globale variablene på forhånd, noe som gjør det krystallklart for folk som leser koden din.

Eksempel 3: Objektgrensesnitt

Nok en tilnærming er å lage moduler ved hjelp av et selvstendig objektgrensesnitt, slik:

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, lar denne tilnærmingen oss bestemme hvilke variabler / metoder vi ønsker å holde private (f.eks. MyGrades ) og hvilke variabler / metoder vi vil eksponere ved å sette dem i returoppgaven (f.eks. Gjennomsnitt og sviktende ).

Eksempel 4: Avdekke modulmønster

Dette ligner veldig på ovennevnte tilnærming, bortsett fra at det sikrer at alle metoder og variabler holdes private til de eksplisitt blir eksponert:

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 mye å ta i, men det er bare toppen av isfjellet når det gjelder modulmønstre. Her er noen av ressursene jeg fant nyttige i mine egne utforskninger:

  • Learning JavaScript Design Patterns av Addy Osmani: en skattekiste av detaljer i en imponerende kortfattet lesning
  • Adequately Good av Ben Cherry: en nyttig oversikt med eksempler på avansert bruk av modulmønsteret
  • Blogg om Carl Danley: oversikt over modulmønstre og ressurser for andre JavaScript-mønstre.

CommonJS og AMD

Tilnærmingene fremfor alt har en ting til felles: bruken av en enkelt global variabel for å pakke koden i en funksjon, og derved skape et privat navneområde for seg selv ved hjelp av et lukkingsomfang.

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.

Da kan du snu og slå dem sammen igjen, ikke noe problem. Det fungerer bare.

Ser frem: bundling av moduler

Wow! Hvor går tiden? Det var en vill tur, men jeg håper inderlig det ga deg en bedre forståelse av moduler i JavaScript.

I neste avsnitt vil jeg gå gjennom modulpakking og dekke kjernetemaer, inkludert:

  • Hvorfor pakker vi moduler
  • Ulike tilnærminger til bundling
  • ECMAScript's modul loader API
  • …og mer. :)

MERKNAD: For å holde ting enkelt, hoppet jeg over noen av de små detaljene (tenk: sykliske avhengigheter) i dette innlegget. Hvis jeg har utelatt noe viktig og / eller fascinerende, vennligst gi meg beskjed i kommentarene!