Modulformátumok
JavaScript már egy ideje hadilábon áll a modulformátumokkal, mivel a nyelvbe csak pár éve került be az ECMAScript modul szintaktika, amely egységes, hivatalos modulformátuma lesz a JavaScript-nek. Ugyanakkor több funkció hiányzik az ECMAScript modulokból, ami a jelenlegi ökoszisztéma számára elengedhetetlen, például a nem relatív modulhivatkozások támogatása. Ezért a hivatalosan (az ECMAScript által) nem elfogadott formátumok támogatása elengedhetetlen. A gyakran használt modulformátumok a global, commonjs, amd, system, ecmascript modules. A modulformátumok konkrét SystemJS implementációjáról bővebben a SystemJS module formats dokumentációban olvashatunk.
Gyors áttekintés, hogy melyik formátum hol használatos, és mi a megvalósítása.
- Global: Valójában nem modulformátum, ugyanis nincs dependenciakezelése, csak a globális névtérből dolgozik és a globális névtérbe ír.
- CommonJS: a fájl egy saját
module
objektumot kap, ahol amodule.exports = myExportedObject
a kiadott interfész (export). A dependenciákrequire("modulnév")
meghívásával szinkronosan kerülnek betöltésre. A modulformátum Node.js oldalon használatos. - AMD: Asynchronous module definition, lényegében CommonJS, ami a böngésző asszinkronos limitációját kívánja megoldani. Minden modul egy
define(["dep1", "dep2", ...], function(dep1, dep2, ...) { return myExportedObject; });
formát vesz fel. A dependenciák tömbben kerülnek felsorolásra. A dependenciák betöltése után az átadott factory function meghívásra kerül, paraméterben átadva a dependenciák exportjait a megfelelő sorrendben. A modul exportja a function visszaadott értéke, vagy ha bekértük a speciális"exports"
dependenciát, akkor arra fűzhetjük fel a kívánt exportokat. - UMD: Universal module definition, egy speciális modulformátum, amely egyszerre feldolgozható Global, CommonJS vagy AMD formátumként is. A megvalósítása implementációnként más lehet, viszont általában a
define
létezése ésdefine.amd
true
értéke esetén AMD-ként viselkedik. Amennyiben nem AMD, akkor megnézi, hogy van-emodule
ésmodule.exports
, mert ebben az esetben CommonJS-ként adja ki magát. Ha egyik formátumot se sikerült detektálni, akkor Global-ként viselkedik. - ECMAScript module: JavaScript hivatalos modulformátuma, dependenciák betöltése deklaratív (nem lehet változó alapján dependálni), az exportok szintúgy deklaratívak (nem lehet dinamikusan előállítani az exportok neveit). Nagy előnye, hogy futás nélkül, csak a kód parszolásával megszerezhető az összes dependenciainformáció. Hátránya, hogy a futásidőben történő betöltési folyamatba semmilyen módosítást vagy belátást nem biztosít még.
- System/SystemJS: Az ECMAScript module modulformátum elterjedéséig használatos kompatibilitási formátum, amely imitálja a hivatalos működést. Betöltése csak SystemJS, vagy azzal kompatibilis implementáció, segítségével lehetséges.
SystemJS-nek fájlpattern, csomag vagy csomagon belüli fájlpattern alapján meg tudjuk adni az adott fájl modulformátumát.
SystemJS.config({
meta: {
"path/to/amd/*.js": {
format: "amd"
}
},
packages: {
"my-package": {
format: "system",
meta: {
"inside/this/package/*.js": {
format: "cjs"
}
}
}
}
});
SystemJS ezeket a modulformátumokat általában detektálja, viszont vannak helyzetek, ahol célszerű előre megadni. Például böngészőkompatiblis formátumok, mint system, amd <script>
-ként betölthetőek, míg a többi formátumhoz a javascript forráskódot előre le kell töltenie a SystemJS-nek majd:
- ECMAScript module esetében a jelenlegi specifikáció limitációi miatt fordítani kell.
- Global esetén a futtatás előtt minden dependenciát be kell tölteni, nem lehet várakoztatni a futtatását.
- CommonJS esetén a futtatás előtt minden dependenciát detektálni kell, betölteni, majd mint global esetén, csak ezután futtatható le a modul kódja.
Ha nem adjuk meg előre a modulformátumot, SystemJS mindenképp le fogja tölteni a forráskódot, majd később lefuttatni azt eval
segítségével. UMD formátum esetén félredetektálhat global-nak, amikor használhatott volna amd-t is, ezzel a dependenciainformációkat elveszítheti.
Global és CommonJS
Mind a global, mind a CommonJS formátum problémás a SystemJS számára, a szinkronos viselkedésük és a nem deklaratív (imperatív) dependenciahivatkozásuk miatt. SystemJS számára érdemes megadni ezeknek a formátumoknak az összes dependenciáját és global esetében a felregisztrált property nevét.
CommonJS esetén csak gyenge kódanalízissel "tippeli meg" a dependenciákat. Global esetén a dependenciákról semmilyen információt nem képes kinyerni, illetve az export csak egy, a globális objektumon lefuttatott különbség vizsgálattal kerül megállapításra, amit szintúgy félredetektálhat.
SystemJS.config({
meta: {
"some-global-file.js": {
format: "global",
deps: ["jquery"],
exports: "myGlobalVariable"
},
"some-commonjs-file.js": {
format: "cjs",
//Ezek a nevek azok amikkel a require meghívásra fog kerülni
deps: ["some-global", "./other-deps"]
}
}
});
ECMAScript modulok
SystemJS.config({
meta: {
"some-esm.js": {
format: "esm"
}
}
});
SystemJS-nek ezeket az ECMAScript modulokat előre le kell töltenie, kiszednie a dependenciainformációkat és át kell alakítania SystemJS (ECMAScript module-t imitáló) formátummá. Erre szükség van olyan böngészők esetében is, ahol az ECMAScript modulok támogatottak, mivel a betöltés folyamatába a SystemJS nem lát be és nem tud beleszólni, a Loader specifikáció pedig, ami ezt lehetővé tenné, még nincs kész.
A modul forráskódjának átalakításához a SystemJS-nek szüksége van JavaScript transpilerre, például babel-re és az adott transpilert SystemJS-el összekötő plugin-re. A transpiler-t a SystemJS csak ECMAScript modulok fordításához fogja használni, más modulokra nem.
SystemJS.config({
map: {
"plugin-babel": "path/to/systemjs-plugin-babel/plugin-babel.js"
},
transpiler: "plugin-babel"
});
Hogyan adom ezt meg JSPM szinten?
A modulformátumokról szóló részben végig a SystemJS-ről írtam, mivel ezzel a problémával a SystemJS-nek, a betöltőnek kell megküzdenie. A SystemJS konfigurációját, vagyis a dependenciainformációkat, ugyanakkor a jspm kezeli. A dependált csomagok felelőssége, hogy a formátumukat definiálják a saját package.json
fájljukban, az alábbi módon:
{
"name": "amazing-lib",
"version": "1.0.0",
"main": "main.js",
"jspm": {
"meta": {
"main.js": {
"format": "cjs",
"deps": ["some-other-lib"]
}
}
}
}
vagy ha van böngészős modulformátum:
{
"name": "amazing-lib",
"version": "1.0.0",
"main": "main.js",
"jspm": {
"main": "dist/amd/main.js",
"format": "amd"
}
}
Természetesen ez nagy probléma, mivel elég kicsi annak a valószínűsége egy-két nagy csomag kivételével, hogy ezeket az információkat megadják a jspm számára. Ugyanakkor a jspm lehetőséget ad, hogy ezt kézzel pótoljuk, package override segítségével. Jspm minden dependencia esetében megnéz egy git repository-t, hogy az adott dependenciához van-e package.json
javítás. A repository elérése megváltoztatható jspm registry config jspm
parancs segítségével, így lehetőségünk van saját, privát override gyűjteményt létrehozni. Jspm a dependencia telepítése után a használt override konfigurációt beleírja a projekt package.json
fájljába, és a további jspm install
parancsok futtatásakor ezt az override konfigurációt fogja használni.
{
"jspm": {
"name": "app",
"main": "app.js",
"dependencies": {
"jquery": "npm:jquery@^3.3.1"
},
"overrides": {
"npm:jquery@3.3.1": {
"format": "amd"
}
}
}
}
Amennyiben egy override nem felel meg nekünk, úgy azt módosíthatjuk a package.json
fájlunkban, és egy jspm install
után tesztelhetjük is. Ha mindent rendben találtunk, és szeretnénk ezt a javítást másokkal is megosztani, akkor ezt az override-ot feltehetjük az override repository-ba, így legközelebb már nem kell a saját package.json
fájlban módosítani egy-két csomag adatait.
Bundling
Míg Node.js és helyi gépen történő fejlesztésnél nem probléma a betöltött modulok száma, a böngészőnél már komoly gondot jelenthet több száz modulból álló kód modulonkénti letöltése. Jspm lehetővé tesz kód bundling-et, ahol több modult egy fájlban fogalmazunk meg, így a teljes alkalmazásunk JavaScript kódja akár egyetlen fájlba is becsomagolható. Jspm a SystemJS Builder projektet használja arra, hogy a dependenciagráfot felépítse, és a kódot átfordítsa egy vagy több nagy JavaScript fájllá. Erről bővebben a jspm dokumentációjában olvashatunk.
Konklúzió
Jspm egy hiánypótló fejlesztői eszköz, ugyanis jelenleg a böngésző oldali dependenciakezelés általában külön fordítási folyamatot igényel fejlesztési időben is, ráadásul a különböző modulformátumok támogatása általában hiányzik. Ennek azonban megvan az az ára, hogy saját override konfigurációkat kell írni a különböző dependált csomaghoz. A SystemJS modulbetöltőre építés számomra nagy pozitívum, mivel SystemJS futási időben elérhető, így képes nem minden esetben szükséges vagy csak később használandó modulokat dinamikusan betölteni.