JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

Pe măsură ce dezvoltarea JavaScript devine din ce în ce mai comună, spațiile de nume și dependențele devin mult mai dificil de gestionat. Diferite soluții au fost dezvoltate pentru a face față acestei probleme sub forma unor sisteme de module. În această postare, vom explora diferitele soluții utilizate în prezent de dezvoltatori și problemele pe care acestea încearcă să le rezolve. Citiți mai departe!

Introducere: De ce sunt necesare modulele JavaScript?

Dacă sunteți familiarizați cu alte platforme de dezvoltare, probabil că aveți o oarecare noțiune despre conceptele de încapsulare și dependență. Diferite bucăți de software sunt de obicei dezvoltate în mod izolat până când o anumită cerință trebuie să fie satisfăcută de o bucată de software existentă anterior. În momentul în care acea altă bucată de software este introdusă în proiect, se creează o dependență între aceasta și noua bucată de cod. Deoarece aceste părți de software trebuie să lucreze împreună, este important să nu apară conflicte între ele. Acest lucru poate părea banal, dar fără un fel de încapsulare, este o chestiune de timp până când două module intră în conflict între ele. Acesta este unul dintre motivele pentru care elementele din bibliotecile C poartă de obicei un prefix:

#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

Încapsularea este esențială pentru a preveni conflictele și pentru a ușura dezvoltarea.

Când vine vorba de dependențe, în dezvoltarea tradițională a JavaScript-ului pe partea de client, acestea sunt implicite. Cu alte cuvinte, este treaba dezvoltatorului să se asigure că dependențele sunt satisfăcute în momentul în care orice bloc de cod este executat. Dezvoltatorii trebuie, de asemenea, să se asigure că dependențele sunt satisfăcute în ordinea corectă (o cerință a anumitor biblioteci).

Exemplul următor face parte din exemplele Backbone.js. Scripturile sunt încărcate manual în ordinea corectă:

<!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>

Pe măsură ce dezvoltarea JavaScript devine din ce în ce mai complexă, gestionarea dependențelor poate deveni greoaie. Refacerea este, de asemenea, afectată: unde ar trebui să fie puse dependențele mai noi pentru a menține ordinea corectă a lanțului de încărcare?

Sistemele de module JavaScript încearcă să rezolve aceste probleme și altele. Ele s-au născut din necesitate pentru a se adapta peisajului JavaScript în continuă creștere. Să vedem ce aduc la masă diferitele soluții.

O soluție ad-hoc: The Revealing Module Pattern

Majoritatea sistemelor de module sunt relativ recente. Înainte ca acestea să fie disponibile, un anumit model de programare a început să fie folosit în tot mai mult cod JavaScript: modelul modulului revelator.

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" );

Acest exemplu a fost preluat din cartea lui Addy Osmani, JavaScript Design Patterns.

Scopurile JavaScript (cel puțin până la apariția lui let în ES2015) funcționează la nivel de funcție. Cu alte cuvinte, orice legătură declarată în interiorul unei funcții nu poate scăpa din domeniul de aplicare al acesteia. Este, din acest motiv, motivul pentru care modelul de modul revelator se bazează pe funcții pentru a încapsula conținutul privat (ca multe alte modele JavaScript).

În exemplul de mai sus, simbolurile publice sunt expuse în dicționarul returnat. Toate celelalte declarații sunt protejate de sfera funcției care le înglobează. Nu este necesar să se utilizeze var și un apel imediat la funcția care înglobează domeniul privat; o funcție cu nume poate fi utilizată și pentru module.

Acest model este utilizat de ceva timp în proiectele JavaScript și tratează destul de bine problema încapsulării. Nu face prea multe în ceea ce privește problema dependențelor. Sistemele de module adecvate încearcă să se ocupe și de această problemă. O altă limitare constă în faptul că includerea altor module nu se poate face în aceeași sursă (cu excepția cazului în care se folosește eval).

Pros

  • Suficient de simplu pentru a fi implementat oriunde (nu necesită biblioteci, nu necesită suport de limbaj).
  • Mai multe module pot fi definite într-un singur fișier.

Cons

  • Nici o modalitate de a importa programatic module (cu excepția utilizării eval).
  • Dependențele trebuie gestionate manual.
  • Încărcarea asincronă a modulelor nu este posibilă.
  • Dependențele circulare pot fi supărătoare.
  • Dificil de analizat pentru analizatorii de cod static.

CommonJS

CommonJS este un proiect care își propune să definească o serie de specificații pentru a ajuta la dezvoltarea de aplicații JavaScript pe server. Unul dintre domeniile pe care echipa CommonJS încearcă să le abordeze este cel al modulelor. Dezvoltatorii Node.js au intenționat inițial să urmeze specificațiile CommonJS, dar ulterior au decis să nu o facă. În ceea ce privește modulele, implementarea Node.js este foarte influențată de aceasta:

// 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)}`);

