Moduler, en fremtidig tilnærming til JavaScript-biblioteker

JavaScript-biblioteker som jQuery har vært gå til tilnærming for å skrive JavaScript i nettleseren i nesten et tiår. De har vært en stor suksess og nødvendig intervensjon for det som en gang var en nettleser land fylt av uoverensstemmelser og implementeringsproblemer. jQuery sømløst glanset over nettleserveil og quirks og gjorde det til en ikke-brainer tilnærming til å få ting gjort, for eksempel hendelseshåndtering, Ajax og DOM manipulasjon.

På den tiden løste jQuery alle våre problemer, vi inkluderer sin allmektige kraft og kommer til å jobbe med en gang. Det var på en måte en svart boks som nettleseren "trengte" for å fungere skikkelig.

Men nettet har utviklet seg, APIer er bedre, standarder blir implementert, nettet er en veldig rask bevegelses scene og jeg er ikke sikker på at gigantiske biblioteker har et sted i fremtiden for nettleseren. Det blir et modulorientert miljø.

Skriv inn modulen

En modul er et innkapslet stykke funksjonalitet som bare gjør en ting, og det er en ting veldig bra. For eksempel kan en modul være ansvarlig for å legge til klasser i et element, kommunisere over HTTP via Ajax og så videre - det finnes uendelige muligheter.

En modul kan komme i mange former og størrelser, men det generelle formålet med dem er å bli importert til et miljø og trene ut av esken. Vanligvis vil hver modul ha noen grunnleggende utviklingsdokumentasjon og installasjonsprosess, samt miljøene den er ment for (for eksempel nettleseren, serveren).

Disse modulene blir deretter prosjektavhengigheter, og avhengighetene blir enkle å administrere. Dagene med å slippe i et stort bibliotek svinger langsomt bort, store biblioteker tilbyr ikke så mye fleksibilitet eller kraft. Biblioteker som jQuery har anerkjent dette også, noe som er fantastisk - de har et verktøy på nettet som lar deg laste ned bare de tingene du trenger.

Moderne APIer er en stor boost for modulinspirasjon, nå som nettleserimplementasjonene har blitt drastisk forbedret, kan vi begynne å lage små verktøymoduler for å hjelpe oss med å gjøre våre vanligste oppgaver.

Modulets tid er her, og det er her for å bli.

Inspirasjon til en første modul

En moderne API som jeg alltid har vært interessert i siden starten, er classList API. Inspirert fra biblioteker som jQuery, har vi nå fått en innfødt måte å legge til klasser på et element uten bibliotek eller bruksfunksjoner.

ClassList API har eksistert noen få år nå, men ikke mange utviklere vet om det. Dette inspirerte meg til å gå og lage en modul som benyttet classList API, og for de nettleserne som er mindre heldige til å støtte den, gir noen form for tilbakebetaling av implementering.

Før vi dykker inn i koden, la oss se på hva jQuery brakte til scenen for å legge til en klasse til et element:

$ (ELEM) .addClass ( 'MyClass');

Når denne manipulasjonen landet inn, endte vi med den nevnte classList API - et DOMTokenList Object (romavskilt verdier) som representerer verdiene lagret mot et elements klassenavn. ClassList API gir oss noen få metoder til å samhandle med denne DOMTokenList, alt veldig "jQuery-like". Her er et eksempel på hvordan klasselisten API legger til en klasse, som bruker classList.add () metode:

elem.classList.add ( 'MyClass');

Hva kan vi lære av dette? Et biblioteksfunksjon som gjør veien til et språk er en ganske stor avtale (eller i det minste inspirerende). Dette er det som er så bra med den åpne nettplattformen, vi kan alle ha litt innsikt i hvordan ting går framover.

Så hva neste? Vi vet om moduler, og vi liker som classList API, men dessverre, ikke alle nettlesere støtter det enda. Vi kunne skrive et fallback, skjønt. Høres ut som en god ide for en modul som bruker classList når det støttes eller automatisk fallbacks hvis ikke.

Opprette en første modul: Apollo.js

