Moduly v JavaScripte

Časť 2: CommonJS a AMD

Milan Herda, 10/2016, úpravy 08/2022

CommonJS

CommonJS

  • 2009, Mozilla, ekosystém pre server-side JavaScript
  • 2013 - Node.js opúšťa CommonJS, ale ponecháva si formát pre moduly

CommonJS nie je súčasťou špecifikácie JavaScriptu a tak nefunguje natívne v prehliadačoch!

CommonJS

Všetko vo vnútri modulu je lokálne iba pre modul.

Von z modulu sa dostane iba to, čo explicitne povolíme.

CommonJS

require
module.exports

Premenná dostupná v každom module. Jej obsah je jediná vec, ktorá sa z modulu exportuje.

Funkcia, pomocou ktorej importujeme iné moduly.

const foo = require('./foo.js');

const bar = () => {
    // ...
};

module.exports = bar;

Stiahnite si zdrojáky

git clone https://bitbucket.org/perungrad/js-modules-2.git

Na najvyššej úrovni máme adresáre pre štyri scenáre, ktoré dnes budeme riešiť:

  • server-commonjs
  • web-oldschool
  • web-commonjs
  • web-amd

V každom z týchto adresárov je ešte adresár src a v ňom sú uložené funkcie unitFactory a ranged z predchádzajúceho školenia

Úloha

  • pracujte v adresári server-commonjs
  • prepíšte oba moduly do CommonJS formátu
  • načítajte ich v index.js
  • vytvorte inštancie pre kopijníka, jazda a lukostrelca
  • "zobrazte" obsah inštancií (napr. cez console.log)
  • require - funkcia pre načítanie modulu
  • module.exports - premenná, do ktorej zapíšete, čo váš modul exportuje

Riešenie

const unitFactory = function() {
    // ...
};

module.exports = unitFactory;

src/unit/unitFactory.js

Riešenie

const extendByRange = function(unit) {
    // ...
};

module.exports = extendByRange;

src/unit/ranged.js

Riešenie

const unitFactory = require('./src/unit/unitFactory.js');
const extendByRange = require('./src/unit/ranged.js');

const pikeman = unitFactory()
    .setName('Pikeman')
    .setSpeed(2)
    .setStrength(3)
    .setHealth(4);

const horseman = /* ... */

const archer = extendByRange(unitFactory())
    .setName('Archer')
    .setSpeed(2)
    .setStrength(2)
    .setHealth(4)
    .setRange(4);

console.log({ pikeman });
console.log({ horseman });
console.log({ archer });

index.js

Toto bolo na serveri.

Ako je to v prehliadači?

Old School prístup

  • dá sa vidieť v adresári web-oldschool
  • "moduly" sa načítavajú pomocou obyčajného script tagu
  • všetky funkcie sú v globálnom priestore

Old School prístup

const unitFactory = function(unit) {
    // ...
};

src/unit/unitFactory.js

Old School prístup

const extendByRange = function(unit) {
    // ...
};

src/unit/ranged.js

Old School prístup

<script src="src/unit/unitFactory.js"></script>
<script src="src/unit/ranged.js"></script>

<script>
    const pikeman = unitFactory();
    const horseman = unitFactory();
    const archer = extendByRange(unitFactory());

    archer.setName('Robin Hood').setSpeed(1).setStrength(5).setHealth(10).setRange(5);

    console.log({ pikeman });
    console.log({ horseman });
    console.log({ archer });
</script>

index.html

Aké problémy vidíte v tomto prístupe?

  1. kód "modulov" je v globálnom mennom priestore
  2. načítavanie cez script tag blokuje rendering stránky
  3. vo väčšom projekte priveľa súborov na načítanie

Možné riešenie?

1. kód "modulov" je v globálnom mennom priestore

2. načítavanie cez script tag blokuje rendering stránky

3. vo väčšom projekte priveľa súborov na načítanie

Môžeme skúsiť obaliť hlavný skript a načítavanie modulov do funkcie (Module Pattern)

Môžeme napr. presunúť script tag na koniec stránky

Spojíme všetky súbory do jedného

CommonJS

v prehliadači

CommonJS nie je súčasťou JS, takže nie je prítomné v prehliadačoch.

