Code in JavaScript auf intelligente, modulare Weise

Einige Leute scheinen immer noch überrascht zu sein, dass JavaScript als seriöse, erwachsene Programmiersprache für ernsthafte Anwendungen angesehen wird. Tatsächlich ist die JavaScript-Entwicklung seit Jahren gut ausgereift, wobei die Best Practices für die modulare Entwicklung ein Paradebeispiel sind.

Die Vorteile des Schreibens von modularem Code sind gut dokumentiert: bessere Wartbarkeit, Vermeidung monolithischer Dateien und Entkopplung von Code in Einheiten, die ordnungsgemäß getestet werden können. Für diejenigen unter Ihnen, die schnell eingeholt werden möchten, sind hier die gängigen Methoden, die moderne JavaScript-Entwickler anwenden, um modularisierten Code zu schreiben.

Modulmuster

Beginnen wir mit einem grundlegenden Entwurfsmuster, das als Modulmuster bezeichnet wird. Wie Sie vielleicht vermuten, können wir so Code modular schreiben, um den Ausführungskontext bestimmter Module zu schützen und global nur das verfügbar zu machen, was wir verfügbar machen möchten. Das Muster sieht ungefähr so ​​aus:

(function(){

//'private' variable

var orderId = 123;

// expose methods and variables by attaching them

// to the global object

window.orderModule = {

getOrderId: function(){

//brought to you by closures

return orderId;

}

};

})()

Ein anonymer  function Ausdruck, der in diesem Fall als Fabrik fungiert, wird sofort geschrieben und aufgerufen. Aus function Gründen der Geschwindigkeit können Sie Variablen explizit an den  Aufruf übergeben und diese Variablen effektiv an einen lokalen Bereich binden. Sie werden dies manchmal auch als Verteidigungsmanöver in Bibliotheken sehen, die alte Browser unterstützen, in denen bestimmte Werte (z. B. undefined) beschreibbare Eigenschaften sind.

(function(global, undefined){

// code here can access the global object quickly,

// and 'undefined' is sure to be 'undefined'

// NOTE: In modern browsers 'undefined' isn't writeable,

// but it's worth keeping it in mind

// when writing code for old browsers.

})(this)

Dies ist nur ein Entwurfsmuster. Mit dieser Technik müssen Sie zum Schreiben von modularem JavaScript keine zusätzlichen Bibliotheken einschließen. Das Fehlen von Abhängigkeiten ist in einigen Einstellungen ein großes Plus (oder geschäftskritisch), insbesondere wenn Sie eine Bibliothek schreiben. Sie werden sehen, dass die meisten gängigen Bibliotheken dieses Muster verwenden, um interne Funktionen und Variablen zu kapseln und nur das zu offenbaren, was erforderlich ist.

Wenn Sie jedoch eine Anwendung schreiben, hat dieser Ansatz einige Nachteile. Angenommen, Sie erstellen ein Modul, für das einige Methoden festgelegt sind  window.orders. Wenn Sie diese Methoden in anderen Teilen Ihrer Anwendung verwenden möchten, müssen Sie sicherstellen, dass das Modul enthalten ist, bevor Sie sie aufrufen. Dann window.orders.getOrderIdschreiben Sie in den Code, in dem Sie anrufen , den Code und hoffen, dass das andere Skript geladen wurde.

Dies scheint nicht das Ende der Welt zu sein, kann jedoch bei komplizierten Projekten schnell außer Kontrolle geraten - und die Verwaltung der Reihenfolge der Skripteinschlüsse wird zu einem Problem. Außerdem müssen alle Ihre Dateien synchron geladen werden, sonst laden Sie die Rennbedingungen ein, um Ihren Code zu brechen. Wenn es nur eine Möglichkeit gäbe, die Module, die Sie für ein bestimmtes Codebit verwenden möchten, explizit zu deklarieren ...

AMD (asynchrone Moduldefinition)