For rundt seks måneder siden bygget jeg en frittstående og veldig lett modul for å legge klasser til et element i vanlig JavaScript - jeg endte opp med å kalle det apollo.js.

Hovedmålet for modulen var å begynne å bruke den strålende classList API og bryte seg bort fra å trenge et bibliotek for å gjøre en veldig enkel og felles oppgave. jQuery var ikke (og fortsatt ikke) bruker classList API, så jeg trodde det ville være en fin måte å eksperimentere med den nye teknologien.

Vi vil gå gjennom hvordan jeg gjorde det også og tenkningen bak hvert stykke som utgjør den enkle modulen.

Bruke classList

Som vi allerede har sett, er ClassList en veldig elegant API og "jQuery utvikler-vennlig", overgangen til den er lett. En ting jeg ikke liker om det, er det faktum at vi må fortsette å henvise til classList Object å bruke en av metodene sine. Jeg har til hensikt å fjerne denne repetisjonen da jeg skrev apollo, og bestemte seg for følgende API-design:

Apollo.addClass (elem, 'myclass');

En god klasse manipulasjonsmodul bør inneholde hasClass, addClass, removeClass og toggleClass metoder. Alle disse metodene vil rive av navnet "Apollo".

Ser tett på ovenstående "addClass" -metode, du kan se at jeg passerer elementet som det første argumentet. I motsetning til jQuery, som er et stort tilpasset objekt som du er bundet til, vil denne modulen akseptere et DOM-element, hvordan det er matet det elementet er opp til utvikleren, innfødte metoder eller en valgmodul. Det andre argumentet er en enkel strengverdi, noe klassenavn du liker.

La oss gå gjennom resten av klassemetoduleringsmetodene som jeg ønsket å lage for å se hvordan de ser ut:

apollo.hasClass (elem, 'myclass'); Apollo.addClass (elem, 'myclass'); Apollo.removeClass (elem, 'myclass'); apollo.toggleClass (elem, 'myclass');

Så hvor begynner vi? Først trenger vi et objekt for å legge til våre metoder til, og noen funksjonslukning for å huske noen interne arbeid / variabler / metoder. Ved å bruke et øyeblikkelig aktivert uttrykk (IIFE), pakker jeg et objekt som heter apollo (og noen metoder som inneholder classList abstraksjoner) for å lage vår moduldefinisjon.

(funksjon () var apollo = ; apollo.hasClass = funksjon (elem, klassenavn) retur elem.classList.contains (klassenavn);; apollo.addClass = funksjon (elem, klassenavn) elem.classList.add (klassenavn);; apollo.removeClass = funksjon (elem, klassenavn) elem.classList.remove (klassenavn);; apollo.toggleClass = funksjon (elem, klassenavn) elem.classList.toggle (className);; window.apollo = apollo;) (); apollo.addClass (document.body, 'test');

Nå har vi klasseliste, vi kan tenke på eldre nettleserstøtte. Målet for apollomodule er å gi en liten og uavhengig konsistent API-implementering for klassepåvirkning, uavhengig av nettleseren. Dette er hvor enkel funksjon gjenkjenning kommer inn i spill.

Den enkle måten å teste funksjonen tilstedeværelse for classList er dette:

hvis ('classList' i document.documentElement) // du har støtte

Vi bruker i operatør som vurderer tilstedeværelsen av classList til boolsk. Det neste skrittet ville være å betinget gi API til klasseliste som bare støtter brukere:

(funksjon () var apollo = ; var harClass, addClass, removeClass, toggleClass; if ('classList' i document.documentElement) hasClass = function () return elem.classList.contains (className); addClass = funksjon (elem, klassenavn) elem.classList.add (klassenavn); removeClass = funksjon (elem, klassenavn) elem.classList.remove (klassenavn); toggleClass = funksjon (elem, klassenavn) elem.classList.toggle (klassenavn); apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo;) ();

Eldre støtte kan gjøres på noen måter, leser klassenavnet String og looping gjennom alle navnene, erstatt dem, legg til dem og så videre. jQuery bruker mye kode for dette, ved hjelp av lange løkker og kompleks struktur, vil jeg ikke helt oppblåse denne friske og lette modulen, så sett ut for å bruke en Regular Expression-matching og erstatter for å oppnå nøyaktig samme effekt med neste til ingen kode i det hele tatt. 