Potrebujeme knižnicu, nástroj, čokoľvek, čo nám ho dodá.

webpack

webpack je "module bundler" pre web, ktorý vie pracovať aj s CommoJS formátom

Funguje tak, že si prečíta vaše zdrojové súbory a pretransformuje ich do cieľovej formy vhodnej pre prehliadače.

Tieto pretransformované súbory sa potom nasadzujú do produkcie.

webpack

Inštalácia

cd web-commonjs

npm init
npm install --dev --save webpack webpack-cli

# alebo

yarn init
yarn add -D webpack webpack-cli

webpack

Adresárová štruktúra projektu

  • src/ - zdrojový adresár
    • unit/
      • unitFactory.js - CommonJS modul
      • ranged.js - CommonJS modul
    • index.js - hlavný skript
  • www/ - cieľový adresár
  • index.html
  • webpack.config.js
  • package.json
  • www/js/app.js bude náš nový vstupný bod do aplikácie
  • bude sa načítavať v html cez jediný script tag
  • app.js sa bude generovať webpackom
  • zdrojový súbor pre vygenerovanie app.js bude src/index.js

webpack

Konfigurácia, webpack.config.js

const path = require('path');

module.exports = {
    entry: {
        app: './src/index.js',
    },
    output: {
        path: path.resolve(__dirname, 'www/js'),
        filename: '[name].js',
    },
    mode: 'production',
};

webpack

src/index.js

const unitFactory = require('./unit/unitFactory.js');
const extendByRange = require('./unit/ranged.js');

const pikeman = unitFactory()
    .setName('Pikeman')
    .setSpeed(2)
    .setStrength(3)
    .setHealth(4);

const horseman = /* ... */

const archer = extendByRange(unitFactory())
    .setName('Archer')
    .setSpeed(2)
    .setStrength(2)
    .setHealth(4)
    .setRange(4);

console.log({ pikeman });
console.log({ horseman });
console.log({ archer });

webpack

src/unit/unitFactory.js

const unitFactory = function() {
    // ...
};

module.exports = unitFactory;

webpack

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>CommonJS module example</title>

    <script src="www/js/app.js"></script>
</head>

<body></body>

</html>

webpack

Spustenie

npx run webpack

# alebo

yarn webpack

webpack

Pozrieme sa na vygenerovaný súbor

a objavíme

Module Pattern a IIFE!

AMD

AMD

Asynchronous Module Definition

Problémy Old School prístupu

1. kód "modulov" je v globálnom mennom priestore

2. načítavanie cez script tag blokuje rendering stránky

3. vo väčšom projekte priveľa súborov na načítanie

Môžeme skúsiť obaliť hlavný skript a načítavanie modulov do funkcie (Module Pattern)

Môžeme skúsiť moduly načítavať asynchrónne

Spojíme všetky súbory do jedného

AMD

require
  • Funkcia, pomocou ktorej importujeme iné moduly
  • Načítanie sa deje asynchrónne
  • Po načítaní všetkých požadovaných modulov sa spustí callback
require(
    ['path/to/module1', 'path/to/module2'],
    function (module1, module2) {
        // kód spustený po načítaní modulov
    }
);

AMD

define
  • Funkcia, pomocou ktorej definujeme modul
  • Ak má modul závislosti, načítavajú sa asynchrónne
  • Po načítaní všetkých požadovaných modulov sa spustí callback definujúci modul
  • Návratová hodnota callbacku reprezentuje API modulu
define(
    'nazov', // názov je voliteľný
    ['path/to/module1', 'path/to/module2'], // závislosti
    function (module1, module2) { // kód modulu, povinné
        // kód spustený po načítaní modulov
    }
);

Názov modulu je nepovinný, pokiaľ je modul v samostatnom súbore

Závislosti sú nepovinné a pokiaľ nie sú, tak sa nemusí uviesť ani prázdne pole

Závislosť môžeme uviesť ako cestu k súboru s modulom alebo ako názov modulu

AMD

v prehliadači

AMD nie je súčasťou JS, takže nie je prítomné v prehliadačoch.

Potrebujeme knižnicu, nástroj, čokoľvek, čo nám ho dodá.

RequireJS

RequireJS je "module loader" pre moduly napísané v AMD formáte