AMD entstand aus der Notwendigkeit heraus, explizite Abhängigkeiten anzugeben und gleichzeitig das synchrone Laden aller Skripte zu vermeiden. Es ist einfach in einem Browser zu verwenden, aber nicht nativ. Daher müssen Sie eine Bibliothek einfügen, die Skripte lädt, z. B. RequireJS oder curl.js. So sieht es aus, wenn Sie ein Modul mit AMD definieren:

// libs/order-module.js

define(function(){

//'private' variable

var orderId = 123;

// expose methods and variables by returning them

return {

getOrderId: function(){

return orderId;

}

});

Es sieht ähnlich aus wie zuvor, nur dass wir nicht sofort unsere Factory-Funktion direkt aufrufen, sondern als Argument an übergeben  define. Die wahre Magie beginnt, wenn Sie das Modul später verwenden möchten:

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

orderModule.getOrderId(); //evaluates to 123

});

Das erste Argument von  define ist jetzt ein Array von Abhängigkeiten, die beliebig lang sein können, und die Factory-Funktion listet formale Parameter für diese Abhängigkeiten auf, die daran angehängt werden sollen. Einige Abhängigkeiten, die Sie benötigen, haben möglicherweise eigene Abhängigkeiten, aber bei AMD müssen Sie Folgendes nicht wissen:

// src/utils.js