Her er den reneste implementeringen jeg kunne komme med:

funksjon hasClass (elem, className) returner ny RegExp ('(^ | \\ s)' + className + '(\\ s | $)'). test (elem.className);  funksjon addClass (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;  funksjon removeClass (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + ' s | $) * ',' g '),'); funksjon toggleClass (elem, klassenavn) (hasClass (elem, klassenavn)? removeClass: addClass) 

La oss integrere dem i modulen, og legg til ellers del for ikke-støttende nettlesere:

(funksjon () var apollo = ; var harClass, addClass, removeClass, toggleClass; hvis ('classList' i document.documentElement) hasClass = function () return elem.classList.contains (className);; addClass = funksjon (elem, klassenavn) elem.classList.add (klassenavn);; removeClass = funksjon (elem, klassenavn) elem.classList.remove (className);; toggleClass = funksjon (elem, klassenavn) elem. classList.toggle (className);; else hasClass = funksjon (elem, klassenavn) returner ny RegExp ('(^ | \\ s)' + klassenavn + '(\\ s | $)') elem.className);; addClass = funksjon (elem, klassenavn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funksjon (elem, klassenavn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) * ',' g '),');; toggleClass = funksjon (elem, klassenavn) (hasClass (elem, klassenavn)? removeClass: addClass) (elem, className);; apollo.has Klasse = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ) ();

En jobber jsFiddle av hva vi har gjort hittil.

La oss legge det der, konseptet er levert. Apollo-modulen har noen flere funksjoner, for eksempel å legge til flere klasser samtidig, du kan sjekke det her, hvis det er interessert.

Så hva har vi gjort? Bygget et innkapslet stykke funksjonalitet dedikert til å gjøre en ting, og en ting bra. Modulen er veldig enkel å lese gjennom og forstå, og endringer kan enkelt gjøres og valideres sammen med enhetstester. Vi har også muligheten til å trekke inn apollo for prosjekter der vi ikke trenger jQuery og det store tilbudet, og den lille apollo-modulen vil være tilstrekkelig.

Dependency Management: AMD og CommonJS

Konseptet med moduler er ikke nytt, vi bruker dem hele tiden. Du er sikkert klar over at JavaScript ikke bare handler om nettleseren, det kjører på servere og til og med TV.

Hvilke mønstre kan vi vedta når du lager og bruker disse nye modulene? Og hvor kan vi bruke dem? Det er to begreper kalt "AMD" og "CommonJS", la oss utforske dem nedenfor.

AMD

Asynkronmoduldefinisjon (vanligvis referert til som AMD) er en JavaScript-API for å definere moduler som skal asynkront lastes. Disse kjøres typisk i nettleseren, da synkron lasting medfører ytelseskostnader, samt bruksproblemer, feilsøking og tilgang til kryssdomener. AMD kan hjelpe utviklingen, og holde JavaScript-moduler innkapslet i mange forskjellige filer.

AMD bruker en funksjon som kalles definere, som definerer en modul selv og eventuelle eksportobjekter. Ved hjelp av AMD, kan vi også referere til eventuelle avhengigheter for å importere andre moduler. Et raskt eksempel tatt fra AMD GitHub prosjektet:

define (['alpha'], funksjon (alfa) return verb: function () return alpha.verb () + 2;;);

Vi kan gjøre noe slikt for apollo hvis vi skulle bruke en AMD-tilnærming:

define (['apollo'], funksjon (alfa) var apollo = ; var hasClass, addClass, removeClass, toggleClass; hvis ('classList' i document.documentElement) hasClass = function () return elem.classList. inneholder (klassenavn);; addClass = funksjon (elem, klassenavn) elem.classList.add (klassenavn);; removeClass = funksjon (elem, klassenavn) elem.classList.remove (klassenavn);; toggleClass = funksjon (elem, klassenavn) elem.classList.toggle (className);; else hasClass = funksjon (elem, klassenavn) returner ny RegExp ('(^ | \\ s)' + klassenavn + ' | $) ') .prøve (elem.className);; addClass = funksjon (elem, klassenavn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funksjon (elem, klassenavn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) *', 'g'), ");; toggleClass = funksjon (elem, klassenavn) (hasClass (elem, klassenavn)? removeClass: addClass) sName); ;  apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ); 

CommonJS

Node.js har steget de siste årene, samt avhengighetsstyringsverktøy og mønstre. Node.js benytter noe som heter CommonJS, som bruker et "eksport" objekt for å definere innholdet i en modul. En virkelig grunnleggende CommonJS-implementering kan se slik ut (ideen om å eksportere noe som skal brukes andre steder):

// someModule.js exports.someModule = funksjon () return "foo"; ;

Ovennevnte kode ville sitte i sin egen fil, jeg har kalt denne someModule.js. For å importere det andre steder og kunne bruke det, spesifiserer CommonJS at vi må bruke en funksjon kalt "kreve" for å hente individuelle avhengigheter:

// gjør noe med 'myModule' var myModule = krever ('someModule');

Hvis du har brukt Grunt / Gulp også, er du vant til å se dette mønsteret.

For å bruke dette mønsteret med apollo, ville vi gjøre følgende og referere til eksport Objekt i stedet for vinduet (se siste linje exports.apollo = apollo):

(funksjon () var apollo = ; var harClass, addClass, removeClass, toggleClass; hvis ('classList' i document.documentElement) hasClass = function () return elem.classList.contains (className);; addClass = funksjon (elem, klassenavn) elem.classList.add (klassenavn);; removeClass = funksjon (elem, klassenavn) elem.classList.remove (className);; toggleClass = funksjon (elem, klassenavn) elem. classList.toggle (className);; else hasClass = funksjon (elem, klassenavn) returner ny RegExp ('(^ | \\ s)' + klassenavn + '(\\ s | $)') elem.className);; addClass = funksjon (elem, klassenavn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funksjon (elem, klassenavn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) * ',' g '),');; toggleClass = funksjon (elem, klassenavn) (hasClass (elem, klassenavn)? removeClass: addClass) (elem, className);; apollo.has Klasse = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; exports.apollo = apollo; ) ();

Universal Module Definition (UMD)

AMD og CommonJS er fantastiske tilnærminger, men hva om vi skulle lage en modul som vi ønsket å jobbe på tvers av alle miljøer: AMD, CommonJS og nettleseren?

I utgangspunktet gjorde vi noen hvis og ellers trickery å overføre en funksjon til hver definisjonstype basert på hva som var tilgjengelig, ville vi snuse ut for AMD eller CommonJS-støtte og bruke den hvis den var der. Denne ideen ble deretter tilpasset og en universell løsning startet, kalt "UMD". Det pakker dette hvis / annet trickery for oss og vi bare passere i en enkelt funksjon som referanse til enten modul type som ble støttet, her er et eksempel fra prosjektets lager:

(funksjon (root, fabrikk) if (typeof define === 'function' && define.amd) // AMD. Registrer som en anonym modul. define (['b'], fabrikk); else // Browser globals root.amdWeb = fabrikk (root.b); (dette, funksjon (b) // bruk b på noen måte. // Bare returner en verdi for å definere modulen eksport. // Dette eksempelet returnerer en gjenstand , men modulen // kan returnere en funksjon som den eksporterte verdien. return ;));

Jøss! Mye skjer her. Vi passerer i en funksjon som det andre argumentet til IIFE-blokken, som under et lokalt variabelt navn fabrikk er dynamisk tilordnet som AMD eller globalt til nettleseren. Ja, dette støtter ikke CommonJS. Vi kan imidlertid legge til den støtten (fjerner kommentarer også denne gangen også):

(funksjon (root, fabrikk) if (typeof define === 'function' && define.amd) define (['b'], fabrikk); else if (type av eksport === 'objekt') .exports = factory; else root.amdWeb = fabrikk (root.b); (dette, funksjon (b) return ;));

Den magiske linjen her er module.exports = fabrikk som tildeler fabrikken til CommonJS.

La oss pakke apollo i dette UMD-oppsettet slik at det kan brukes i CommonJS-miljøer, AMD og nettleseren! Jeg vil inkludere det fullstendige apollo-skriptet, fra den nyeste versjonen på GitHub, slik at tingene vil se litt mer komplisert ut enn det jeg dekket over (noen nye funksjoner er lagt til, men ikke med hensikt inkludert i eksemplene ovenfor):

/ *! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo * / (funksjon (root, fabrikk) hvis (type definere === 'funksjon' && define.amd) define (factory); annet hvis = 'object') module.exports = factory; else root.apollo = fabrikk ();) (denne funksjonen () 'bruk streng'; var apollo = ; var hasClass, addClass, removeClass , toggleClass; var forEach = funksjon (elementer, fn) hvis (Object.prototype.toString.call (items)! == '[objekt Array]') items = items.split ("); for = 0; jeg < items.length; i++)  fn(items[i], i);  ; if ('classList' in document.documentElement)  hasClass = function (elem, className)  return elem.classList.contains(className); ; addClass = function (elem, className)  elem.classList.add(className); ; removeClass = function (elem, className)  elem.classList.remove(className); ; toggleClass = function (elem, className)  elem.classList.toggle(className); ;  else  hasClass = function (elem, className)  return new RegExp('(^|\\s)' + className + '(\\s|$)').test(elem.className); ; addClass = function (elem, className)  if (!hasClass(elem, className))  elem.className += (elem.className ?":") + className;  ; removeClass = function (elem, className)  if (hasClass(elem, className))  elem.className = elem.className.replace(new RegExp('(^|\\s)*' + className + '(\\s|$)*', 'g'),");  ; toggleClass = function (elem, className)  (hasClass(elem, className) ? removeClass : addClass)(elem, className); ;  apollo.hasClass = function (elem, className)  return hasClass(elem, className); ; apollo.addClass = function (elem, classes)  forEach(classes, function (className)  addClass(elem, className); ); ; apollo.removeClass = function (elem, classes)  forEach(classes, function (className)  removeClass(elem, className); ); ; apollo.toggleClass = function (elem, classes)  forEach(classes, function (className)  toggleClass(elem, className); ); ; return apollo; ); 

Vi har laget, pakket vår modul for å jobbe på tvers av mange miljøer, dette gir oss stor fleksibilitet når det kommer nye avhengigheter i arbeidet vårt - noe et JavaScript-bibliotek kan ikke gi oss uten å bryte det inn i små funksjonelle stykker til å begynne med.

testing

Vanligvis er våre moduler ledsaget av enhetstester, små bitstørrelsestester som gjør det enkelt for andre utviklere å bli med på prosjektet ditt og sende inn forespørsler om forbedringer av funksjonen, det er også mye mindre skremmende enn et stort bibliotek og utarbeider sitt byggesystem ! Små moduler oppdateres ofte raskt, mens større biblioteker kan ta seg tid til å implementere nye funksjoner og fikse feil.

Wrapping Up

Det var flott å lage vår egen modul og vet at vi støtter mange utviklere i mange utviklingsmiljøer. Dette gjør utviklingen mer vedlikeholdsbar, morsom og vi forstår verktøyene vi bruker mye bedre. Modulene er ledsaget av dokumentasjon som vi kan komme raskt i gang med og integrere i arbeidet vårt. Hvis en modul ikke passer, kan vi enten finne en annen eller skrive oss selv - noe vi ikke kunne gjøre like enkelt med et stort bibliotek som en enkelt avhengighet, vi ønsker ikke å knytte oss til en enkelt løsning.

Bonus: ES6-moduler

Et fint notat å fullføre på, var det ikke så bra å se hvordan JavaScript-bibliotekene hadde påvirket morsmål med ting som klassemanipulering? Vel, med ES6 (neste generasjon av JavaScript-språket) har vi slått gull! Vi har innfødt import og eksport!

Sjekk ut det, eksportere en modul:

/// myModule.js funksjonen myModule () // modul innhold eksport myModule;

Og import:

importer myModule fra 'myModule';

Du kan lese mer på ES6 og modulspesifikasjonen her.