Moduler, en framtida strategi för JavaScript-bibliotek

JavaScript-bibliotek som jQuery har gått tillvägagångssättet för att skriva JavaScript i webbläsaren i nästan ett decennium. De har varit en stor framgång och nödvändig intervention för det som en gång var en webbläsare land full av avvik och genomförandeproblem. jQuery glömde sömlöst över webbläsarfel och -känslor och gjorde det till ett bra sätt att göra saker gjort, till exempel händelsehantering, Ajax och DOM-manipulation.

Vid den tiden löste jQuery alla våra problem, vi inkluderar dess allsmäktiga makt och får jobba genast. Det var på sätt sätt en svart låda som webbläsaren "behövde" fungera korrekt.

Men webben har utvecklats, API: erna förbättras, standarder implementeras, webben är en väldigt snabb rörelse och jag är inte säker på att jättebibliotek har en plats i framtiden för webbläsaren. Det blir en modulorienterad miljö.

Ange modulen

En modul är en inkapslad bit av funktionalitet som bara gör en sak, och den där saken mycket bra. En modul kan till exempel vara ansvarig för att lägga till klasser till ett element, kommunicera via HTTP via Ajax osv. Det finns oändliga möjligheter.

En modul kan komma i många former och storlekar, men det allmänna syftet med dem är att importeras till en miljö och träna ur lådan. Generellt skulle varje modul ha grundläggande dokumentations- och installationsprocess för utvecklare, liksom de miljöer den är avsedd för (till exempel webbläsaren, servern).

Dessa moduler blir sedan projektberoende, och beroendet blir lätt att hantera. Dagen att släppa i ett stort bibliotek suddas långsamt bort, stora biblioteken erbjuder inte lika mycket flexibilitet eller kraft. Bibliotek som jQuery har också erkänt detta, vilket är fantastiskt - de har ett verktyg på nätet som låter dig ladda ner bara de saker du behöver.

Moderna API är en enorm uppmuntran till modulinspiration, nu när webbläsarimplementeringar har förbättrats drastiskt, kan vi börja skapa små verktygsmoduler för att hjälpa oss att göra våra vanligaste uppgifter.

Modulets tid är här, och det är här för att stanna.

Inspiration för en första modul

Ett modernt API som jag alltid har varit intresserad av sedan starten var classList API. Inspirerad från bibliotek som jQuery har vi nu ett inbyggt sätt att lägga till klasser till ett element utan bibliotek eller verktygsfunktioner.

ClassList API har funnits om några år nu, men inte många utvecklare vet om det. Detta inspirerade mig att gå och skapa en modul som utnyttjade klasslistor API, och för de webbläsare som är mindre lyckliga att stödja det, ger någon form av fallback-implementering.

Innan vi dyker in i koden, låt oss titta på vad jQuery tog med på scenen för att lägga till en klass till ett element:

