JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015 – DZone Web Dev JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

Mivel a JavaScript fejlesztés egyre elterjedtebbé válik, a névterek és függőségek kezelése egyre nehezebbé válik. Ennek a problémának a kezelésére különböző megoldások születtek modulrendszerek formájában. Ebben a bejegyzésben a fejlesztők által jelenleg alkalmazott különböző megoldásokat és az általuk megoldani próbált problémákat vizsgáljuk meg. Olvasson tovább!

Bevezetés: Miért van szükség JavaScript modulokra?

Ha ismeri más fejlesztési platformokat, valószínűleg van némi fogalma a kapszulázás és a függőség fogalmáról. A különböző szoftverdarabokat általában elszigetelten fejlesztik, amíg valamilyen követelményt nem kell kielégíteni egy korábban már létező szoftverrel. Abban a pillanatban, amikor ez a másik szoftver bekerül a projektbe, függőség jön létre közte és az új kódrészlet között. Mivel ezeknek a szoftverdaraboknak együtt kell működniük, fontos, hogy ne keletkezzenek konfliktusok közöttük. Ez talán triviálisnak hangzik, de valamiféle kapszulázás nélkül csak idő kérdése, hogy két modul konfliktusba kerüljön egymással. Ez az egyik oka annak, hogy a C könyvtárak elemei általában egy előtagot viselnek:

#ifndef MYLIB_INIT_H#define MYLIB_INIT_Henum mylib_init_code { mylib_init_code_success, mylib_init_code_error};enum mylib_init_code mylib_init(void);// (...)#endif //MYLIB_INIT_H

A kapszulázás elengedhetetlen a konfliktusok megelőzéséhez és a fejlesztés megkönnyítéséhez.

A függőségek a hagyományos kliensoldali JavaScript-fejlesztésben implicitek. Más szóval a fejlesztő feladata, hogy meggyőződjön arról, hogy a függőségek bármelyik kódblokk végrehajtásakor teljesülnek. A fejlesztőknek arról is gondoskodniuk kell, hogy a függőségek a megfelelő sorrendben teljesüljenek (ez bizonyos könyvtárak követelménye).

A következő példa a Backbone.js példáinak része. A szkriptek kézzel töltődnek be a megfelelő sorrendben:

<!DOCTYPE html><html lang="en"> <head> <meta charset="utf-8"> <title>Backbone.js Todos</title> <link rel="stylesheet" href="todos.css"/> </head> <body> <script src="../../test/vendor/json2.js"></script> <script src="../../test/vendor/jquery.js"></script> <script src="../../test/vendor/underscore.js"></script> <script src="../../backbone.js"></script> <script src="../backbone.localStorage.js"></script> <script src="todos.js"></script> </body> <!-- (...) --></html>

Amint a JavaScript fejlesztés egyre összetettebbé válik, a függőségek kezelése nehézkessé válhat. A refaktorálás is megnehezül: hova kell az újabb függőségeket tenni, hogy a betöltési lánc megfelelő sorrendje megmaradjon?

A JavaScript modulrendszerek megpróbálják kezelni ezeket és más problémákat. Kényszerűségből születtek, hogy alkalmazkodjanak az egyre növekvő JavaScript-tájképhez. Lássuk, mit hoznak a különböző megoldások.

Egy ad hoc megoldás: A feltáró modulminta

A legtöbb modulrendszer viszonylag új keletű. Mielőtt ezek megjelentek volna, egy bizonyos programozási mintát kezdtek egyre több JavaScript-kódban használni: a revealing module patternt.

