Module, ein zukünftiger Ansatz für JavaScript-Bibliotheken

JavaScript-Bibliotheken wie jQuery sind seit fast einem Jahrzehnt die erste Wahl, um JavaScript im Browser zu schreiben. Sie waren ein großer Erfolg und notwendige Eingriffe für ein Land, in dem einst ein Browser voller Unstimmigkeiten und Implementierungsprobleme war. jQuery hat Browser-Bugs und Macken nahtlos überarbeitet und es zu einem unkomplizierten Ansatz gemacht, um Dinge wie Event-Handling, Ajax- und DOM-Manipulation zu erledigen.

Zu diesem Zeitpunkt hat jQuery alle unsere Probleme gelöst, wir bringen seine mächtige Kraft mit ein und machen uns sofort an die Arbeit. In gewisser Weise war es eine Blackbox, die der Browser „brauchte“, um ordnungsgemäß zu funktionieren.

Aber das Web hat sich weiterentwickelt, APIs verbessern sich, Standards werden implementiert, das Web ist eine sehr schnelllebige Szene und ich bin nicht sicher, ob riesige Bibliotheken in Zukunft einen Platz für den Browser haben. Es wird zu einer modulorientierten Umgebung.

Betritt das Modul

Ein Modul ist eine gekapselte Funktion, die nur eine Sache und die eine Sache sehr gut macht. Beispielsweise kann ein Modul dafür verantwortlich sein, einem Element Klassen hinzuzufügen, über Ajax usw. über HTTP zu kommunizieren - es gibt unendlich viele Möglichkeiten.

Ein Modul kann in vielen Formen und Größen vorliegen, der allgemeine Zweck besteht jedoch darin, in eine Umgebung importiert zu werden und sofort einsatzbereit zu sein. Im Allgemeinen verfügt jedes Modul über einige grundlegende Entwicklerdokumentationen und Installationsprozesse sowie die Umgebungen, für die es bestimmt ist (z. B. Browser, Server)..

Diese Module werden dann zu Projektabhängigkeiten, und die Abhängigkeiten lassen sich leicht verwalten. Die Tage des Abfalls in einer riesigen Bibliothek verblassen langsam, große Bibliotheken bieten nicht so viel Flexibilität oder Leistung. Bibliotheken wie jQuery haben dies ebenfalls erkannt, was fantastisch ist. Sie haben online ein Tool, mit dem Sie nur die Dinge herunterladen können, die Sie benötigen.

Moderne APIs sind ein enormer Impuls für die Inspiration von Modulen. Nachdem Browserimplementierungen drastisch verbessert wurden, können wir mit der Entwicklung kleiner Versorgungsmodule beginnen, die uns bei den häufigsten Aufgaben helfen.

Die Ära des Moduls ist da und es bleibt hier.

Inspiration für ein erstes Modul

Eine moderne API, an der ich schon immer interessiert war, ist die classList-API. Inspiriert von Bibliotheken wie jQuery, haben wir jetzt eine native Möglichkeit, einem Element Klassen ohne Bibliotheks- oder Hilfsfunktionen hinzuzufügen.

Die classList-API gibt es bereits seit einigen Jahren, aber nur wenige Entwickler wissen davon. Dies hat mich dazu inspiriert, ein Modul zu entwickeln, das die classList-API verwendet, und für diese Browser, deren Unterstützung weniger gut ist, eine Form der Fallback-Implementierung bieten.

Bevor wir in den Code eintauchen, schauen wir uns an, was jQuery für das Hinzufügen einer Klasse zu einem Element in die Szene gebracht hat:

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

Als diese Manipulation nativ gelandet ist, haben wir die oben genannte classList-API erhalten - ein DOMTokenList-Objekt (durch Leerzeichen getrennte Werte), das die Werte darstellt, die im className eines Elements gespeichert sind. Die classList-API bietet uns einige Methoden, um mit dieser DOMTokenList zu interagieren, alles sehr „jQuery-like“. Hier ein Beispiel, wie die classList-API eine Klasse hinzufügt, die die classList.add () Methode:

elem.classList.add ('myclass');

Was können wir daraus lernen? Eine Bibliotheksfunktion, die ihren Weg in eine Sprache findet, ist eine ziemlich große Sache (oder zumindest inspirierend). Das ist es, was an der offenen Web-Plattform so großartig ist, dass wir alle Einblick in den Fortschritt haben können.

Was kommt als nächstes? Wir wissen über Module Bescheid und mögen die classList-API, aber leider unterstützen noch nicht alle Browser diese Funktion. Wir könnten jedoch einen Fallback schreiben. Klingt nach einer guten Idee für ein Modul, das classList verwendet, wenn es unterstützt wird, oder automatische Fallbacks, wenn dies nicht der Fall ist.

