Moduly v Javascripte

10/2016 / Milan "perún" Herda / @moriquend / prezentacie.perunhq.org

Časť 2: CommonJS a AMD

Úloha:

  1. Uložte dva moduly vytvorené minule do súborov unit/unitFactory.js a unit/ranged.js
  2. Načítajte ich vo vašom index.js
  3. Vytvorte inštancie pre kopijníka, jazdca a lukostrelca

Riešenie v CommonJS formáte


var unitFactory = function () {
    var name,
        speed,
        strength,
        health,
        obj = {};

    obj.setName = function (newName) {
        name = newName;

        return obj;
    };

    obj.getName = function () {
        return name;
    };

    obj.setSpeed = function (newSpeed) {
        speed = newSpeed;

        return obj;
    };

    obj.getSpeed = function () {
        return speed;
    };

    obj.setStrength = function (newStrength) {
        strength = newStrength;

        return obj;
    };

    obj.getStrength = function () {
        return strength;
    };

    obj.setHealth = function (newHealth) {
        health = newHealth;

        return obj;
    };

    obj.getHealth = function () {
        return health;
    };

    return obj;
};

module.exports = unitFactory;
                    

Riešenie v CommonJS formáte


var extendByRange = function (unit) {
    var range;

    unit.setRange = function (newRange) {
        range = newRange;

        return unit;
    };

    unit.getRange = function () {
        return range;
    };

    return unit;
};

module.exports = extendByRange;
                    

Riešenie v CommonJS formáte


var unitFactory   = require('./unit/unitFactory'),
    extendByRange = require('./unit/ranged');

var pikeman  = unitFactory(),
    horseman = unitFactory(),
    archer   = extendByRange(unitFactory());

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

CommonJS

require a module.exports

Všetko vo vnútri modulu je lokálne iba pre modul a von sa dostane iba to, čo uvedieme v module.exports

CommonJS bol projekt, ktorý začal vznikať v roku 2009 v Mozille ako ekosystém pre server-side JavaScript.

V roku 2013 ho začal Node.js opúšťať, ale formát pre moduly ostal.

Takto sa teda pracovalo s modulmi na serverovej strane pred ES2015 (resp. Babelom).

Poďme na web!

Dnešný web

  • Stránky sa stali aplikáciami
  • Na frontende sa toho deje stále viac
  • Množstvo a zložitosť JS kódu rastie
  • JS sa rieši aj na serverovej strane

Požiadavky na vývoj

  • viacnásobne použiteľné časti kódu
  • jednoduché vkladanie závislostí

Úloha:

  • Vytvorte súbor www/index.html a adresár www/js
  • Uložte dva moduly vytvorené minule do súborov unit/unitFactory.js a unit/ranged.js do vnútra www/js
    * odstráňte z nich riadok s module.exports
  • Načítajte ich na stránke a vytvorte inštancie pre kopijníka, jazdca a lukostrelca

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS module example</title>

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

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

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

        console.log(archer);
    </script>
</head>
<body>

</body>
</html>
                    

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

  1. moduly sú v globálnom mennom priestore
  2. načítavanie cez script tag blokuje rendering stránky
  3. priveľa súborov na načítanie

Riešenia

  1. moduly sú v globálnom mennom priestore
    obaliť hlavný skript aj s načítaním modulov do funkcie
  2. načítavanie cez script tag blokuje rendering stránky
    presun skriptov na koniec stránky?
  3. priveľa súborov na načítanie
    spojenie do jedného súboru

Už poznáme CommonJS moduly, tak ich skúsime

Pomôžeme si webpack-om

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

Mohli by sme skúsiť aj browserify, ale webpack je viac cool :)

Inštalácia


npm init
npm install webpack -g
                    

Adresáre

  • webpack.config.js
  • assets
    • src
      • js
        • app.js
        • unit
          • unitFactory.js
          • ranged.js
  • www
    • index.html

Nové adresáre, pretože toto je už seriózna práca a chceme mať pekne oddelené zdrojáky od vygenerovaných vecí

app.js potrebujeme ako nový vstupný bod, pretože nechceme exportovať premenné do globálneho priestoru

webpack.config.js


module.exports = {
    entry: {
        app: './assets/src/js/app.js'
    },
    output: {
        path: './www/assets/js',
        filename: '[name].js'
    }
};
                    

Z unitFactory a ranged urobíme CommonJS moduly

Na koniec súborov pridáme


// assets/src/js/unit/unitFactory.js

module.exports = unitFactory;

// assets/src/js/unit/ranged.js

module.exports = extendByRange;
                    

app.js


var unitFactory   = require('./unit/unitFactory'),
    extendByRange = require('./unit/ranged');

var pikeman  = unitFactory(),
    horseman = unitFactory(),
    archer   = extendByRange(unitFactory());

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

console.log(archer);
                    

Spustíme webpack


webpack
                    

Hotovo

"Nedostatok" CommonJS prístupu

K premenným v moduloch nemáme prístup zo script tagov v html

Ako nastavíme hodnoty pre jednotky podľa údajov z databázy?

Moduly majú prístup k html a ku globálnym premenným

Ako nastavíme hodnoty pre jednotky podľa údajov z databázy?

  1. údaje z DB uložíme do globálnej premennej ešte pred načítaním app.js
  2. údaje uložíme v json stringu do atribútu nejakého tagu (<head>) a app.js si ich prečíta
  3. app.js si údaje pomocou ajaxu vypýta sám

Iný prístup k modulom


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS module example</title>

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

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

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

        console.log(archer);
    </script>
</head>
<body>

</body>
</html>
                    

