Programmering

Kod i JavaScript på den smarte, modulære måde

Nogle mennesker synes stadig overrasket over, at JavaScript betragtes som et respektabelt, voksen programmeringssprog til seriøse applikationer. Faktisk har JavaScript-udvikling modnet pænt i årevis med de bedste fremgangsmåder til modulær udvikling som et godt eksempel.

Fordelene ved at skrive modulær kode er veldokumenteret: større vedligeholdelsesevne, undgå monolitiske filer og afkobling af kode til enheder, der kan testes ordentligt. For dem af jer, der gerne vil blive fanget hurtigt, her er de almindelige fremgangsmåder, der anvendes af moderne JavaScript-udviklere til at skrive modulariseret kode.

Modulmønster

Lad os starte med et grundlæggende designmønster kaldet modulmønsteret. Som du måske har mistanke om, giver dette os mulighed for at skrive kode på en modulær måde, så vi kan beskytte eksekveringskonteksten af ​​givne moduler og kun eksponere globalt det, vi vil eksponere. Mønsteret ligner:

(fungere(){

// 'privat' variabel

var orderId = 123;

// eksponere metoder og variabler ved at vedhæfte dem

// til det globale objekt

window.orderModule = {

getOrderId: funktion () {

// bragt til dig ved lukninger

returnere OrderId;

}

};

})()

En anonymfungere udtryk, der fungerer som en fabrik i dette tilfælde, skrives og kaldes straks. For hastighed kan du eksplicit overføre variabler tilfungere kald, effektivt genbinde disse variabler til et lokalt omfang. Du vil også se dette undertiden som en defensiv manøvre i biblioteker, der understøtter gamle browsere, hvor visse værdier (f.eksudefineret) er skrivbare egenskaber.

(funktion (global, udefineret) {

// kode her kan hurtigt få adgang til det globale objekt,

// og 'udefineret' er helt sikkert 'udefineret'

// BEMÆRK: I moderne browsere kan ikke 'udefineret' skrives,

// men det er værd at huske det

// når du skriver kode til gamle browsere.

})(det her)

Dette er bare et designmønster. Med denne teknik behøver du ikke at medtage ekstra biblioteker for at skrive modulært JavaScript. Manglen på afhængigheder er et stort plus (eller missionskritisk) i nogle indstillinger, især hvis du skriver et bibliotek. Du vil se, at de fleste af de populære biblioteker vil bruge dette mønster til at indkapsle interne funktioner og variabler og kun udsætte det, der er nødvendigt.

Hvis du skriver en ansøgning, er der dog et par ulemper ved denne tilgang. Lad os sige, at du opretter et modul, der sætter et par metoder tilvindue. ordrer. Hvis du vil bruge disse metoder i andre dele af din applikation, skal du sørge for, at modulet er inkluderet, inden du ringer til dem. Derefter i koden, hvor du ringerwindow.orders.getOrderId, skriver du koden og håber, at det andet script er indlæst.

Dette ser måske ikke ud som verdens ende, men det kan hurtigt komme ud af kontrol på komplicerede projekter - og det er en smerte at administrere rækkefølgen af ​​inkludering af script. Også alle dine filer skal indlæses synkront, ellers inviterer du løbetilstand til at bryde din kode. Hvis der kun var en måde at eksplicit erklære de moduler, du ønskede at bruge til en given kode kode ....

AMD (definition af asynkront modul)

AMD blev født ud af behovet for at specificere eksplicitte afhængigheder, mens man undgik synkron indlæsning af alle scripts. Det er let at bruge i en browser, men er ikke indfødt, så du skal medtage et bibliotek, der laver script-loading, som RequireJS eller curl.js. Sådan ser det ud at definere et modul ved hjælp af AMD:

// libs / order-module.js