define( [ 'libs/underscore' ], function(_) {

return {

moduleId: 'foo',

_ : _

});

// src/myapp.js

define( [

'libs/jquery',

'libs/handlebars',

'src/utils'

], function($, Handlebars, Utils){

// Use each of the stated dependencies without

// worrying if they're there or not.

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

// Sub dependencies have also been taken care of

Utils._.keys(window);

});

Dies ist eine großartige Möglichkeit, modulares JavaScript zu entwickeln, wenn Sie sich mit vielen beweglichen Teilen und Abhängigkeiten befassen. Die Verantwortung für das Bestellen und Einfügen von Skripten liegt nun beim Script-Loader. Sie können also einfach angeben, was Sie benötigen, und es verwenden.

Auf der anderen Seite gibt es einige potenzielle Probleme. Zunächst müssen Sie eine zusätzliche Bibliothek einbinden und deren Verwendung erlernen. Ich habe keine Erfahrung mit curl.js, aber RequireJS beinhaltet das Erlernen der Konfiguration für Ihr Projekt. Es dauert einige Stunden, bis Sie sich mit den Einstellungen vertraut gemacht haben. Danach sollte das Schreiben der Erstkonfiguration nur noch wenige Minuten dauern. Moduldefinitionen können auch langwierig werden, wenn sie zu einem Stapel von Abhängigkeiten führen. Hier ist ein Beispiel aus der Erläuterung dieses Problems in der RequireJS-Dokumentation:

// From RequireJS documentation:

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

define([ "require", "jquery", "blade/object", "blade/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"],

function (require, $, object, fn, rdapi,

oauth, jig, url, dispatch, accounts,

storage, services, AccountPanel, TabButton,

AddAccount, less, osTheme) {

});

Autsch! RequireJS bietet etwas syntaktischen Zucker, um damit umzugehen. Dies ähnelt einer anderen beliebten API für die modulare Entwicklung, CommonJS.

CJS (CommonJS)

Wenn Sie jemals serverseitiges JavaScript mit Node.js geschrieben haben, haben Sie CommonJS-Module verwendet. Jede Datei, die Sie schreiben, ist nicht in etwas Besonderes verpackt, sondern hat Zugriff auf eine Variable  exports , der Sie alles zuweisen können, was Sie vom Modul verfügbar machen möchten. So sieht es aus:

// a 'private' variable

var orderId = 123;

exports.getOrderId = function() {

return orderId;

};

Wenn Sie das Modul dann verwenden möchten, deklarieren Sie es inline:

// orderModule gets the value of 'exports'

var orderModule = require('./order-module');

orderModule.getOrderId(); // evaluates to 123

Syntaktisch hat dies für mich immer besser ausgesehen, vor allem, weil es nicht um die verschwenderische Einrückung geht, die in den anderen Optionen enthalten ist, die wir besprochen haben. Andererseits unterscheidet es sich stark von den anderen darin, dass es für das synchrone Laden von Abhängigkeiten ausgelegt ist. Dies ist auf dem Server sinnvoller, auf dem Front-End jedoch nicht. Das synchrone Laden von Abhängigkeiten bedeutet längere Ladezeiten für Seiten, was für das Web nicht akzeptabel ist. Während CJS bei weitem meine am liebsten aussehende Modulsyntax ist, kann ich sie nur auf dem Server verwenden (und beim Schreiben mobiler Apps mit Appcelerators Titanium Studio).

Einer, der sie alle regiert

Der aktuelle Entwurf der sechsten Ausgabe von ECMAScript (ES6), der Spezifikation, aus der JavaScript implementiert wird, bietet native Unterstützung für Module. Die Spezifikation befindet sich noch in Entwurfsform, aber es lohnt sich, einen Blick darauf zu werfen, wie die Zukunft für eine modulare Entwicklung aussehen könnte. In der ES6-Spezifikation (auch als Harmony bekannt) werden einige neue Schlüsselwörter verwendet, von denen einige mit Modulen verwendet werden:

// libs/order-module.js

var orderId = 123;

export var getOrderId = function(){

return orderId;

};

Sie können es später auf verschiedene Arten aufrufen:

import { getOrderId } from "libs/order-module";

getOrderId();

Oben wählen Sie aus, welche Exporte innerhalb eines Moduls Sie an lokale Variablen binden möchten. Alternativ können Sie das gesamte Modul so importieren, als wäre es ein Objekt (ähnlich dem  exports Objekt in CJS-Modulen):

import "libs/order-module" as orderModule;

orderModule.getOrderId();

ES6-Module bieten viel mehr (weitere Informationen finden Sie im 2ality-Blog von Dr. Axel Rauschmayer), aber das Beispiel sollte Ihnen einige Dinge zeigen. Erstens haben wir eine Syntax ähnlich wie bei CJS-Modulen, was bedeutet, dass keine zusätzliche Einrückung zu finden ist, obwohl dies nicht immer der Fall ist, wie Sie im obigen Link 2ality finden. Was im Beispiel nicht offensichtlich ist, insbesondere weil es CJS sehr ähnlich sieht, ist, dass die in der import-Anweisung aufgeführten Module asynchron geladen werden.

Das Endergebnis ist die einfach zu lesende Syntax von CJS, gemischt mit der asynchronen Natur von AMD. Leider wird es eine Weile dauern, bis diese in allen gängigen Browsern vollständig unterstützt werden. Das heißt, "eine Weile" wurde immer kürzer, da Browser-Anbieter ihre Veröffentlichungszyklen verkürzen.

Eine Kombination dieser Tools ist heute der richtige Weg, wenn es um die modulare JavaScript-Entwicklung geht. Es hängt alles davon ab, was Sie tun. Wenn Sie eine Bibliothek schreiben, verwenden Sie das Modulentwurfsmuster. Wenn Sie Anwendungen für den Browser erstellen, verwenden Sie AMD-Module mit einem Script-Loader. Wenn Sie sich auf dem Server befinden, nutzen Sie die CJS-Module. Schließlich wird ES6 natürlich auf ganzer Linie unterstützt - an diesem Punkt können Sie die Dinge auf ES6-Weise erledigen und den Rest verschrotten!

Fragen oder Gedanken? Fühlen Sie sich frei, eine Nachricht unten in den Kommentaren zu hinterlassen oder mich auf Twitter, @ freethejazz, zu erreichen.