JavaScript Module Systems Showdown: CommonJS vs AMD vs ES2015

Kun JavaScript-kehitys yleistyy, nimiavaruudet ja riippuvuudet vaikeutuvat huomattavasti. Tähän ongelmaan on kehitetty erilaisia ratkaisuja moduulijärjestelmien muodossa. Tässä postauksessa tutustumme erilaisiin kehittäjien tällä hetkellä käyttämiin ratkaisuihin ja ongelmiin, joita niillä yritetään ratkaista. Lue eteenpäin!

Esittely: Why Are JavaScript Modules Needed?

Jos tunnet muita kehitysalustoja, sinulla on luultavasti jonkinlainen käsitys kapseloinnin ja riippuvuuden käsitteistä. Eri ohjelmistoja kehitetään yleensä erillään toisistaan, kunnes jokin vaatimus on täytettävä jo olemassa olevalla ohjelmistolla. Sillä hetkellä, kun toinen ohjelmiston osa otetaan mukaan projektiin, sen ja uuden koodinpätkän välille luodaan riippuvuus. Koska näiden ohjelmistokappaleiden on toimittava yhdessä, on tärkeää, ettei niiden välille synny ristiriitoja. Tämä saattaa kuulostaa triviaalilta, mutta ilman jonkinlaista kapselointia on vain ajan kysymys, milloin kaksi moduulia joutuu ristiriitaan keskenään. Tämä on yksi syy siihen, että C-kirjastojen elementeissä on yleensä etuliite:

#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

Kapselointi on välttämätöntä ristiriitojen välttämiseksi ja kehityksen helpottamiseksi.

Kun kyse on riippuvuuksista, perinteisessä asiakaspuolen JavaScript-kehityksessä ne ovat epäsuoria. Toisin sanoen kehittäjän tehtävänä on varmistaa, että riippuvuudet täyttyvät siinä vaiheessa, kun jokin koodilohko suoritetaan. Kehittäjien on myös varmistettava, että riippuvuudet täyttyvät oikeassa järjestyksessä (tiettyjen kirjastojen vaatimus).

Oheinen esimerkki on osa Backbone.js:n esimerkkejä. Skriptit ladataan manuaalisesti oikeassa järjestyksessä:

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

Kun JavaScript-kehitys muuttuu yhä monimutkaisemmaksi, riippuvuuksien hallinta voi käydä hankalaksi. Myös refaktorointi vaikeutuu: mihin uudet riippuvuudet pitäisi laittaa, jotta latausketjun oikea järjestys säilyisi?

JavaScript-moduulijärjestelmät yrittävät ratkaista näitä ja muita ongelmia. Ne ovat syntyneet välttämättömyydestä mukautuakseen jatkuvasti kasvavaan JavaScript-maisemaan. Katsotaanpa, mitä eri ratkaisut tuovat tullessaan.

An Ad Hoc Solution: The Revealing Module Pattern

Useimmat moduulijärjestelmät ovat suhteellisen uusia. Ennen kuin niitä oli saatavilla, erästä tiettyä ohjelmointimallia alettiin käyttää yhä useammassa JavaScript-koodissa: paljastavaa moduulimallia.

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

Tämä esimerkki on otettu Addy Osmanin JavaScript Design Patterns -kirjasta.

JavaScriptin laajuudet (ainakin let ES2015:n let ilmestymiseen asti) toimivat funktiotasolla. Toisin sanoen, mikä tahansa sitoutuminen on ilmoitettu funktion sisällä, ei voi paeta sen scopea. Juuri tästä syystä paljastava moduulikuvio luottaa funktioihin yksityisen sisällön kapseloimiseksi (kuten monet muutkin JavaScript-kuviot).

Yllä olevassa esimerkissä julkiset symbolit paljastuvat palautetussa sanakirjassa. Kaikki muut deklaraatiot on suojattu niitä ympäröivällä funktion scopeilla. Ei ole välttämätöntä käyttää var ja välitöntä kutsua yksityistä scopea ympäröivää funktiota; nimettyä funktiota voidaan käyttää myös moduuleissa.

