Modules, een toekomstige benadering van JavaScript-bibliotheken

JavaScript-bibliotheken zoals jQuery zijn al bijna tien jaar de aangewezen methode om JavaScript in de browser te schrijven. Ze zijn een enorm succes en noodzakelijke interventie geweest voor wat eens een browserland was vol met verschillen en implementatieproblemen. jQuery naadloos verdoezeld over browser bugs en eigenaardigheden en maakte het een niet-brainer-benadering om dingen gedaan te krijgen, zoals gebeurtenisafhandeling, Ajax en DOM-manipulatie.

Destijds loste jQuery al onze problemen op, we nemen zijn almachtige kracht op en gaan meteen aan de slag. Het was in zekere zin een zwarte doos die de browser "nodig" had om goed te functioneren.

Maar het web is geëvolueerd, API's verbeteren, standaarden worden geïmplementeerd, het web is een zeer snel ontroerend tafereel en ik ben er niet zeker van dat gigantische bibliotheken een plaats hebben in de toekomst voor de browser. Het wordt een module-georiënteerde omgeving.

Ga de module in

Een module is een ingekapseld stuk functionaliteit dat maar één ding doet, en dat ene ding heel goed. Een module kan bijvoorbeeld verantwoordelijk zijn voor het toevoegen van klassen aan een element, communiceren via HTTP via Ajax, enzovoort - er zijn eindeloze mogelijkheden.

Een module kan in vele vormen en maten worden geleverd, maar het algemene doel ervan is om in een omgeving te worden geïmporteerd en uit de doos te werken. Over het algemeen zou elke module basis-ontwikkelaarsdocumentatie en installatieproces hebben, evenals de omgevingen waarvoor het is bedoeld (zoals de browser, server).

Deze modules worden projectafhankelijkheden en de afhankelijkheden worden eenvoudig te beheren. De dagen van vallen in een enorme bibliotheek vervagen langzaam, grote bibliotheken bieden niet zoveel flexibiliteit of kracht. Bibliotheken zoals jQuery hebben dit ook herkend, wat fantastisch is - ze hebben een online tool waarmee je alleen de dingen kunt downloaden die je nodig hebt.

Moderne API's zijn een enorme stimulans voor module-inspiratie, nu browser-implementaties drastisch zijn verbeterd, kunnen we beginnen met het maken van kleine hulpprogramma-modules die ons helpen onze meest algemene taken uit te voeren.

Het moduletijdperk is hier, en het is hier om te blijven.

Inspiratie voor een eerste module

Een moderne API waar ik sinds het begin altijd al in geïnteresseerd ben geweest, is de classList-API. Geïnspireerd door bibliotheken zoals jQuery, hebben we nu een native manier om klassen toe te voegen aan een element zonder een bibliotheek of hulpprogramma's.

De classList-API bestaat nu al een paar jaar, maar niet veel ontwikkelaars weten hiervan. Dit inspireerde me om een ​​module te maken die de classList API gebruikte, en voor die minder bevoorrechte browsers om een ​​of andere vorm van fallback-implementatie te bieden.

Voordat we in de code duiken, gaan we kijken naar wat jQuery heeft gedaan om een ​​klasse aan een element toe te voegen:

$ (Elem) .addClass ( 'myclass');

Toen deze manipulatie van nature landde, kwamen we uit bij de bovengenoemde classList API - een DOMTokenList-object (door spaties gescheiden waarden) dat de waarden weergeeft die zijn opgeslagen tegen de className van een element. De classList API biedt ons een paar methoden om te communiceren met deze DOMTokenList, allemaal erg "jQuery-achtig". Hier is een voorbeeld van hoe de classList API een klasse toevoegt, die de classList.add () methode:

elem.classList.add (MijnKlasse);

Wat kunnen we hiervan leren? Een bibliotheekfunctie die zijn weg vindt naar een taal is een vrij grote deal (of op zijn minst inspirerend). Dit is wat zo geweldig is aan het open webplatform, we kunnen allemaal enig inzicht hebben in hoe dingen vorderen.

Dus wat is het volgende? We zijn op de hoogte van modules en we vinden de classList API aardig, maar helaas ondersteunen niet alle browsers dit nog. We zouden echter een fallback kunnen schrijven. Klinkt als een goed idee voor een module die classlist gebruikt wanneer ondersteund of automatische fallbacks als dat niet het geval is.