$ (Elem) .addClass (MyClass ');

När denna manipulering landade individen slutade vi med det ovan nämnda klasslistor API - ett DOMTokenList Object (rymdseparerade värden) som representerar värdena lagrade mot ett elements klassnamn. ClassList API ger oss några metoder att interagera med den här DOMTokenList, alldeles "jQuery-like". Här är ett exempel på hur klasslistor API lägger till en klass som använder classList.add () metod:

elem.classList.add ( 'MyClass');

Vad kan vi lära av detta? En biblioteksfunktion som går in på ett språk är en ganska stor sak (eller åtminstone inspirerande). Det här är så bra om den öppna webbplattformen, vi kan alla ha insikt om hur saker och ting utvecklas.

Så vad nästa? Vi vet om moduler, och vi tycker om klasslistor API, men tyvärr stöder inte alla webbläsare det än. Vi kunde dock skriva en fallback. Låter som en bra idé för en modul som använder classList när den stöds eller automatiskt fallbacks om inte.

Skapa en första modul: Apollo.js

För ungefär sex månader sedan byggde jag en fristående och väldigt lätt modul för att lägga till klasser till ett element i vanlig JavaScript - jag slutade ringa det apollo.js.

Huvudmålet för modulen var att börja använda det briljanta klasslistor API och bryta sig bort från att behöva ett bibliotek för att göra en mycket enkel och gemensam uppgift. jQuery var inte (och fortfarande inte) använder classList API, så jag trodde att det skulle vara ett bra sätt att experimentera med den nya tekniken.

Vi går igenom hur jag gjorde det också och tanken bakom varje bit som utgör den enkla modulen.

Använda classList

Som vi redan har sett är ClassList ett mycket elegant API och "jQuery utvecklarvänligt", övergången till det är lätt. En sak som jag inte tycker om det är dock att vi måste fortsätta att hänvisa till klasslistobjektet för att använda en av dess metoder. Jag försökte ta bort denna upprepning när jag skrev apollo och bestämde mig för följande API-design:

apollo.addClass (elem, "myclass");

En bra klassmoduleringsmodul bör innehålla hasClass, addClass, removeClass och toggleClass metoder. Alla dessa metoder kommer att rida av namnet "apollo".

När du tittar noga på ovanstående "addClass" -metod kan du se att jag passerar in i elementet som det första argumentet. Till skillnad från jQuery, vilket är ett enormt anpassat objekt som du är bunden till, accepterar den här modulen ett DOM-element, hur det matas det elementet är upp till utvecklaren, inhemska metoder eller en selektormodul. Det andra argumentet är ett enkelt strängvärde, vilket klassnamn du vill ha.

Låt oss gå igenom resten av klassmanipuleringsmetoderna som jag ville skapa för att se hur de ser ut:

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

Så var börjar vi? För det första behöver vi ett objekt för att lägga till våra metoder till och en del funktionslukning för att hysa alla interna arbetssätt / variabler / metoder. Med hjälp av ett omedelbart aktiverat funktionsuttryck (IIFE), paketerar jag ett objekt som heter apollo (och vissa metoder som innehåller classList-abstraktioner) för att skapa vår moduldefinition.

(funktion () var apollo = ; apollo.hasClass = funktion (elem, klassnamn) return elem.classList.contains (className);; apollo.addClass = funktion (elem, klassnamn) elem.classList.add (klassnamn);; apollo.removeClass = funktion (elem, klassnamn) elem.classList.remove (className);; apollo.toggleClass = funktion (elem, klassnamn) elem.classList.toggle (className);; window.apollo = apollo;) (); apollo.addClass (document.body, "test");

Nu har vi klasslistor, vi kan tänka på äldre webbläsarstöd. Syftet med apollomodule är att tillhandahålla en liten och fristående konsekvent API-implementering för klassmanipulation, oberoende av webbläsaren. Det här är det där enkelt funktionsdetektering kommer in i spel.

Det enkla sättet att testa funktionens närvaro för klasslista är detta:

om ('classList' i dokument.documentElement) // du har stöd

Vi använder i operatör som utvärderar förekomsten av classList till Boolean. Nästa steg skulle vara att villkorligt tillhandahålla API till klasslistan endast stödja användare:

(funktion () var apollo = ; var harClass, addClass, removeClass, toggleClass; if ('classList' i document.documentElement) hasClass = function () return elem.classList.contains (className); addClass = funktionen (elem, klassnamn); toggleClass = funktion (elem, klassnamn) elem.classList.toggle class_name (klassnamn); apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo;) ();

Äldre stöd kan göras på några sätt, läsa klassnamnsträngen och loopa igenom alla namn, ersätta dem, lägga till dem och så vidare. jQuery använder mycket kod för detta, genom att använda långa loopar och komplexa strukturer, vill jag inte helt uppblåsa denna färska och lätta modulen, så sätt ut att använda en Regular Expression matchning och ersätter för att uppnå exakt samma effekt med nästa till ingen kod alls. 

Här är det renaste genomförandet jag kunde komma med:

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

Låt oss integrera dem i modulen, lägga till annan del för icke-stödjande webbläsare:

(funktion () var apollo = ; var harClass, addClass, removeClass, toggleClass; om ('classList' i document.documentElement) hasClass = function () returnera elem.classList.contains (className);; addClass = funktion (elem, klassnamn) elem.classList.add (klassnamn);; removeClass = funktion (elem, klassnamn) elem.classList.remove (className);; toggleClass = funktion (elem, klassnamn) elem. classList.toggle (className);; else hasClass = funktion (elem, klassnamn) returnera nya RegExp ('(^ | \\ s)' + klassnamn + '(\\ s | $)') elem.className);; addClass = funktion (elem, klassnamn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funktion (elem, klassnamn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) *, 'g'), ');; toggleClass = funktion (elem, klassnamn) (hasClass (elem, klassnamn)? removeClass: addClass) (elem, klassnamn);; apollo.has Klass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ) ();

En fungerande fråga om vad vi gjort hittills.

Låt oss lämna det där, konceptet har levererats. Apollo-modulen har några fler funktioner, till exempel att lägga till flera klasser samtidigt, det kan du kontrollera om det är intressant.

Så vad har vi gjort? Byggde en inkapslad bit av funktionalitet dedikerad till att göra en sak, och en sak bra. Modulen är mycket enkel att läsa igenom och förstå, och ändringar kan enkelt göras och valideras vid sidan av enhetstester. Vi har också möjlighet att dra in apollo för projekt där vi inte behöver jQuery och dess enorma erbjudande, och den lilla apollo-modulen kommer att räcka.

Dependency Management: AMD och CommonJS

Modulbegreppet är inte nytt, vi använder dem hela tiden. Du är säkert medveten om att JavaScript inte bara handlar om webbläsaren, det körs på servrar och även tv.

Vilka mönster kan vi anta när du skapar och använder dessa nya moduler? Och var kan vi använda dem? Det finns två begrepp som kallas "AMD" och "CommonJS", låt oss utforska dem nedan.

AMD

Asynkronmodul Definition (vanligtvis kallad AMD) är ett JavaScript-API för att definiera moduler som ska laddas asynkront. Dessa körs vanligen i webbläsaren, eftersom synkron belastning medför prestandakostnader samt användbarhet, felsökning och problem med tvärdomän. AMD kan stödja utvecklingen, hålla JavaScript-moduler inkapslade i många olika filer.

AMD använder en funktion som heter definiera, som definierar en modul själv och alla exportobjekt. Med AMD kan vi också hänvisa till eventuella beroenden för att importera andra moduler. Ett snabbt exempel från AMD GitHub-projektet:

definiera (['alfa'], funktion (alfa) return verb: function () returnera alpha.verb () + 2;;);

Vi kan göra något så här för apollo om vi skulle använda ett AMD-tillvägagångssätt:

definiera (['apollo'], funktion (alfa) var apollo = ; var harClass, addClass, removeClass, toggleClass; om ('classList' i document.documentElement) hasClass = function () return elem.classList. innehåller (klassnamn);; addClass = funktion (elem, klassnamn) elem.classList.add (klassnamn);; removeClass = funktion (elem, klassnamn) elem.classList.remove (className);; toggleClass = funktion (elem, klassnamn) elem.classList.toggle (className);; else hasClass = funktion (elem, klassnamn) returnera ny RegExp ('(^ | \\ s)' + klassnamn + ' elem.className;; addClass = funktion (elem, klassnamn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funktion (elem, klassnamn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) *', 'g'), ');; toggleClass = funktion (elem, klassnamn) (hasClass (elem, klassnamn)? removeClass: addClass) SNAME); ;  apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ); 

CommonJS

Node.js har stigit under de senaste åren, liksom beredskapsverktyg och mönster. Node.js använder något som heter CommonJS, som använder ett "export" objekt för att definiera en moduls innehåll. Ett riktigt grundläggande CommonJS-genomförande kan se ut så här (idén om att "exportera" något som ska användas på annat håll):

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

Ovanstående kod skulle sitta i sin egen fil, jag har namngett den här someModule.js. För att importera det på annat håll och kunna använda det, anger CommonJS att vi behöver använda en funktion som kallas "kräva" för att hämta enskilda beroenden:

// gör något med 'myModule' var myModule = kräver ('someModule');

Om du har använt Grunt / Gulp så är du van att se detta mönster.

För att använda detta mönster med apollo skulle vi göra följande och hänvisa till export Objekt istället för fönstret (se sista raden exports.apollo = apollo):

(funktion () var apollo = ; var harClass, addClass, removeClass, toggleClass; om ('classList' i document.documentElement) hasClass = function () returnera elem.classList.contains (className);; addClass = funktion (elem, klassnamn) elem.classList.add (klassnamn);; removeClass = funktion (elem, klassnamn) elem.classList.remove (className);; toggleClass = funktion (elem, klassnamn) elem. classList.toggle (className);; else hasClass = funktion (elem, klassnamn) returnera nya RegExp ('(^ | \\ s)' + klassnamn + '(\\ s | $)') elem.className);; addClass = funktion (elem, klassnamn) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = funktion (elem, klassnamn) if (hasClass (elem, className)) elem.className = elem.className.replace (nytt RegExp ('(^ | \\ s) *' + className + '(\\ s | $) *, 'g'), ');; toggleClass = funktion (elem, klassnamn) (hasClass (elem, klassnamn)? removeClass: addClass) (elem, klassnamn);; apollo.has Klass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; exports.apollo = apollo; ) ();

Universal Module Definition (UMD)

AMD och CommonJS är fantastiska tillvägagångssätt, men vad händer om vi skulle skapa en modul som vi ville arbeta i alla miljöer: AMD, CommonJS och webbläsaren?

Ursprungligen gjorde vi några om och annan trickery att skicka en funktion till varje definitionstyp utifrån vad som var tillgängligt, skulle vi sniffa ut för AMD eller CommonJS-support och använda det om det var där. Denna idé anpassades sedan och en universell lösning började, kallad "UMD". Det paketerar detta om annat trickery för oss och vi passerar bara i en enda funktion som referens till antingen modultyp som stöddes, här är ett exempel från projektets förråd:

(funktion (root, fabrik) if (typeof define === 'function' && define.amd) // AMD. Registrera som en anonym modul. define (['b'] Browser globals root.amdWeb = factory (root.b); (detta, funktionen (b) // använd b på något sätt. // Bara returnera ett värde för att definiera modulen export. // Detta exempel returnerar ett objekt , men modulen // kan returnera en funktion som det exporterade värdet. return ;));

Oj! Massor händer här. Vi passerar i en funktion som det andra argumentet till IIFE-blocket, som under ett lokalt variabelt namn fabrik är dynamiskt tilldelad som AMD eller globalt till webbläsaren. Ja, det här stöder inte CommonJS. Vi kan dock lägga till det här stödet (ta bort kommentarer den här gången också):

funktion (root, fabrik) if (typof definiera === 'funktion' && define.amd) define (['b'], factory); annars om (typ av export === 'objekt') modul .exports = factory; else root.amdWeb = factory (root.b); (detta, funktionen (b) return ;));

Den magiska linjen här är module.exports = factory som tilldelar vår fabrik till CommonJS.

Låt oss paketera apollo i denna UMD-inställning så att den kan användas i CommonJS-miljöer, AMD och webbläsaren! Jag kommer att inkludera hela apollo-skriptet, från den senaste versionen på GitHub, så sakerna kommer att se lite mer komplexa ut än vad jag nämnde ovan (några nya funktioner har lagts till men inte medvetet med i ovanstående exempel):

/ *! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo * / (funktion (root, factory) om (typ definiera === 'funktion' && define.amd) define (factory); annars om = 'object') module.exports = factory; else root.apollo = factory ();) (detta, funktionen () 'använd strikt'; var apollo = ; var hasClass, addClass, removeClass , toggleClass; var forEach = funktion (objekt, fn) if (Object.prototype.toString.call (items)! == '[objekt Array]') items = items.split ("); för = 0; jag < 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 skapat, en förpackad modul för att fungera i många miljöer, vilket ger oss stor flexibilitet när vi skapar nya beroenden i vårt arbete - något som ett JavaScript-bibliotek inte kan ge oss utan att bryta det till små funktionella bitar till att börja med.

Testning

Våra moduler är vanligtvis åtföljda av enhetstester, småbitstesttest som gör det enkelt för andra utvecklare att ansluta sig till ditt projekt och skicka in dragförfrågningar om funktionstillägg, det är också mycket mindre skrämmande än ett stort bibliotek och utarbetar sitt byggsystem ! Små moduler uppdateras ofta snabbt medan större bibliotek kan ta tid att implementera nya funktioner och fixa buggar.

Avslutar

Det var fantastiskt att skapa vår egen modul och veta att vi stöder många utvecklare i många utvecklingsmiljöer. Detta gör utvecklingen mer underhållbar, kul och vi förstår de verktyg vi använder mycket bättre. Moduler åtföljs av dokumentation som vi kan få till snabbhet med ganska snabbt och integrera i vårt arbete. Om en modul inte passar kan vi antingen hitta en annan eller skriva en egen - något vi inte kunde göra lika enkelt med ett stort bibliotek som ett enda beroende, vi vill inte knyta oss till en enda lösning.

Bonus: ES6-moduler

En bra anteckning som slutfördes, var det inte bra att se hur JavaScript-bibliotek hade påverkat inhemska språk med saker som klassmanipulation? Ja, med ES6 (nästa generation av JavaScript-språket) har vi slagit guld! Vi har inhemsk import och export!

Kolla in det, exportera en modul:

/// myModule.js funktion myModule () // modul innehåll exportera myModule;

Och import:

importera myModule från "myModule";

Du kan läsa mer på ES6 och modulens specifikation här.