Tämä kuvio on ollut käytössä jo jonkin aikaa JavaScript-projekteissa, ja se käsittelee kapselointiasiaa melko mukavasti. Riippuvuuskysymykselle se ei tee paljoakaan. Kunnolliset moduulijärjestelmät yrittävät käsitellä myös tätä ongelmaa. Toinen rajoitus on se, että muiden moduulien sisällyttäminen ei onnistu samassa lähdekoodissa (ellei käytetä eval).

Pros

  • Tyydyttävän yksinkertainen toteutettavaksi missä tahansa (ei kirjastoja, ei vaadita kielitukea).
  • Monia moduuleja voidaan määritellä yhdessä tiedostossa.

Miinukset

  • Ei tapaa tuoda moduuleja ohjelmallisesti (paitsi käyttämällä eval).
  • Riippuvuudet on käsiteltävä manuaalisesti.
  • Moduulien asynkroninen lataaminen ei ole mahdollista.
  • Ympyrämäiset riippuvuudet voivat olla hankalia.
  • Vaikea analysoida staattisilla koodianalysaattoreilla.

CommonJS

CommonJS on projekti, jonka tavoitteena on määritellä joukko määrittelyjä, jotka auttavat palvelinpuolen JavaScript-sovellusten kehittämisessä. Yksi osa-alue, jota CommonJS-tiimi yrittää käsitellä, on moduulit. Node.js-kehittäjät aikoivat alun perin noudattaa CommonJS-määrittelyä, mutta päättivät myöhemmin luopua siitä. Moduulien osalta Node.js:n toteutus on saanut paljon vaikutteita siitä:

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

Eräänä iltana Joyentissa, kun mainitsin olevani hieman turhautunut johonkin naurettavaan pyyntöön ominaisuudesta, jonka tiesin olevan kauhea idea, hän sanoi minulle: ”Unohda CommonJS. Se on kuollut. Me olemme palvelinpuolen JavaScript.” – NPM:n luoja Isaac Z. Schlueter siteeraa Node.js:n luojaa Ryan Dahlia

Node.js:n moduulijärjestelmän päällä on abstraktioita kirjastojen muodossa, jotka kurovat umpeen kuilua Node.js:n moduulien ja CommonJS:n välillä. Tässä postauksessa esitellään vain perusominaisuudet, jotka ovat enimmäkseen samat.

Sekä Noden että CommonJS:n moduuleissa on periaatteessa kaksi elementtiä, joilla voi olla vuorovaikutuksessa moduulijärjestelmän kanssa: require ja exports. require on funktio, jolla voidaan tuoda symboleja toisesta moduulista nykyiseen scopeen. Parametri, joka välitetään require:lle, on moduulin id. Noden toteutuksessa se on node_modules-hakemistossa olevan moduulin nimi (tai, jos se ei ole kyseisessä hakemistossa, polku siihen). exports on erityinen objekti: kaikki, mitä siihen laitetaan, viedään julkisena elementtinä. Kenttien nimet säilyvät. Erikoinen ero Noden ja CommonJS:n välillä syntyy module.exports-objektin muodossa. Nodessa module.exports on todellinen erikoisobjekti, joka viedään, kun taas exports on vain muuttuja, joka sidotaan oletuksena module.exports:een. CommonJS:ssä sen sijaan ei ole module.exports-objektia. Käytännön seuraus on, että Nodessa ei ole mahdollista viedä täysin valmiiksi rakennettua objektia käymättä läpi 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 };}

CommonJS-moduulit on suunniteltu palvelinkehitystä ajatellen. Luonnollisesti API on synkroninen. Toisin sanoen moduulit ladataan sillä hetkellä ja siinä järjestyksessä kuin niitä tarvitaan lähdetiedoston sisällä.

Pros

  • Yksinkertainen: kehittäjä voi hahmottaa konseptin katsomatta dokumentteja.
  • Riippuvuuksien hallinta on integroitu: moduulit vaativat muita moduuleja ja ne ladataan tarvittavassa järjestyksessä.
  • require voidaan kutsua missä tahansa: moduulit voidaan ladata ohjelmallisesti.
  • Ympyrämäisiä riippuvuuksia tuetaan.

Miinukset

  • Synkroninen API tekee siitä sopimattoman tiettyihin käyttötarkoituksiin (client-side).
  • Yksi tiedosto per moduuli.
  • Selaimet vaativat latauskirjaston tai transpiloinnin.
  • Ei konstruktoritoimintoa moduuleille (Node tosin tukee tätä).
  • Vaikea analysoida staattisille koodianalysaattoreille.