Riešenia problémov

  1. moduly sú v globálnom mennom priestore
    obaliť hlavný skript aj s načítaním modulov do funkcie
  2. načítavanie cez script tag blokuje rendering stránky
    načítavať skripty asynchrónne
  3. priveľa súborov na načítanie
    spojenie do jedného súboru

AMD

Asynchronous Module Definition

AMD - require

require slúži na načítanie modulov

  • načítanie modulov je asynchrónne
  • po načítaní všetkých vyžadovaných modulov sa spustí callback

require(['module1', 'module2'], function (module1, module2) {
    // tento kód sa spustí po načítaní oboch modulov
});
                    

AMD - define

define slúži na definíciu modulov

  • ak má modul závislosti, načítavajú sa asynchrónne
  • po načítaní vyžadovaných modulov sa spustí callback definujúci modul

define('názovModulu', ['module1', 'module2'], function (module1, module2) {
    // tento kód sa spustí po načítaní oboch modulov

    return moduleDefinition;
});
                    

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

Závislosti sú nepovinné a nemusí sa uviesť ani prázdne pole

requirejs

Najpopulárnejšia implementácia AMD

Inštalácia


npm init
npm install requirejs --save
npm install gulp del run-requence --save-dev
sudo npm install -g gulp
                    

Úprava index.html


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS module example</title>

    <script src="assets/vendor/require.js"></script>
    <script>
        requirejs.config({
            baseUrl: 'assets/js'
        });

        require(['unit/unitFactory', 'unit/ranged'], function (unitFactory, extendByRange) {
            var pikeman  = unitFactory(),
                horseman = unitFactory(),
                archer   = extendByRange(unitFactory());

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

            console.log(archer);
        });
    </script>
</head>
<body>
</body>
</html>
                    

Modul unitFactory


// súbor assets/src/js/unit/unitFactory.js
define(function () {
    unitFactory = function () {
        var name,
            speed,
            strength,
            health,
            obj = {};

        obj.setName = function (newName) {
            name = newName;

            return obj;
        };

        obj.getName = function () {
            return name;
        };

        obj.setSpeed = function (newSpeed) {
            speed = newSpeed;

            return obj;
        };

        obj.getSpeed = function () {
            return speed;
        };

        obj.setStrength = function (newStrength) {
            strength = newStrength;

            return obj;
        };

        obj.getStrength = function () {
            return strength;
        };

        obj.setHealth = function (newHealth) {
            health = newHealth;

            return obj;
        };

        obj.getHealth = function () {
            return health;
        };

        return obj;
    };

    return unitFactory;
});
                    

Modul ranged


// súbor assets/src/js/unit/ranged.js
define(function () {
    var extendByRange = function (unit) {
        var range;

        unit.setRange = function (newRange) {
            range = newRange;

            return unit;
        };

        unit.getRange = function () {
            return range;
        };

        return unit;
    };

    return extendByRange;
});
                    

Zostavenie (pomocou gulpu)


// obsah súboru gulpfile.js
var gulp = require('gulp'),
    del = require('del'),
    runSequence = require('run-sequence');

gulp.task('clean', function () {
    return del('www/assets/**/*');
});

gulp.task('copy:require', function () {
    return gulp.src(['node_modules/requirejs/*.js'])
        .pipe(gulp.dest('www/assets/vendor'));
});

gulp.task('copy:assets', function () {
    return gulp.src(['assets/src/js/**/*'])
        .pipe(gulp.dest('www/assets/js'));
});

gulp.task('copy', function (done) {
    runSequence('copy:require', 'copy:assets', done);
});

gulp.task('default', function (done) {
    runSequence('clean', 'copy', done);
});
                    

Výhody

  • poradie uvedenia závislostí nie je dôležité
  • jednoduché odovzdávanie parametrov načítaným modulom

Chýba vám niečo v tomto príklade?

Každý samostatný modul je samostatný súbor.

Každý súbor je request na server

r.js

Nástroj pre requirejs, ktorý umožňuje spájať moduly do jedného súboru

Nainštaloval sa spolu s requirejs, ale potrebujeme modul do gulpu


npm install gulp-requirejs --save-dev
                    

Presun kódu z html do app.js


// súbor assets/src/js/app.js
define(['unit/unitFactory', 'unit/ranged'], function (unitFactory, extendByRange) {
    return function () {
        var pikeman  = unitFactory(),
            horseman = unitFactory(),
            archer   = extendByRange(unitFactory());

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

        console.log(archer);
    };
});
                    

Presun kódu z html do app.js


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>JS module example</title>

    <script src="assets/vendor/require.js"></script>
    <script>
        requirejs.config({
            baseUrl: 'assets/js'
        });

        require(['app'], function (app) {
            app();
        });
    </script>
</head>
<body>
</body>
</html>
                    

gulpfile


var gulp        = require('gulp'),
    del         = require('del'),
    runSequence = require('run-sequence'),
    rjs         = require('gulp-requirejs');

gulp.task('clean', function () {
    return del('www/assets/**/*');
});

gulp.task('copy:require', function () {
    return gulp.src(['node_modules/requirejs/*.js'])
        .pipe(gulp.dest('www/assets/vendor'));
});

gulp.task('copy', function (done) {
    runSequence('copy:require', done);
});

gulp.task('rjs', function () {
    rjs({
        name: 'app',
        baseUrl: 'assets/src/js',
        out: 'app.js',
    }).pipe(gulp.dest('www/assets/js'));
});

gulp.task('default', function (done) {
    runSequence('clean', 'copy', 'rjs', done);
});
                    

AMD alebo CommonJS?

Ja som používal AMD, ktoré sa mi pre web zdalo vhodnejšie

S nástupom ES2015, babel a webpack prechádzam na natívne ES2015 moduly

Zdrojové súbory

Nabudúce:

Čo je UMD, moduly v ES2015

Ďakujem za pozornosť

Otázky?