Szerző: Zentai Norbert

Böngésző oldali csomagkezelés jspm segítségével (2. rész)

Az előző bejegyzésben megismerkedtünk a jspm csomagkezelővel és a SystemJS modulbetöltővel. Ebben a bejegyzésben belemegyünk a jspm és SystemJS működésének részleteibe, megválaszolva az előző bejegyzés végén feltett kérdéseket.

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 a module.exports = myExportedObject a kiadott interfész (export). A dependenciák require("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 és define.amd true értéke esetén AMD-ként viselkedik. Amennyiben nem AMD, akkor megnézi, hogy van-e module és module.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.

Ha tetszett a cikk oszd meg másokkal is.