var myRevealingModule = (function () { var privateVar = "Ben Cherry", publicVar = "Hey there!"; function privateFunction() { console.log( "Name:" + privateVar ); } function publicSetName( strName ) { privateVar = strName; } function publicGetName() { privateFunction(); } // Reveal public pointers to // private functions and properties return { setName: publicSetName, greeting: publicVar, getName: publicGetName };})();myRevealingModule.setName( "Paul Kinlan" );

Ez a példa Addy Osmani JavaScript Design Patterns című könyvéből származik.

A JavaScript hatókörök (legalábbis az ES2015-ben megjelent let megjelenéséig) függvényszinten működnek. Más szóval, bármilyen kötés egy függvényen belül van deklarálva, nem tud kikerülni annak hatóköréből. Éppen ezért a feltáró modulminta a függvényekre támaszkodik a privát tartalmak kapszulázására (mint sok más JavaScript-minta).

A fenti példában a nyilvános szimbólumok a visszaadott szótárban vannak kitéve. Az összes többi deklarációt az őket körülvevő függvény hatókör védi. Nem szükséges a var és a privát hatókörbe záró függvény azonnali hívása; a modulokhoz egy megnevezett függvény is használható.

Ezt a mintát már jó ideje használják a JavaScript-projektekben, és elég szépen kezeli a kapszulázás kérdését. A függőségek kérdésével nem sokat tesz. A megfelelő modulrendszerek ezt a problémát is megpróbálják kezelni. Egy másik korlátozás abban rejlik, hogy más modulok bevonása nem lehetséges ugyanabban a forrásban (hacsak nem használjuk a eval-t).

Pros

  • Elég egyszerű ahhoz, hogy bárhol megvalósítható legyen (nincs szükség könyvtárakra, nincs szükség nyelvi támogatásra).
  • Egyetlen fájlban több modul is definiálható.

Hátrányok

  • Nincs mód a modulok programozott importálására (kivéve a eval használatát).
  • A függőségeket manuálisan kell kezelni.
  • A modulok szinkron betöltése nem lehetséges.
  • A körkörös függőségek zavaróak lehetnek.
  • Nehéz elemezni a statikus kódelemzők számára.

CommonJS

A CommonJS egy olyan projekt, amelynek célja egy sor olyan specifikáció meghatározása, amely segíti a szerveroldali JavaScript alkalmazások fejlesztését. Az egyik terület, amellyel a CommonJS csapata megpróbál foglalkozni, a modulok. A Node.js fejlesztői eredetileg a CommonJS specifikációt akarták követni, de később elálltak ettől. Ami a modulokat illeti, a Node.js implementációjára nagy hatással van:

// In circle.jsconst PI = Math.PI;exports.area = (r) => PI * r * r;exports.circumference = (r) => 2 * PI * r;// In some fileconst circle = require('./circle.js');console.log( `The area of a circle of radius 4 is ${circle.area(4)}`);

A Joyent egyik estéjén, amikor megemlítettem, hogy kissé frusztrált vagyok egy nevetséges kérés miatt, ami egy olyan funkcióra vonatkozott, amiről tudtam, hogy szörnyű ötlet, azt mondta nekem: “Felejtsd el a CommonJS-t. Az halott. Mi szerveroldali JavaScript vagyunk.” – Isaac Z. Schlueter, az NPM megalkotója idézi Ryan Dahlt, a Node.js megalkotóját

A Node.js modulrendszerének tetején absztrakciók vannak könyvtárak formájában, amelyek áthidalják a Node.js moduljai és a CommonJS közötti szakadékot. Ebben a bejegyzésben csak az alapvető funkciókat mutatjuk be, amelyek többnyire megegyeznek.

A Node és a CommonJS moduljaiban is alapvetően két elem van a modulrendszerrel való interakcióra: require és exports. A require egy olyan függvény, amellyel szimbólumokat importálhatunk egy másik modulból az aktuális hatókörbe. A require-nek átadott paraméter a modul azonosítója. A Node implementációjában ez a modul neve a node_modules könyvtáron belül (vagy ha nem ebben a könyvtárban van, akkor a hozzá vezető útvonal). A exports egy speciális objektum: bármi, amit beleteszünk, nyilvános elemként exportálódik. A mezők nevei megmaradnak. Sajátos különbség a Node és a CommonJS között a module.exports objektum formájában merül fel. A Node-ban a module.exports a valódi speciális objektum, amely exportálásra kerül, míg a exports csak egy változó, amely alapértelmezés szerint a module.exports-hez lesz kötve. A CommonJS-ben viszont nincs module.exports objektum. Ennek gyakorlati következménye, hogy a Node-ban nem lehet teljesen előre felépített objektumot exportálni anélkül, hogy a module.exports-n keresztülmenne:

// This won't work, replacing exports entirely breaks the binding to// modules.exports.exports = (width) => { return { area: () => width * width };}// This works as expected.module.exports = (width) => { return { area: () => width * width };}

A CommonJS modulokat szerverfejlesztésre tervezték. Természetesen az API szinkronizált. Más szóval a modulok abban a pillanatban és abban a sorrendben töltődnek be, ahogyan egy forrásfájlon belül szükségesek.

Pros

  • Egyszerű: egy fejlesztő a dokumentáció megnézése nélkül is felfoghatja a koncepciót.
  • A függőségkezelés integrált: a modulok más modulokat igényelnek, és a szükséges sorrendben töltődnek be.
  • require Bárhol hívható: a modulok programozottan is betölthetők.
  • A körkörös függőségek támogatottak.

Hátrányok

  • A szinkron API miatt bizonyos felhasználási célokra (kliensoldali) nem alkalmas.
  • Modulonként egy fájl.
  • A böngészők betöltő könyvtárat vagy transzpilálást igényelnek.
  • Nincs konstruktor funkció a modulokhoz (a Node azonban támogatja ezt).
  • Nehéz elemezni a statikus kódelemzők számára.

Implementációk

Egy implementációról már beszéltünk (részleges formában): Node.js.

Node.js JavaScript modulok

A kliens számára jelenleg két népszerű lehetőség van: webpack és browserify. A browserify kifejezetten arra lett kifejlesztve, hogy elemezze a Node-szerű moduldefiníciókat (sok Node-csomag működik vele out-of-the-box!), és a te kódodat, valamint az ezekből a modulokból származó kódot egyetlen fájlba csomagolja, amely magában hordozza az összes függőséget. A Webpack ezzel szemben arra lett kifejlesztve, hogy a publikálás előtt bonyolult forrás-átalakítási csővezetékek létrehozását kezelje. Ez magában foglalja a CommonJS modulok összefűzését is.

Asynchronous Module Definition (AMD)

Az AMD egy olyan fejlesztői csoportból született, akik elégedetlenek voltak a CommonJS által elfogadott irányvonallal. Valójában az AMD már a fejlesztés korai szakaszában levált a CommonJS-ről. Az AMD és a CommonJS közötti fő különbség az aszinkron modulbetöltés támogatásában rejlik.

//Calling define with a dependency array and a factory functiondefine(, function (dep1, dep2) { //Define the module value by returning a value. return function () {};});// Or:define(function (require) { var dep1 = require('dep1'), dep2 = require('dep2'); return function () {};});

Az aszinkron betöltést a JavaScript hagyományos zárási idiómája teszi lehetővé: egy függvényt hív meg, amikor a kért modulok betöltése befejeződik. A moduldefiníciókat és a modulok importálását ugyanaz a függvény végzi: amikor egy modult definiálunk, a függőségek explicité válnak. Ezért egy AMD betöltő futásidőben teljes képet kaphat egy adott projekt függőségi gráfjáról. A betöltés szempontjából egymástól nem függő könyvtárak így egyszerre tölthetők be. Ez különösen fontos a böngészők esetében, ahol a jó felhasználói élményhez elengedhetetlen az indulási idő.

Előnyök

  • Aszinkron betöltés (jobb indulási idők).
  • A körkörös függőségek támogatottak.
  • A require és exports kompatibilitás.
  • A függőségkezelés teljesen integrált.
  • A modulok szükség esetén több fájlra oszthatók.
  • Konstruktorfunkciók támogatottak.
  • Plugin támogatás (egyéni betöltési lépések).

Hátrányok

  • Szintaktikailag kissé bonyolultabb.
  • A betöltő könyvtárakra szükség van, hacsak nem fordítják át.
  • Nehéz elemezni statikus kódelemzők számára.

Implementációk

Az AMD jelenleg legnépszerűbb implementációi require.js és a Dojo.

Require.js for JavaScript Modules

A require.js használata meglehetősen egyszerű: építsük be a könyvtárat a HTML-fájlunkba, és a data-main attribútummal mondjuk meg a require.js-nek, hogy melyik modult töltse be először. A Dojo hasonló beállítással rendelkezik.

ES2015 Modulok

Szerencsére a JavaScript szabványosítása mögött álló ECMA-csoport úgy döntött, hogy foglalkozik a modulok kérdésével. Az eredmény a JavaScript szabvány legújabb kiadásában látható: ECMAScript 2015 (korábbi nevén ECMAScript 6). Az eredmény szintaktikailag tetszetős és kompatibilis mind a szinkron, mind az aszinkron működési móddal.

//------ lib.js ------export const sqrt = Math.sqrt;export function square(x) { return x * x;}export function diag(x, y) { return sqrt(square(x) + square(y));}//------ main.js ------import { square, diag } from 'lib';console.log(square(11)); // 121console.log(diag(4, 3)); // 5

Példa Axel Rauschmayer blogjából

A import direktíva segítségével a modulok a névtérbe hozhatók. Ez a direktíva a require és define direktívákkal ellentétben nem dinamikus (azaz nem hívható meg bárhol). A export direktíva viszont arra használható, hogy az elemeket explicit módon nyilvánossá tegyük.

A import és a export direktíva statikus jellege lehetővé teszi a statikus elemzők számára a függőségek teljes fájának felépítését a kód futtatása nélkül. Az ES2015 támogatja a modulok dinamikus betöltését:

System.import('some_module') .then(some_module => { // Use some_module }) .catch(error => { // ... });

Az ES2015 valójában csak a dinamikus és statikus modulbetöltők szintaxisát határozza meg. A gyakorlatban az ES2015 implementációknak semmit sem kell tenniük ezen direktívák elemzése után. Az olyan modulbetöltőkre, mint a System.js, továbbra is szükség van, amíg a következő ECMAScript spec meg nem jelenik.

Ez a megoldás, mivel a nyelvbe integrálva van, lehetővé teszi a futtatók számára, hogy a modulok betöltésének legjobb stratégiáját válasszák. Más szóval, ha az aszinkron betöltés előnyökkel jár, a futásidő használhatja.

Előnyök

  • Szinkron és aszinkron betöltés támogatott.
  • Szintaktikusan egyszerű.
  • Sztatikus elemző eszközök támogatása.
  • A nyelvbe integrálva (végül mindenhol támogatott, nincs szükség könyvtárakra).
  • Körkörös függőségek támogatása.

Hátrányok

  • Még mindig nem mindenhol támogatott.

Implementációk

Sajnos a főbb JavaScript futtatók egyike sem támogatja az ES2015 modulokat a jelenlegi stabil ágaiban. Ez azt jelenti, hogy nincs támogatás a Firefox, a Chrome vagy a Node.js számára. Szerencsére sok transzpiler támogatja a modulokat, és egy polyfill is elérhető. Jelenleg a Babel ES2015 előbeállítása gond nélkül kezeli a modulokat.

Babel for JavaScript Modules

The All-in-One Solution: System.js

Egyetlen modulrendszert használva próbálhat meg eltávolodni a régebbi kódtól. Vagy biztosra akarsz menni, hogy bármi is történjen, a választott megoldás továbbra is működni fog. Lépj be a System.js-be: egy univerzális modulbetöltő, amely támogatja a CommonJS, az AMD és az ES2015 modulokat. Együtt tud működni az olyan transzpilerekkel, mint a Babel vagy a Traceur, és támogatja a Node és az IE8+ környezeteket. Használata annyi, hogy betölti a System.js-t a kódjában, majd rámutat az alap-URL-re:

 <script src="system.js"></script> <script> // set our baseURL reference path System.config({ baseURL: '/app', // or 'traceur' or 'typescript' transpiler: 'babel', // or traceurOptions or typescriptOptions babelOptions: { } }); // loads /app/main.js System.import('main.js'); </script>

Mivel a System.js minden munkát menet közben végez el, az ES2015 modulok használatát általában egy transzpilerre kell bízni a build lépés során, termelési üzemmódban. Amikor nem termelési üzemmódban van, a System.js meg tudja hívni a transzpilert az Ön helyett, így zökkenőmentes átmenetet biztosít a termelési és a hibakeresési környezetek között.

Mellett: Amit az Auth0-nál használunk

Az Auth0-nál nagymértékben használjuk a JavaScriptet. A szerveroldali kódunkhoz CommonJS-stílusú Node.js modulokat használunk. Bizonyos kliensoldali kódokhoz az AMD-t részesítjük előnyben. A React-alapú Passwordless Lock könyvtárunkhoz ES2015 modulokat választottunk.

Tetszik, amit látsz? Regisztrálj és kezdd el használni az Auth0-t a projektjeidben még ma.

Elfejlesztő vagy és tetszik a kódunk? Ha igen, jelentkezz most egy mérnöki pozícióra. Fantasztikus csapatunk van!

Következtetés

A modulok építése és a függőségek kezelése a múltban nehézkes volt. Az újabb megoldások, könyvtárak vagy ES2015 modulok formájában, a fájdalom nagy részét elvették. Ha új modult vagy projektet szeretne indítani, az ES2015 a megfelelő megoldás. Mindig támogatott lesz, és a jelenlegi támogatás a transzpilerek és polyfillek segítségével kiváló. Másrészt, ha inkább ragaszkodik a sima ES5 kódhoz, akkor továbbra is a szokásos felosztás marad az AMD a kliens és a CommonJS/Node a szerver számára. Ne felejtsd el megírni nekünk a gondolataidat az alábbi megjegyzések között. Hack on!

Szólj hozzá!