definer (funktion () {

// 'privat' variabel

var orderId = 123;

// eksponere metoder og variabler ved at returnere dem

Vend tilbage {

getOrderId: funktion () {

returordreId;

}

});

Det ligner det, vi havde med at gøre med før, bortset fra at i stedet for straks at kalde vores fabriksfunktion direkte, passerer vi som et argument tilDefinere. Den virkelige magi begynder at ske, når du senere vil bruge modulet:

definer (['' libs / order-module '], function (orderModule) {

orderModule.getOrderId (); // evalueres til 123

});

Det første argument afDefinere er nu en række afhængigheder, som kan være vilkårligt lange, og fabriksfunktionen viser formelle parametre for de afhængigheder, der skal knyttes til den. Nu kan nogle afhængigheder, du har brug for, have deres egne afhængigheder, men med AMD behøver du ikke vide det:

// src / utils.js

definere (['libs / understregning'], funktion (_) {

Vend tilbage {

moduleId: 'foo',

_ : _

});

// src / myapp.js

Definere( [

'libs / jquery',

'libs / styr',

'src / utils'

], funktion ($, styr, hjælpeprogrammer) {

// Brug hver af de angivne afhængigheder uden

// bekymrende, hvis de er der eller ikke.

$ ('div'). addClass ('bar');

// Underafhængigheder er også taget hånd om

Utils ._. Nøgler (vindue);

});

Dette er en fantastisk måde at udvikle modulær JavaScript på, når man beskæftiger sig med mange bevægelige dele og afhængigheder. Ansvaret for at bestille og inkludere scripts ligger nu på script-loaderens skuldre, hvilket giver dig frihed til blot at angive, hvad du har brug for, og begynde at bruge det.

På den anden side er der et par potentielle problemer. For det første har du et ekstra bibliotek til at inkludere og lære at bruge. Jeg har ingen erfaring med curl.js, men RequireJS indebærer at lære at konfigurere konfiguration til dit projekt. Dette vil tage nogle timer at blive fortrolig med indstillingerne, hvorefter skrivning af den oprindelige konfiguration kun skal tage minutter. Moduldefinitioner kan også blive lange, hvis de fører til en bunke afhængigheder. Her er et eksempel taget fra forklaringen på dette problem i RequireJS-dokumentationen:

// Fra RequireJS-dokumentation:

// //requirejs.org/docs/whyamd.html#sukker

definere (["kræve", "jquery", "klinge / objekt", "klinge / fn", "rdapi",

"oauth", "blade / jig", "blade / url", "dispatch", "accounts",

"storage", "services", "widgets / AccountPanel", "widgets / TabButton",

"widgets / AddAccount", "less", "osTheme", "jquery-ui-1.8.7.min",

"jquery.textOverflow"],

funktion (kræver, $, objekt, fn, rdapi,

oauth, jig, url, forsendelse, konti,

opbevaring, tjenester, AccountPanel, TabButton,

AddAccount, less, osTheme) {

});

Av! RequireJS giver noget syntaktisk sukker til at håndtere dette, hvilket ligner en hel del en anden populær API til modulær udvikling, CommonJS.

CJS (CommonJS)

Hvis du nogensinde har skrevet JavaScript på serversiden ved hjælp af Node.js, har du brugt CommonJS-moduler. Hver fil, du skriver, er ikke pakket ind i noget fancy, men har adgang til en variabel kaldeteksport som du kan tildele alt, hvad du vil blive udsat for af modulet. Sådan ser det ud:

// en 'privat' variabel

var orderId = 123;

exports.getOrderId = funktion () {

returnere OrderId;

};

Så når du vil bruge modulet, erklærer du det inline:

// orderModule får værdien af ​​'eksport'

var orderModule = kræve ('./ ordre-modul');

orderModule.getOrderId (); // evalueres til 123

Syntaktisk har dette altid set bedre ud for mig, hovedsagelig fordi det ikke involverer den spildende fordybelse, der er til stede i de andre muligheder, vi har diskuteret. På den anden side adskiller det sig meget fra de andre, da det er designet til synkron indlæsning af afhængigheder. Dette giver mere mening på serveren, men det gør det ikke i frontenden. Synkron afhængighedsindlæsning betyder længere sideindlæsningstider, hvilket er uacceptabelt for Internettet. Mens CJS langt fra er min yndlingssyntaks for moduler, får jeg kun bruge det på serveren (og mens jeg skriver mobile apps med Appcelerators Titanium Studio).

En til at herske over dem alle

Det nuværende udkast til den sjette udgave af ECMAScript (ES6), den specifikation, hvorfra JavaScript er implementeret, tilføjer native support til moduler. Specifikationen er stadig i kladdeform, men det er værd at kigge på, hvordan fremtiden kan se ud for modulær udvikling. En hel del nye nøgleord bruges i ES6-specifikationen (også kendt som Harmony), hvoraf flere bruges med moduler:

// libs / order-module.js

var orderId = 123;

eksport var getOrderId = funktion () {

returnere OrderId;

};

Du kan ringe til det senere på flere måder:

importer {getOrderId} fra "libs / order-module";

getOrderId ();

Ovenfor vælger du, hvilken eksport inden for et modul, du vil binde til lokale variabler. Alternativt kan du importere hele modulet, som om det var et objekt (svarende tileksport objekt i CJS-moduler):

importer "libs / order-module" som orderModule;

orderModule.getOrderId ();

Der er meget mere til ES6-moduler (tjek Dr. Axel Rauschmayer's 2ality-blog for mere), men eksemplet skal vise dig et par ting. For det første har vi en syntaks svarende til CJS-moduler, hvilket betyder, at ekstra fordybning ikke findes nogen steder, selvom det ikke altid er tilfældet, som du finder i linket 2ality ovenfor. Hvad der ikke er indlysende ved at se på eksemplet, især fordi det ligner så meget CJS, er, at modulerne, der er anført i importopgørelsen, indlæses asynkront.

Slutresultatet er den letlæselige syntaks for CJS blandet med AMD's asynkrone natur. Desværre tager det et stykke tid, før disse understøttes fuldt ud i alle almindeligt målrettede browsere. Når det er sagt, er "et stykke tid" blevet kortere og kortere, når browserudbydere strammer deres frigivelsescyklusser.

I dag er en kombination af disse værktøjer vejen at gå, når det kommer til modulær JavaScript-udvikling. Det hele afhænger af, hvad du laver. Hvis du skriver et bibliotek, skal du bruge modulets designmønster. Hvis du bygger applikationer til browseren, skal du bruge AMD-moduler med en script-loader. Hvis du er på serveren, skal du drage fordel af CJS-moduler. Til sidst vil selvfølgelig ES6 blive understøttet over hele linjen - på hvilket tidspunkt du kan gøre ting på ES6-måde og skrot resten!

Spørgsmål eller tanker? Du er velkommen til at efterlade en besked nedenfor i kommentarfeltet eller kontakte mig på Twitter, @ freethejazz.