Ein erstes Modul erstellen: Apollo.js

Vor ungefähr sechs Monaten habe ich ein eigenständiges und sehr leichtes Modul zum Hinzufügen von Klassen zu einem Element in einfachem JavaScript erstellt apollo.js.

Das Hauptziel des Moduls bestand darin, die brillante classList-API zu verwenden und nicht mehr eine Bibliothek für eine sehr einfache und allgemeine Aufgabe zu benötigen. jQuery hat (und hat immer noch nicht) die classList-API verwendet. Ich dachte, es wäre eine großartige Möglichkeit, mit der neuen Technologie zu experimentieren.

Wir gehen durch, wie ich es auch geschafft habe und die Hintergedanken jedes Stücks, aus denen das einfache Modul besteht.

ClassList verwenden

Wie wir bereits gesehen haben, ist classList eine sehr elegante API und "jQuery-Entwickler-freundlich". Der Übergang zu diesem Programm ist einfach. Was mir aber nicht gefällt, ist die Tatsache, dass wir uns immer wieder auf das classList-Objekt beziehen müssen, um eine seiner Methoden zu verwenden. Ich wollte diese Wiederholung entfernen, als ich Apollo schrieb und mich für das folgende API-Design entschied:

apollo.addClass (elem, 'myclass');

Ein gutes Klassenbearbeitungsmodul sollte enthalten hasClass, addClass, removeClass und toggleClass Methoden. Alle diese Methoden werden den Namensraum "Apollo" verlassen.

Wenn Sie sich die obige Methode "addClass" genau ansehen, können Sie sehen, dass ich das Element als erstes Argument übergeben habe. Im Gegensatz zu jQuery, einem riesigen benutzerdefinierten Objekt, an das Sie gebunden sind, akzeptiert dieses Modul ein DOM-Element. Wie es zugeführt wird, hängt von diesem Element dem Entwickler, seinen nativen Methoden oder einem Auswahlmodul ab. Das zweite Argument ist ein einfacher String-Wert, jeder beliebige Klassenname.

Gehen wir die restlichen Methoden zur Klassenbearbeitung durch, die ich erstellen wollte, um zu sehen, wie sie aussehen:

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

Also wo fangen wir an? Erstens brauchen wir ein Objekt, zu dem wir unsere Methoden hinzufügen können, und einige Funktionsschließungen, um interne Arbeitsweisen / Variablen / Methoden unterzubringen. Mit einem sofort aufgerufenen Funktionsausdruck (IIFE) wickle ich ein Object namens apollo (und einige Methoden, die classList-Abstraktionen enthalten) ein, um unsere Moduldefinition zu erstellen.

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

Jetzt haben wir die Klassenliste, wir können über die Unterstützung älterer Browser nachdenken. Das Ziel für die Apollomodul ist eine winzige und eigenständige konsistente API-Implementierung für die Klassenmanipulation, unabhängig vom Browser. Hier kommt die einfache Funktionserkennung ins Spiel.

Die einfache Möglichkeit, das Vorhandensein von Funktionen für classList zu testen, lautet folgendermaßen:

if ('classList' in document.documentElement) // Sie haben Unterstützung

Wir benutzen die im Operator, der das Vorhandensein von classList in Boolean auswertet. Der nächste Schritt wäre die bedingte Bereitstellung der API für classList, die nur Benutzer unterstützt:

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

Die Unterstützung für ältere Versionen kann auf verschiedene Arten durchgeführt werden: Lesen Sie den className-String und ziehen Sie alle Namen durch, ersetzen Sie sie, fügen Sie sie hinzu und so weiter. jQuery verwendet dafür sehr viel Code, verwendet lange Loops und komplexe Strukturen. Ich möchte dieses frische und leichte Modul nicht vollständig aufblähen. Verwenden Sie also einen regulären Ausdruck und ersetzen Sie ihn, um den gleichen Effekt mit next zu erzielen zu keinem Code. 

Hier ist die sauberste Implementierung, die ich finden könnte:

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

Lassen Sie uns sie in das Modul integrieren, indem Sie das hinzufügen sonst Teil für nicht unterstützende Browser:

(function () var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () 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) gibt neues RegExp ('(^ | \\ s)' + className + '(\\ s | $)') zurück. test ( elem.className);; addClass = Funktion (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = Funktion (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (neues 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; ) ();

Ein funktionierendes JsFiddle dessen, was wir bisher gemacht haben.

Lassen wir es dort liegen, das Konzept wurde geliefert. Das Apollo-Modul verfügt über einige weitere Funktionen, z. B. das Hinzufügen mehrerer Klassen auf einmal. Sie können dies hier überprüfen, wenn Sie interessiert sind.

Was haben wir getan? Bau einer gekapselten Funktionalität, die nur eine Sache erledigt, und eine Sache gut. Das Modul ist sehr einfach zu lesen und zu verstehen, und Änderungen können neben Unit-Tests leicht vorgenommen und validiert werden. Wir haben auch die Möglichkeit, Apollo für Projekte einzuziehen, bei denen wir jQuery und sein riesiges Angebot nicht benötigen, und das winzige Apollo-Modul wird ausreichen.

Abhängigkeitsmanagement: AMD und CommonJS

Das Konzept der Module ist nicht neu, wir verwenden sie ständig. Sie wissen wahrscheinlich, dass es bei JavaScript nicht mehr nur um den Browser geht, sondern auf Servern und sogar Fernsehgeräten.

Welche Muster können wir übernehmen, wenn Sie diese neuen Module erstellen und verwenden? Und wo können wir sie einsetzen? Es gibt zwei Konzepte, die als "AMD" und "CommonJS" bezeichnet werden.

AMD

Asynchronous Module Definition (in der Regel als AMD bezeichnet) ist eine JavaScript-API zum Definieren von asynchron geladenen Modulen. Diese werden normalerweise im Browser ausgeführt, da beim synchronen Laden Leistungskosten sowie Probleme bei der Benutzerfreundlichkeit, beim Debugging und beim domänenübergreifenden Zugriff auftreten. AMD kann die Entwicklung unterstützen, indem JavaScript-Module in vielen verschiedenen Dateien eingebettet bleiben.

AMD verwendet eine aufgerufene Funktion definieren, das definiert ein Modul selbst und alle Exportobjekte. Mit AMD können wir auch auf Abhängigkeiten verweisen, um andere Module zu importieren. Ein kurzes Beispiel aus dem AMD GitHub-Projekt:

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

Wir könnten so etwas für Apollo tun, wenn wir einen AMD-Ansatz verwenden würden:

define (['apollo'], Funktion (alpha) var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () gibt elem.classList zurück. Enthält (className);; addClass = Funktion (elem, className) elem.classList.add (className);; removeClass = function (elem, className) elem.classList.remove (className);; toggleClass = Funktion (elem, className) elem.classList.toggle (className);; else hasClass = function (elem, className) gibt neues RegExp ('(^ | \\ s)' + className + '(\\ s | $) '). test (elem.className);; addClass = Funktion (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = function (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (neues RegExp ('(^ | \\ s))' + Klassenname + '(\\ s | $) *', 'g'), ");; toggleClass = Funktion (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 hat in den letzten Jahren zugenommen, sowie Tools und Muster für das Abhängigkeitsmanagement. Node.js verwendet etwas namens CommonJS, das ein Objekt "exportiert" verwendet, um den Inhalt eines Moduls zu definieren. Eine wirklich grundlegende CommonJS-Implementierung könnte folgendermaßen aussehen (die Idee, etwas zu "exportieren", um es anderswo zu verwenden):

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

Der obige Code würde in einer eigenen Datei stehen, ich habe diesen Namen genannt someModule.js. Um es an anderer Stelle zu importieren und verwenden zu können, gibt CommonJS an, dass wir eine Funktion namens "required" verwenden müssen, um einzelne Abhängigkeiten abzurufen:

// etwas mit 'myModule' tun var myModule = requir ('someModule');

Wenn Sie auch Grunt / Gulp verwendet haben, sind Sie es gewohnt, dieses Muster zu sehen.

Um dieses Muster mit Apollo zu verwenden, würden wir Folgendes tun und auf das verweisen Exporte Objekt anstelle des Fensters (siehe letzte Zeile) exports.apollo = apollo):

(function () var apollo = ; var hasClass, addClass, removeClass, toggleClass; if ('classList' in document.documentElement) hasClass = function () 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) gibt neues RegExp ('(^ | \\ s)' + className + '(\\ s | $)') zurück. test ( elem.className);; addClass = Funktion (elem, className) if (! hasClass (elem, className)) elem.className + = (elem.className? ":") + className;; removeClass = Funktion (elem, className) if (hasClass (elem, className)) elem.className = elem.className.replace (neues 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 und CommonJS sind fantastische Ansätze, aber was wäre, wenn wir ein Modul erstellen würden, das wir in allen Umgebungen bearbeiten wollten: AMD, CommonJS und den Browser?

Anfangs haben wir einige gemacht ob und sonst Wenn es darum geht, eine Funktion an jeden Definitionstyp zu übergeben, würden wir nach AMD- oder CommonJS-Unterstützung suchen und sie verwenden, wenn sie vorhanden wäre. Diese Idee wurde dann angepasst und eine universelle Lösung mit dem Namen "UMD" begann. Es verpackt dies ansonsten trickery für uns und wir übergeben nur eine einzige Funktion als Referenz für jeden unterstützten Modultyp, hier ein Beispiel aus dem Projekt-Repository:

(Funktion (root, factory) if (typeof define === 'function' && define.amd) // AMD. Als anonymer Baustein registrieren. define (['b'], factory); else // Browser globals root.amdWeb = factory (root.b); (this, function (b) // benutze b auf irgendeine Weise. // Gibt einfach einen Wert zurück, um den Modulaxport zu definieren. // Dieses Beispiel gibt ein Objekt zurück , aber das Modul // kann eine Funktion als exportierten Wert zurückgeben. return ;));

Whoa! Hier ist viel los. Wir übergeben eine Funktion als zweites Argument an den IIFE-Block, der unter einem lokalen Variablennamen steht Fabrik wird dynamisch als AMD oder global dem Browser zugewiesen. Ja, das unterstützt CommonJS nicht. Wir können diese Unterstützung jedoch hinzufügen (diesmal auch Kommentare entfernen):

(Funktion (Wurzel, Werk) if (typeof define === 'function' && define.amd) define (['b'], factory); else if (typeof export === 'object') modul .exports = factory; else root.amdWeb = factory (root.b); (this, Funktion (b) return ;));

Die magische Linie hier ist module.exports = Werk das ordnet unsere Fabrik CommonJS zu.

Lassen Sie uns Apollo in dieses UMD-Setup einpacken, damit es in CommonJS-Umgebungen, AMD und dem Browser verwendet werden kann! Ich füge das vollständige Apollo-Skript der neuesten Version von GitHub bei, sodass die Dinge etwas komplexer aussehen werden als das, was ich oben beschrieben habe (einige neue Funktionen wurden hinzugefügt, waren aber in den obigen Beispielen nicht absichtlich enthalten):

/ *! apollo.js v1.7.0 | (c) 2014 @toddmotto | https://github.com/toddmotto/apollo * / (Funktion (Root, Factory) if (typeof define === 'function' && define.amd) define (factory); else if (export typ == = 'object') module.exports = factory; else root.apollo = factory (); () 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 ("); für (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; ); 

Wir haben ein Modul erstellt, das in vielen Umgebungen eingesetzt werden kann. Dies gibt uns eine große Flexibilität, wenn es darum geht, neue Abhängigkeiten in unsere Arbeit zu integrieren - etwas, das uns eine JavaScript-Bibliothek nicht bieten kann, ohne es zunächst in kleine Funktionselemente zu zerlegen.

Testen

In der Regel werden unsere Module von Komponententests begleitet. Kleine Bissgrößen-Tests, die es anderen Entwicklern erleichtern, Ihrem Projekt beizutreten und Pull-Requests für Funktionserweiterungen einzureichen. Dies ist weit weniger entmutigend als eine große Bibliothek und das Erarbeiten des Build-Systems ! Kleine Module werden häufig schnell aktualisiert, während größere Bibliotheken Zeit benötigen, um neue Funktionen zu implementieren und Fehler zu beheben.

Einpacken

Es war großartig, ein eigenes Modul zu erstellen und zu wissen, dass wir viele Entwickler in vielen Entwicklungsumgebungen unterstützen. Dies macht die Entwicklung einfacher, macht Spaß und wir verstehen die Werkzeuge, die wir verwenden, viel besser. Die Module werden von einer Dokumentation begleitet, mit der wir uns relativ schnell einarbeiten und in unsere Arbeit integrieren können. Wenn ein Modul nicht passt, können wir entweder ein anderes finden oder ein eigenes schreiben - etwas, das wir mit einer großen Bibliothek nicht so leicht tun könnten wie eine einzelne Abhängigkeit, möchten wir uns nicht auf eine einzige Lösung festlegen.

Bonus: ES6-Module

Eine schöne Anmerkung zum Schluss, war es nicht toll zu sehen, wie JavaScript-Bibliotheken die Muttersprachen durch Klassenmanipulation beeinflusst haben? A Nun, mit ES6 (der nächsten Generation der JavaScript-Sprache) haben wir Gold geschlagen! Wir haben einheimische Importe und Exporte!

Check it out, ein Modul exportieren:

/// myModule.js function myModule () // Inhalt des Moduls export myModule;

Und importieren:

myModule aus 'myModule' importieren;

Weitere Informationen zu ES6 und den Modulspezifikationen finden Sie hier.