Weblog

Modern and modular JavaScript

On the way of investigating the state of JavaScript modularity we found that only ECMAScript 6 brings reasonable methodology to achieve this. It's also importand that it brings short and familar to other technologies class definition import and export syntax. As an approach for module loading in browser SystemJS looks as the most coprehensive solutions nowdays as among ES6 styled modules transpilled to ES5 it can also load modules in other formats widely used such as CommonJS (CJS), AMD (RequireJS) and even globals, so if you have to use a bunch of things like jquery in your modern web app while want to have it smell better SystemJS is the tool just for you. Among loader there is a companion project SystemJS Builder that can assemble all your javascript in bundles according to their dependencies declared with import/export synatx, configurations and autodetection. One more good thing that it can load other types of resources like css, images etc and this means your can specify them as dependencies for your app modules.  

The project is hosted on GitHub: https://github.com/systemjs/systemjs. As expected you can find some documentation and examples on this page.

Configuration

To load modules in browser .config function of System global object is used, it's provided from system.js loaded statically (using script tag) and requires also traceur-runtime.js to be loaded and es6-module-loader.js to be present at the same dirto have module autloading. This will enable lazy but funnly functional loading of modules with fetching their files and ondemand parse.

<script src="js/traceur-runtime.js"></script>
<script src="js/system.js"></script>
<script>

    System.config({
        baseURL: 'app',
        bundles: {'app-bundle': ['views/testview']},
        paths: {
            app-bundle: 'dist/js/app-bundle.js'
        },
        map: {
            underscore: 'lib/underscore'
        }
    });

</script>


General script prefix is defined with baseURL configuration parameter. Module type is autoguessed but you can defined it in meta section. Section bundles is used to defined asembled files containment in other words what modules are located in what bundles. Section paths is used to define name to path and filename aliases e.g.  'jquery': 'path/to/some/jquery-1.x.y.js' will tell the loader to associate jquery module with code provided by concrete file instead of autoloading from app/jquery.js. Section map doest the same but in terms of module to module mapping, so if you want to have module somelbrary-1.2.3 to be known also as somelibrary you should have it provded by configuration logic and possible for loadng.

In case you want to have modular configuration you can use the trick with merging global configuration object and calling config function each time new directives are added. Sothat you will have somethig like:

<script>

    var moduleConfig = moduleConfig || { meta: {}, bundles: {}, paths: {}};

    moduleConfig.meta['views/dmgrid'] = { format: 'register', deps: [
        'jquery_ui', 'jquery_ui_css!css',
         ]
    };
    moduleConfig.bundles['app-bundle'] = ['views/testview'];
    moduleConfig.paths['app-bundle'] = '/js/app-bundle.js';
    moduleConfig.paths['jquery_ui_css'] = '/css/jquery-ui.min.css...';

    System.config(moduleConfig);

</script>

First line does global config object taking into context or creates it's structure in case this is the first time.

The second line does dependency definition that contains also css resources declaration that may be not providing by javascript bundle file.

The third line tells that module testview is located in app-bundle.

The rest of code defines particular file paths for modules.

Usage

So the code that is using this modules can looks line:

index.html

<script>

    System.import('views/testview').then(function(m) {

        var testView = new m.TestView({
            el: '#testView',
            tpl: {
                hello: '#tplHello'
            },
            l11n: {
                helloMessage: 'Hello World !'
            }
        }).render();

    }, console.error.bind(console));

    Promise.all([
        System.import('views/testview'),
        System.import('views/testview2')
    ]).then(function(modules) {

        var testView2 = new modules[1].TestView2({
            el: '#testView2'
        }).render();

    }, console.error.bind(console));

</script>

 

import function callback provieds loaded modules to argument with promise mechanism so you wll have them in m[0] and m.TestView. If you want to import a number of modules implicit call of Promise.all must be used.

To assemble modules in bundle with SystemJS it's subproject named Builder is used. You can find it at: https://github.com/systemjs/builder together with configuration examples. To have a bundle building you should have a script for nodejs that can be runed manually or by buildsystem like grunt or gulp. The good thins is that you can write your code with modern ES6 standart and it will be converted to browser compatible ES5 and all your dependencies even old fasioned will be included automagically. For build script you must have a configuration similar to browser config:

build.js

var builder = require('systemjs-builder'),
  path = require('path');

builder.build('views/testview', {
  baseURL: path.resolve('.'),

  meta: {

  },
  ...
  paths: {

    }

}, 'dist/js/app-bundle.js').then(function() {

  console.log('Build complete');
}).catch(function(err) {
  console.log('Build error');
  console.log(err);
});

To avoid bundling of common used and defintely loaded modules you can specifu build: false param for the meta section:

meta: {
  'jquery': { format: 'amd', build: false },
  'jquery_ui': { format: 'amd', deps: ['jquery'], build: false }
},

, Or enchance your script with dependency subtract functionality. To run building simply call:

node build.js

from console. The example assumes you have dist/js directory already created before. For gulp and grunt we may use external command execution plugins:

gulpfile.js

var gulp = require('gulp'),
    run = require('gulp-run')
    ;
gulp.task('systemjs-build', function() {
    run('node build.js').exec();
});
gulp.task('default', ['systemjs-build']);

or

Gruntfile.js

grunt.initConfig({
    exec: {
        'systemjs-build': 'node build.js'
    }
});
grunt.loadNpmTasks('grunt-exec');
grunt.registerTask('default', ['systemjs-build']);

Code

The buildscripts will assemble a view that may looks like the following:

views/testview.js

import $ from "jquery";
import _ from "underscore";
import Backbone from "backbone";

var { View } = Backbone;


export class TestView extends View {

    constructor(params) {
        this.$el = $(params.els.el);

        super(params);
        console.log('test view inited');
    }

    render($el) {
        var $el = $el != undefined ? $el : this.$el;
        $el.html('hello');

    }

}

Here we have Backbone framework used that  is small, compatible with new standarts and have no needless things.

The view is using constructor parameters for dom links and selectors, configurations urls and even localization resources that turnes it into reusable component. In addition it performs some selector caching that reduces costly dom calls and we have all this in cute es6 syntaced classed defined. Your can find full example here: https://bitbucket.org/techminded/es6-backbone-skel/

Average (0 Votes)
The average rating is 0.0 stars out of 5.


No comments yet. Be the first.