Într-o seară la Joyent, când am menționat că sunt puțin frustrat de o cerere ridicolă pentru o caracteristică despre care știam că este o idee groaznică, el mi-a spus: „Uitați de CommonJS. Este mort. Noi suntem server side JavaScript”. – Isaac Z. Schlueter, creatorul NPM, citându-l pe Ryan Dahl, creatorul Node.js

Există abstracțiuni deasupra sistemului de module Node.js sub forma unor biblioteci care fac legătura între modulele Node.js și CommonJS. În scopul acestei postări, vom arăta doar caracteristicile de bază care sunt în mare parte aceleași.

În ambele module Node și CommonJS există în esență două elemente pentru a interacționa cu sistemul de module: require și exports. require este o funcție care poate fi utilizată pentru a importa simboluri dintr-un alt modul în domeniul curent. Parametrul transmis către require este id-ul modulului. În implementarea Node, acesta este numele modulului din interiorul directorului node_modules (sau, dacă nu se află în acest director, calea către acesta). exports este un obiect special: orice lucru pus în el va fi exportat ca element public. Numele pentru câmpuri sunt păstrate. O diferență deosebită între Node și CommonJS apare în forma obiectului module.exports. În Node, module.exports este adevăratul obiect special care este exportat, în timp ce exports este doar o variabilă care este legată în mod implicit la module.exports. CommonJS, pe de altă parte, nu are un obiect module.exports. Implicația practică este că în Node nu este posibil să exportați un obiect complet pre-construit fără a trece prin module.exports:

// 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 };}

Modulele CommonJS au fost concepute cu gândul la dezvoltarea de servere. În mod firesc, API-ul este sincron. Cu alte cuvinte, modulele sunt încărcate în momentul și în ordinea în care sunt necesare în interiorul unui fișier sursă.

Pros

  • Simplu: un dezvoltator poate înțelege conceptul fără să se uite la documentație.
  • Gestionarea dependențelor este integrată: modulele necesită alte module și sunt încărcate în ordinea necesară.
  • require pot fi apelate oriunde: modulele pot fi încărcate programatic.
  • Sunt acceptate dependențele circulare.

Cons

  • Apif-ul sincron face ca acesta să nu fie potrivit pentru anumite utilizări (client-side).
  • Un singur fișier per modul.
  • Browserele necesită o bibliotecă de încărcare sau transpilare.
  • Nici o funcție de constructor pentru module (Node suportă totuși acest lucru).
  • Dificil de analizat pentru analizatorii de cod static.

Implementații

Am vorbit deja despre o implementare (în formă parțială): Node.js.

Node.js JavaScript Modules

Pentru client, există în prezent două opțiuni populare: webpack și browserify. Browserify a fost dezvoltat în mod explicit pentru a analiza definițiile modulelor de tip Node (multe pachete Node funcționează out-of-the-box cu el!) și pentru a grupa codul dvs. plus codul din acele module într-un singur fișier care transportă toate dependențele. Webpack, pe de altă parte, a fost dezvoltat pentru a gestiona crearea de conducte complexe de transformări ale sursei înainte de publicare. Aceasta include gruparea împreună a modulelor CommonJS.

Asynchronous Module Definition (AMD)

AMD s-a născut dintr-un grup de dezvoltatori care erau nemulțumiți de direcția adoptată de CommonJS. De fapt, AMD a fost despărțit de CommonJS la începutul dezvoltării sale. Principala diferență dintre AMD și CommonJS constă în suportul său pentru încărcarea asincronă a modulelor.

//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 () {};});

Încărcarea asincronă este posibilă prin utilizarea idiomului tradițional de închidere al JavaScript: o funcție este apelată atunci când modulele solicitate au terminat de încărcat. Definițiile modulelor și importul unui modul sunt efectuate de aceeași funcție: atunci când un modul este definit, dependențele sale sunt făcute explicite. Prin urmare, un încărcător AMD poate avea o imagine completă a graficului de dependențe pentru un anumit proiect la momentul execuției. Astfel, bibliotecile care nu depind unele de altele pentru încărcare pot fi încărcate în același timp. Acest lucru este deosebit de important pentru browsere, unde timpii de pornire sunt esențiali pentru o experiență bună a utilizatorului.

Pros

  • Încărcare asincronă (timpi de pornire mai buni).
  • Sunt acceptate dependențele circulare.
  • Compatibilitate pentru require și exports.
  • Managementul dependențelor complet integrat.
  • Modulii pot fi împărțiți în mai multe fișiere dacă este necesar.
  • Funcțiile de construcție sunt suportate.
  • Suport pentru plugin-uri (etape de încărcare personalizate).

Cons

  • Sunt ușor mai complexe din punct de vedere sintactic.
  • Bibliotecile de încărcare sunt necesare dacă nu sunt transpilate.
  • Dificil de analizat pentru analizatorii de cod static.

Implementații

În prezent, cele mai populare implementări ale AMD sunt require.js și Dojo.

Require.js pentru module JavaScript