Toteutukset

Olemme jo puhuneet yhdestä toteutuksesta (osittaisena): Node.js.

Node.js JavaScript-moduulit

Asiakkaalle on tällä hetkellä kaksi suosittua vaihtoehtoa: webpack ja browserify. Browserify kehitettiin nimenomaan jäsentämään Noden kaltaisia moduulimäärittelyjä (monet Node-paketit toimivat sen kanssa out-of-the-box!) ja niputtamaan sinun koodisi sekä näiden moduulien koodi yhteen tiedostoon, joka sisältää kaikki riippuvuudet. Webpack taas kehitettiin käsittelemään monimutkaisten lähdemuunnosten putkistojen luomista ennen julkaisemista. Tähän kuuluu myös CommonJS-moduulien niputtaminen yhteen.

Asynchronous Module Definition (AMD)

AMD syntyi ryhmästä kehittäjiä, jotka olivat tyytymättömiä CommonJS:n omaksumaan suuntaan. Itse asiassa AMD erotettiin CommonJS:stä jo varhaisessa kehitysvaiheessa. Suurin ero AMD:n ja CommonJS:n välillä on sen tuki asynkroniselle moduulilataukselle.

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

Asynkroninen lataus mahdollistetaan käyttämällä JavaScriptin perinteistä closure-idiomia: funktiota kutsutaan, kun pyydettyjen moduulien lataus on valmis. Moduulien määrittelyt ja moduulin tuonti suoritetaan samalla funktiolla: kun moduuli määritellään, sen riippuvuudet tehdään selviksi. Näin ollen AMD:n lataajalla voi olla ajonaikana täydellinen kuva tietyn projektin riippuvuusgraafista. Kirjastot, jotka eivät ole toisistaan riippuvaisia ladattaessa, voidaan siis ladata samanaikaisesti. Tämä on erityisen tärkeää selaimille, joissa käynnistymisajat ovat olennaisen tärkeitä hyvän käyttökokemuksen kannalta.

Pros

  • Asynkroninen lataus (paremmat käynnistymisajat).
  • Ympyräriippuvuudet tuetaan.
  • Yhteensopivuus require:n ja exports:n kanssa.
  • Riippuvuuksien hallinta täysin integroitu.
  • Moduulit voidaan jakaa tarvittaessa useampaan tiedostoon.
  • Konstruktorifunktioita tuetaan.
  • Liitännäistuki (mukautetut latausvaiheet).

Miinukset

  • Hieman monimutkaisempi syntaktisesti.
  • Loader-kirjastoja tarvitaan, ellei niitä ole transpiloitu.
  • Vaikea analysoida staattisilla koodianalysaattoreilla.

Toteutukset

Tällä hetkellä suosituimmat toteutukset AMD:ltä vaativat.js ja Dojo.

Require.js JavaScript-moduuleille

T require.js:n käyttäminen on melko suoraviivaista: sisällytä kirjasto HTML-tiedostoosi ja kerro require.js:lle data-main-attribuutilla, mikä moduuli ladataan ensin. Dojossa on samanlainen asetus.

ES2015 Moduulit

Javaskriptin standardoinnin takana oleva ECMA-tiimi päätti onneksi puuttua moduulien ongelmaan. Tulos on nähtävissä JavaScript-standardin uusimmassa versiossa: ECMAScript 2015 (aiemmin ECMAScript 6). Tulos on syntaktisesti miellyttävä ja yhteensopiva sekä synkronisten että asynkronisten toimintatapojen kanssa.

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

Esimerkki otettu Axel Rauschmayerin blogista

Direktiiviä import voidaan käyttää moduulien tuomiseen nimiavaruuteen. Tämä direktiivi, toisin kuin require ja define, ei ole dynaaminen (eli sitä ei voi kutsua missä tahansa paikassa). Sen sijaan export-direktiiviä voidaan käyttää elementtien tekemiseen nimenomaisesti julkisiksi.

import– ja export-direktiivien staattisen luonteen ansiosta staattiset analysaattorit voivat rakentaa täydellisen riippuvuuspuun ilman koodin ajamista. ES2015 tukee kyllä moduulien dynaamista lataamista:

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