Je možné ho nakonfigurovať tak, aby vedel pracovať aj s nie-AMD formátom

RequireJS

Inštalácia

cd web-amd

npm init
npm install --dev --save requirejs

# alebo

yarn init
yarn add -D requirejs

AMD

src/index.js

require(['unit/unitFactory', 'unit/ranged'], (unitFactory, extendByRange) => {
    const pikeman = unitFactory()
        .setName('Pikeman')
        .setSpeed(2)
        .setStrength(3)
        .setHealth(4);

    const horseman = /* ... */

    const archer = extendByRange(unitFactory())
        .setName('Archer')
        .setSpeed(2)
        .setStrength(2)
        .setHealth(4)
        .setRange(4);

    console.log({ pikeman });
    console.log({ horseman });
    console.log({ archer });
});

AMD

src/unit/unitFactory

define(() => {
    const unitFactory = function() {
        // ...
    }
      
    return unitFactory;
});

AMD

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>AMD module example</title>
</head>

<body>
    <script
        src="node_modules/requirejs/require.js"
        data-main="src/index"
    ></script>
</body>

</html>

AMD formát nám umožňuje mať viacero modulov v jednom súbore.

Vďaka tomu vieme optimalizovať súbory pre produkciu a spájať moduly do jedného alebo viac súborov.

RequireJS poskytuje pre tento účel utilitu nazvanú r.js

RequireJS

Adresárová štruktúra projektu

  • src/ - zdrojový adresár
    • unit/
      • unitFactory.js - AMD modul
      • ranged.js - AMD modul
    • index.js - hlavný skript
  • www/ - cieľový adresár pre build z r.js
  • index.html
  • package.json
  • www/js/app.js bude náš nový vstupný bod do aplikácie
  • bude sa načítavať v html cez jediný script tag
  • app.js sa bude generovať pomocou r.js
  • zdrojový súbor pre vygenerovanie app.js bude src/index.js

RequireJS

package.json

{
    "name": "web-amd",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
    "dependencies": {
        "requirejs": "^2.3.6"
    },
    "scripts": {
        "build": "r.js -o baseUrl=./src paths.requireLib=../node_modules/requirejs/require include=requireLib name=index out=www/js/app.js"
    }
}
/*
 *  r.js
 *    -o baseUrl=./src
 *    paths.requireLib=../node_modules/requirejs/require
 *    include=requireLib
 *    name=index
 *    out=www/js/app.js
 */

RequireJS

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <title>AMD module example</title>
</head>

<body>
    <script src="www/js/app.js"></script>
</body>

</html>

Zhrnutie

CommonJS alebo AMD?

  • ✅ AMD bolo vytvárané pre web
  • ❌ síce je použiteľné aj v prostredí Node.js, ale je to trochu meh
  • ✅ v AMD sú závislosti viditeľné
  • ✅ v AMD sa pekne aplikuje Dependency Injection
  • ✅ AMD modul sa jednoduchšie testuje, lebo závislosti dostáva ako argumenty funkcie

CommonJS alebo AMD?

  • ✅ CommonJS bolo vytvárané pre server
  • ❌ CommonJS nie je použiteľné v browseroch a tak sa tam implementuje ako Module Pattern
  • ❌ závislosti nie sú viditeľné, lebo require sa môže použiť aj uprostred kódu
  • ❌ CommonJS modul sa ťažšie testuje, lebo nie sú viditeľné závislosti a treba zložito mockovať importy

CommonJS alebo AMD?

CommonJS alebo AMD?

Historicky vyhral CommonJS

  • bol dostatočne dobrý
  • vývojári naň boli zvyknutí z prostredia Node.js
  • webpack prevalcoval ostatné nástroje pre bundlovanie

Súčasnosť a budúcnosť

  • CommonJS sa stáva minulosťou
  • natívne JS moduly sú podporvané v Node.js, prehliadačoch a v nových serverových runtimeoch (Deno, Bun)
  • webpack sa stáva zbytočným (alternatívy: Rollup, Parcel, Vite)

Nabudúce

Stručne o UMD

a obšírnejšie o natívnych JS moduloch

Ďakujem za pozornosť

Moduly v JavaScripte, časť 2

By Milan Herda

Moduly v JavaScripte, časť 2

  • 71