Utilizarea require.js este destul de simplă: includeți biblioteca în fișierul HTML și folosiți atributul data-main pentru a-i spune lui require.js ce modul trebuie să fie încărcat primul. Dojo are o configurație similară.

Modulele ES2015

Din păcate, echipa ECMA din spatele standardizării JavaScript a decis să abordeze problema modulelor. Rezultatul poate fi văzut în cea mai recentă versiune a standardului JavaScript: ECMAScript 2015 (cunoscut anterior ca ECMAScript 6). Rezultatul este plăcut din punct de vedere sintactic și compatibil atât cu modurile de operare sincrone, cât și cu cele asincrone.

//------ 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

Exemplu preluat de pe blogul lui Axel Rauschmayer

Directiva import poate fi utilizată pentru a aduce modulele în spațiul de nume. Această directivă, spre deosebire de require și define, nu este dinamică (adică nu poate fi apelată în orice loc). Directiva export, pe de altă parte, poate fi folosită pentru a face publice elementele în mod explicit.

Natura statică a directivelor import și export permite analizatorilor statici să construiască un arbore complet de dependențe fără a rula codul. ES2015 suportă încărcarea dinamică a modulelor:

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

În realitate, ES2015 specifică doar sintaxa pentru încărcătoare de module dinamice și statice. În practică, implementările ES2015 nu sunt obligate să facă nimic după analizarea acestor directive. Încărcătoarele de module, cum ar fi System.js, sunt încă necesare până la publicarea următoarei specificații ECMAScript.

Această soluție, în virtutea faptului că este integrată în limbaj, permite timpilor de execuție să aleagă cea mai bună strategie de încărcare a modulelor. Cu alte cuvinte, atunci când încărcarea asincronă oferă beneficii, aceasta poate fi utilizată de către timpul de execuție.

Pros

  • Sunt suportate încărcarea sincronă și asincronă.
  • Simplu din punct de vedere sintactic.
  • Suport pentru instrumentele de analiză statică.
  • Integrată cu limbajul (în cele din urmă este suportată peste tot, nu este nevoie de biblioteci).
  • Suportă dependențe circulare.

Cons

  • Încă nu este suportat peste tot.

Impletări

Din păcate, niciunul dintre cele mai importante runtime-uri JavaScript nu suportă modulele ES2015 în ramurile lor stabile actuale. Acest lucru înseamnă că nu există suport pentru Firefox, Chrome sau Node.js. Din fericire, multe transpilatoare acceptă modulele și este disponibil și un polyfill. În prezent, preset-ul ES2015 pentru Babel poate gestiona modulele fără probleme.

Babel for JavaScript Modules

The All-in-One Solution: System.js

Se poate să vă găsiți încercând să vă îndepărtați de codul moștenit folosind un sistem de module. Sau poate doriți să vă asigurați că, orice s-ar întâmpla, soluția pe care ați ales-o va funcționa în continuare. Intrați în System.js: un încărcător de module universal care acceptă modulele CommonJS, AMD și ES2015. Poate funcționa în tandem cu transpilatoare precum Babel sau Traceur și poate suporta Node și mediile IE8+. Folosirea lui este o chestiune de încărcare a System.js în codul dvs. și apoi de direcționare către URL-ul de bază:

 <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>

Pentru că System.js face toată treaba din mers, utilizarea modulelor ES2015 ar trebui, în general, să fie lăsată pe seama unui transpiler în timpul etapei de construire în modul de producție. Atunci când nu se află în modul de producție, System.js poate apela transpilerul în locul dumneavoastră, asigurând o tranziție fără probleme între mediile de producție și de depanare.

Apropo: Ce folosim la Auth0

La Auth0, folosim foarte mult JavaScript. Pentru codul nostru de pe partea serverului, folosim module Node.js de tip CommonJS. Pentru anumite coduri pe partea de client, preferăm AMD. Pentru biblioteca noastră de blocare fără parolă bazată pe React, am optat pentru module ES2015.

Vă place ce vedeți? Înscrieți-vă și începeți să utilizați Auth0 în proiectele dvs. astăzi.

Sunteți un dezvoltator și vă place codul nostru? Dacă da, aplicați acum pentru o poziție de inginer. Avem o echipă grozavă!

Concluzie

Construirea modulelor și gestionarea dependențelor era greoaie în trecut. Soluțiile mai noi, sub formă de biblioteci sau module ES2015, au eliminat cea mai mare parte a durerii. Dacă intenționați să începeți un nou modul sau un nou proiect, ES2015 este calea cea mai bună de urmat. Acesta va fi întotdeauna susținut, iar suportul actual, folosind transpileri și polifuncții, este excelent. Pe de altă parte, dacă preferați să rămâneți la codul ES5 simplu, împărțirea obișnuită între AMD pentru client și CommonJS/Node pentru server rămâne alegerea obișnuită. Nu uitați să ne lăsați părerile dumneavoastră în secțiunea de comentarii de mai jos. Hack on!

Lasă un comentariu