Todellisuudessa ES2015 määrittelee vain dynaamisten ja staattisten moduulilataajien syntaksin. Käytännössä ES2015-toteutusten ei tarvitse tehdä mitään näiden direktiivien jäsentämisen jälkeen. System.js:n kaltaisia moduulinlataajia tarvitaan edelleen, kunnes seuraava ECMAScript-spektio julkaistaan.

Tämä ratkaisu, koska se on integroitu kieleen, antaa suoritusaikojen valita moduuleille parhaan latausstrategian. Toisin sanoen, kun asynkroninen lataus antaa etuja, runtime voi käyttää sitä.

Pros

  • Synkroninen ja asynkroninen lataus tuettu.
  • Syntaktisesti yksinkertainen.
  • Tuki staattisille analyysityökaluille.
  • Integroitu kieleen (tuettu lopulta kaikkialle, ei tarvitse kirjastoja).
  • Tukee ympäripyöreitä riippuvuuksia.

Miinukset

  • Ei edelleenkään tueta kaikkialla.

Toteutukset

Ei valitettavasti yksikään tärkeimmistä JavaScript-ajoaikatauluista tue ES2015-moduuleja nykyisissä stabiileissa haaroissaan. Tämä tarkoittaa, että Firefoxille, Chromelle tai Node.js:lle ei ole tukea. Onneksi monet transpilerit tukevat moduuleja, ja saatavilla on myös polyfill. Tällä hetkellä Babelin ES2015-esiasetus osaa käsitellä moduuleja ongelmitta.

Babel for JavaScript Modules

The All-in-One Solution: System.js

Olet ehkä huomannut yrittäväsi siirtyä pois vanhasta koodista käyttämällä yhtä moduulijärjestelmää. Tai saatat haluta varmistaa, että mitä tahansa tapahtuukin, valitsemasi ratkaisu toimii edelleen. Astu sisään System.js: universaali moduulinlataaja, joka tukee CommonJS-, AMD- ja ES2015-moduuleja. Se voi toimia yhdessä Babelin tai Traceurin kaltaisten transpilereiden kanssa ja tukee Node- ja IE8+-ympäristöjä. Sen käyttäminen edellyttää System.js:n lataamista koodissasi ja sen osoittamista perus-URL:iin:

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

Koska System.js tekee kaiken työn lennossa, ES2015-moduulien käyttäminen tulisi yleensä jättää transpilerille build-vaiheen aikana tuotantotilassa. Kun et ole tuotantotilassa, System.js voi kutsua transpileria puolestasi, mikä tarjoaa saumattoman siirtymisen tuotanto- ja virheenkorjausympäristöjen välillä.

Sivut: Mitä käytämme Auth0:ssa

Auth0:ssa käytämme paljon JavaScriptiä. Palvelinpuolen koodissa käytämme CommonJS-tyylisiä Node.js-moduuleja. Tietyissä asiakaspuolen koodeissa suosimme AMD:tä. React-pohjaisessa Passwordless Lock -kirjastossamme olemme valinneet ES2015-moduulit.

Tykkäätkö siitä, mitä näet? Rekisteröidy ja aloita Auth0:n käyttö projekteissasi jo tänään.

Oletko kehittäjä ja pidät koodistamme? Jos olet, hae insinöörin paikkaa nyt. Meillä on mahtava tiimi!

Johtopäätös

Moduulien rakentaminen ja riippuvuuksien käsittely oli aiemmin hankalaa. Uudemmat ratkaisut kirjastojen tai ES2015-moduulien muodossa ovat poistaneet suurimman osan vaivasta. Jos aiot aloittaa uuden moduulin tai projektin, ES2015 on oikea tapa toimia. Sitä tuetaan aina, ja nykyinen tuki transpilereiden ja polyfillien avulla on erinomainen. Toisaalta, jos haluat pitää kiinni pelkästä ES5-koodista, tavanomainen jako AMD:n asiakaskäyttöön ja CommonJS/Node palvelinkäyttöön on edelleen tavanomainen valinta. Älä unohda jättää meille ajatuksiasi alla olevaan kommenttiosioon. Hack on!

Jätä kommentti