Een eerste module maken: Apollo.js

Ongeveer zes maanden geleden heb ik een zelfstandige en zeer lichtgewicht module gebouwd om klassen aan een element toe te voegen in gewoon JavaScript - ik belde het uiteindelijk apollo.js.

Het belangrijkste doel van de module was om de briljante classList-API te gebruiken en een bibliotheek niet langer nodig te hebben om een ​​zeer eenvoudige en veelvoorkomende taak uit te voeren. jQuery heeft de classList API niet (en nog steeds niet), dus ik dacht dat dit een geweldige manier zou zijn om met de nieuwe technologie te experimenteren.

We zullen doorlopen hoe ik het heb gemaakt en de denkwijze achter elk stuk waaruit de eenvoudige module bestaat.

ClassList gebruiken

Zoals we al hebben gezien, classList is een erg elegante API en "jQuery ontwikkelaarvriendelijk", de overgang ernaar is eenvoudig. Wat ik er echter niet leuk aan vind, is het feit dat we moeten blijven verwijzen naar het object classList om een ​​van zijn methoden te gebruiken. Ik was van plan deze herhaling te verwijderen toen ik Apollo schreef, waarbij ik besloot het volgende API-ontwerp te kiezen:

apollo.addClass (elem, 'myclass');

Een goede klasse manipulatiemodule moet bevatten hasClass, addClass, removeClass en toggleClass methoden. Al deze methoden zullen de "apollo" -naamruimte verlaten.

Als je goed kijkt naar de bovenstaande "addClass" -methode, kun je zien dat ik het element als eerste argument doorhaal. In tegenstelling tot jQuery, dat een enorm aangepast object is waaraan u bent gebonden, accepteert deze module een DOM-element, de manier waarop dit element wordt ingevoerd, is afhankelijk van de ontwikkelaar, native-methoden of een selectiemodule. Het tweede argument is een eenvoudige tekenreekswaarde, elke klassennaam die je leuk vindt.

Laten we door de rest van de klassenmanipulatiemethoden gaan die ik wilde maken om te zien hoe ze eruit zien:

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

Dus waar beginnen we? Ten eerste hebben we een Object nodig om onze methoden toe te voegen en sommige functies sluiten af ​​om interne werkingen / variabelen / methoden te huisvesten. Met behulp van een onmiddellijk-opgeroepen functie-uitdrukking (IIFE) wikkel ik een object met de naam apollo (en enkele methoden met abstracties van classList) om onze moduledefinitie te maken.

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

Nu we werken met ClassList, kunnen we nadenken over de ondersteuning van oudere browsers. Het doel voor de apollomodule is om een ​​kleine en standalone consistente API-implementatie te bieden voor klassenmanipulatie, ongeacht de browser. Dit is waar eenvoudige kenmerkdetectie in het spel komt.

De eenvoudige manier om aanwezigheid van een functie voor classList te testen, is dit:

if ('classList' in document.documentElement) // je hebt ondersteuning

We gebruiken de in operator die de aanwezigheid van classList tot Boolean evalueert. De volgende stap zou zijn om voorwaardelijk de API aan classList alleen ondersteunende gebruikers te bieden:

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

Legacy-ondersteuning kan op een aantal manieren worden gedaan, door de className-reeks te lezen en alle namen door te lussen, te vervangen, toe te voegen enzovoort. jQuery gebruikt hiervoor veel code, gebruikmakend van lange lussen en een complexe structuur, ik wil deze frisse en lichtgewicht module niet volledig uitbroeden, dus ga op zoek naar een Regular Expression matching en vervang dit om exact hetzelfde effect te bereiken bij volgende helemaal geen code. 

Dit is de schoonste implementatie die ik kon bedenken:

function hasClass (elem, className) retourneer nieuwe RegExp ('(^ | \\ s)' + className + '(\\ s | $)'). test (elem.className);  function addClass (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;  functie removeClass (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (new RegExp ('(^ | \\ s) *' + className + '(\\ s | $) * ',' g '), "); functie toggleClass (elem, className) (hasClass (elem, className)? removeClass: addClass) (elem, className); 

Laten we ze integreren in de module, het toevoegen van de anders onderdeel voor niet-ondersteunende browsers:

(functie () var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () return elem.classList.contains (className);; addClass = functie (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) retourneer nieuwe 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.has Klasse = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ) ();

Een goed werkgevecht van wat we tot nu toe hebben gedaan.

Laten we het daar laten, het concept is afgeleverd. De apollo-module heeft nog een aantal functies, zoals het tegelijkertijd toevoegen van meerdere klassen. Je kunt dit hier, als je geïnteresseerd bent, controleren.

Wat hebben we gedaan? Gebouwd een ingekapseld stuk functionaliteit gewijd aan het doen van één ding, en één ding goed. De module is heel eenvoudig te lezen en te begrijpen, en wijzigingen kunnen eenvoudig worden gemaakt en gevalideerd naast unit tests. We hebben ook de mogelijkheid om apollo in te zamelen voor projecten waar we jQuery en zijn enorme aanbod niet nodig hebben, en de kleine apollo-module zal volstaan.

Dependency Management: AMD en CommonJS

Het concept van modules is niet nieuw, we gebruiken ze altijd. U weet waarschijnlijk dat JavaScript niet alleen meer over de browser gaat, maar ook op servers en zelfs tv's.

Welke patronen kunnen we aannemen bij het maken en gebruiken van deze nieuwe modules? En waar kunnen we ze gebruiken? Er zijn twee concepten met de naam "AMD" en "CommonJS", laten we deze hieronder bekijken.

AMD

Definitie van asynchrone modules (meestal AMD genoemd) is een JavaScript-API voor het definiëren van modules die asynchroon worden geladen. Deze worden meestal in de browser uitgevoerd omdat synchroon laden leidt tot prestatiekosten, evenals problemen met bruikbaarheid, foutopsporing en toegang tot meerdere domeineisen. AMD kan de ontwikkeling ondersteunen, doordat JavaScript-modules in veel verschillende bestanden worden ingekapseld.

AMD gebruikt een functie genaamd bepalen, die een module zelf en eventuele exportobjecten definieert. Met behulp van AMD kunnen we ook verwijzen naar afhankelijkheden om andere modules te importeren. Een snel voorbeeld uit het AMD GitHub-project:

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

We kunnen zoiets voor Apollo doen als we een AMD-aanpak zouden gebruiken:

define (['apollo'], functie (alpha) var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () retour elem.classList. bevat (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) retourneer nieuwe 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, clas sName); ;  apollo.hasClass = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; window.apollo = apollo; ); 

CommonJS

Node.js is de laatste paar jaar gestegen, evenals tools en patronen voor afhankelijkheidsbeheer. Node.js maakt gebruik van iets dat CommonJS wordt genoemd, en gebruikt een "exports" -object om de inhoud van een module te definiëren. Een echt eenvoudige CommonJS-implementatie ziet er misschien zo uit (het idee om iets te exporteren dat ergens anders kan worden gebruikt):

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

De bovenstaande code zou in zijn eigen bestand zitten, ik heb deze genoemd someModule.js. Om het elders te importeren en te kunnen gebruiken, geeft CommonJS aan dat we een functie moeten gebruiken die "require" wordt genoemd om individuele afhankelijkheden op te halen:

// doe iets met 'myModule' var myModule = require ('someModule');

Als je Grunt / Gulp ook hebt gebruikt, ben je dit patroon gewend.

Om dit patroon met apollo te gebruiken, zouden we het volgende doen en verwijzen naar de export Object in plaats van het venster (zie laatste regel exports.apollo = apollo):

(functie () var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () return elem.classList.contains (className);; addClass = functie (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) retourneer nieuwe 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.has Klasse = hasClass; apollo.addClass = addClass; apollo.removeClass = removeClass; apollo.toggleClass = toggleClass; exports.apollo = apollo; ) ();

Universal Module Definition (UMD)

AMD en CommonJS zijn fantastische benaderingen, maar wat als we een module zouden maken die we in alle omgevingen zouden willen gebruiken: AMD, CommonJS en de browser?

Aanvankelijk deden we wat als en anders bedrog om een ​​functie door te geven aan elk definitietype op basis van wat beschikbaar was, we snuffelden voor AMD of CommonJS-ondersteuning en gebruik het als het er was. Dit idee werd vervolgens aangepast en een universele oplossing begon, genaamd "UMD". Het verpakt dit if / else bedrog voor ons en we geven slechts één functie door als referentie naar elk van de moduletypes die werd ondersteund. Dit is een voorbeeld uit de repository van het project:

(functie (root, fabriek) if (typeof define === 'function' && define.amd) // AMD. Registreer als een anonieme module. define (['b'], factory); else // Browser-globalen root.amdWeb = factory (root.b); (this, function (b) // gebruik b op de een of andere manier. // Retourneer een waarde om de module-export te definiëren. // Dit voorbeeld retourneert een object , maar de module // kan een functie retourneren als de geëxporteerde waarde. return ;));

Whoa! Er gebeurt hier veel. We geven een functie door als het tweede argument voor het IIFE-blok, dat onder een lokale variabelenaam staat fabriek wordt dynamisch toegewezen als AMD of globaal aan de browser. Ja, dit ondersteunt CommonJS niet. We kunnen echter die ondersteuning toevoegen (ook deze keer reacties verwijderen):

(functie (root, fabriek) if (typeof define === 'function' && define.amd) define (['b'], factory); else if (typeof exports === 'object') module .exports = factory; else root.amdWeb = factory (root.b); (this, function (b) return ;));

De magische lijn is hier module.exports = fabriek die onze fabriek aan CommonJS toewijst.

Laten we Apollo in deze UMD-installatie plaatsen, zodat deze kan worden gebruikt in CommonJS-omgevingen, AMD en de browser! Ik zal het volledige apollo-script opnemen, van de nieuwste versie op GitHub, dus dingen zien er iets ingewikkelder uit dan wat ik hierboven behandelde (sommige nieuwe functies zijn toegevoegd, maar waren niet met opzet opgenomen in de bovenstaande voorbeelden):

/ *! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo * / (functie (root, fabriek) if (typeof define === 'function' && define.amd) define (factory); else if (typeof exports == = 'object') module.exports = factory; else root.apollo = factory ();) (this, function () 'use strict'; var apollo = ; var hasClass, addClass, removeClass , toggleClass; var forEach = function (items, fn) if (Object.prototype.toString.call (items)! == '[object Array]') items = items.split ("); for (var i = 0; i < 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; ); 

We hebben onze module zo samengesteld dat deze in veel omgevingen werkt, dit geeft ons enorme flexibiliteit bij het aanbrengen van nieuwe afhankelijkheden in ons werk - iets dat een JavaScript-bibliotheek ons ​​niet kan bieden zonder het in kleine functionele stukjes te breken om te beginnen.

testen

Doorgaans worden onze modules vergezeld van unit tests, kleine bit-size tests die het gemakkelijk maken voor andere ontwikkelaars om deel te nemen aan uw project en pull-aanvragen in te dienen voor functie-uitbreidingen, het is ook een stuk minder ontmoedigend dan een enorme bibliotheek en het uitwerken van hun build-systeem ! Kleine modules worden vaak snel bijgewerkt, terwijl grotere bibliotheken de tijd kunnen nemen om nieuwe functies te implementeren en bugs op te lossen.

Afsluiten

Het was geweldig om onze eigen module te maken en we weten dat we veel ontwikkelaars ondersteunen in veel ontwikkelomgevingen. Dit maakt het ontwikkelen van onderhoudsvriendelijker en leuker en we begrijpen de tools die we veel beter gebruiken. Modules gaan vergezeld van documentatie die we vrij snel kunnen bijhouden en die we kunnen integreren in ons werk. Als een module niet past, kunnen we een andere vinden of onze eigen schrijven - iets wat we niet zo gemakkelijk kunnen doen met een grote bibliotheek als een enkele afhankelijkheid, we willen ons niet in een enkele oplossing binden.

Bonus: ES6-modules

Een leuke opmerking om te voltooien, was het niet geweldig om te zien hoe JavaScript-bibliotheken native-talen hadden beïnvloed met dingen als klassenmanipulatie? A Nou, met ES6 (de volgende generatie van de JavaScript-taal) hebben we goud gewonnen! We hebben inheemse invoer en uitvoer!

Bekijk het en exporteer een module:

/// myModule.js-functie myModule () // module-inhoud myModule exporteren;

En importeren:

importeer myModule vanuit 'myModule';

U kunt hier meer lezen over ES6 en de modulespecificatie.