Modern Stack for Your Webapp

Using JSPM and Codecept

Jordane Grenat | @JoGrenat

State of the art

  • Non-standard module system
  • Dev-mode requires build
  • Tests are a pain in the a**
  • Complex workflow for build

What do we want?

Package manager

ES2015!

Fat Arrow functions

Generators

Promises

(Classes)

...

...

...

...

ES Modules!

Framework front

Backbone

Tests without pain

Codecept

ES Modules

export function name1() {

};

export const name2 = 'value';

export let name3 = 1;

export class MyClass {};


let name4 = () => {};
let name5 = 5;

export { name4, name5 };

export { name4 as arrowFunc, name5 as five };

Named Exports

export default 'value';

Default Export

import theDefaultExport from 'lib';

import { name1, name2 } from 'lib';

import theDefaultExport, { name1 } from 'lib';

import { name1 as otherName } from 'lib';

import * as myLib from 'lib';

import 'lib';

Imports

Module Loader API

  • Dynamically declare and import modules


     
  • Configure how modules are imported

Dynamic Imports

System.import('lib')
    .then(myLib => {
        // Code
     })
    .catch(error => {
        // Error
    });

Configure imports

SystemJS.config({
  packageConfigPaths: [
    "npm:@*/*.json",
    "npm:*.json",
    "github:*/*.json"
  ],
  transpiler: "plugin-babel",

  map: {
    "backbone": "npm:backbone@1.2.3",
    "css": "github:systemjs/plugin-css@0.1.20",
    "handlebars": "github:components/handlebars.js@4.0.5",
    "hbs": "github:davis/plugin-hbs@1.2.1",
    "jquery": "npm:jquery@2.2.1",
    "json": "github:systemjs/plugin-json@0.1.0",
    "plugin-babel": "npm:systemjs-plugin-babel@0.0.6",
    "process": "github:jspm/nodelibs-process@0.2.0-alpha"
  },

  packages: {
    "app": {
      "main": "app.js"
    },
    "github:davis/plugin-hbs@1.2.1": {
      "map": {
        "handlebars": "github:components/handlebars.js@4.0.5"
      }
    },
    "npm:backbone@1.2.3": {
      "map": {
        "underscore": "npm:underscore@1.8.3"
      }
    }
  }
});

SystemJS

Polyfill for the
ES Module Loader

<script src="system.js"></script>
<script>
  // loads relative to the current page URL
  System.import('./local-module.js'); 

  // load from an absolute URL directly
  System.import('https://code.jquery.com/jquery.js');
</script>

SystemJS is an

universal module manager

It can load:

define(['jquery'], function($) {
    return function() {};
});

AMD Modules

var $ = require('jquery');
exports.myModule = function () {};

CommonJS Modules

import $ from 'jquery';
export default () => {};

ES2015 Modules

window.myModule = function() {};

Global Modules

You don't even CARE what kind of module you're loading!

With plugins, you can also load:

  • CSS
  • Text
  • JSON
  • Images
  • Markdown
  • JSX
  • Fonts
  • ...

Package Manager

Package manager

built on top of SystemJS

It can load packages
from any registry:

  • NPM
  • Bower
  • Github
  • JSPM
  • ...
jspm install npm:aurelia

jspm install github:twbs/bootstrap

jspm install backbone

Generates SystemJS config automatically

Front Framework

Front MVP framework with

4 kinds of elements

Template management is left to the developper

We're gonna use

Let's start
our project!

Setup our project with JSPM

# One global install for the CLI
npm install -g jspm@beta
# One local install for the project
npm install --save-dev jspm@beta

Our project is initialized!

Our application

A webpage that displays random quotes

jspm install backbone jquery
{
    "quotes": [
        {
            "text": "I think the worst time to have a heart attack is during a game of charades.",
            "author": "Demitri Martin"
        }, {
            "text": "When people ask me how many people work here, I say, about a third of them.",
            "author": "Lisa Kennedy Montgomery"
        }, {
            "text": "Always and never are two words you should always remember never to use.",
            "author": "Wendell Johnson"
        }, {
            "text": "Those who believe in telekinetics, raise my hand!",
            "author": "Kurt Vonnegut"
        }, {
            "text": "Always go to other people's funerals, otherwise they won't come to yours.",
            "author": "Yogi Berra"
        }, {
            "text": "Knowledge is knowing a tomato is a fruit; wisdom is not putting it in a fruit salad.",
            "author": "Miles Kington"
        }
    ]
}

src/config.json

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

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">

    <title>Random Quotes</title>
</head>

<body>

    <div class="view-main"></div>
	


    <script src="/jspm_packages/system.js"></script>

    <script src="/jspm.browser.js"></script>
    <script src="/jspm.config.js"></script>
    <script>
        System['import']('app/app.js');
    </script>

</body>

</html>

index.html

1- Load SystemJS

2- Load our configs

3- Import our entry point

(use ['import'] for IE8 compatibility)

import Backbone from 'backbone';

export default Backbone.Model.extend({

    defaults: {
        text: '',
        author: ''
    }

});

src/quote/QuoteModel.js

import Backbone from 'backbone';

import config from 'app/config.json!';
import Quote from './QuoteModel.js';


export default Backbone.Collection.extend({

    model: Quote,

    initialize() {
        this.set(config.quotes);
    }

});

src/quote/QuoteCollection.js

jspm install json

Load JSON config file

<blockquote class="quote">
	{{ quote.text }}
	<cite class="quote-author">{{ quote.author}}</cite>
</blockquote>

<button class="js-newQuote quoteButton">Another quote!</button>

src/quote/quoteTemplate.hbs

jspm install handlebars

html, body {
  height: 100%;
}

body {
  background-color: #daf0f8;
}

.view-main {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  height: 100%;
}

.quote {
  width: 60%;
  margin: 0;
  text-align: center;
  font-family: Georgia, serif;
  font-size: 3rem;
  font-style: italic;
  line-height: 1.45;
  color: #383838;
}

@media (max-width: 1200px) {
  .quote {
    width: 90%;
  }
}

.quote-author {
  color: #999999;
  font-size: 1.5rem;
  display: block;
  margin: 1rem 0;
  text-align: center;
}

src/quote/quote.css


.quote-author:before, .quote-author:after {
  content: "\2009 \2014 \2009";
}

.quoteButton {
  position: relative;
  margin-top: 2rem;
  min-height: 3rem;
  padding: 0.4rem 2rem;
  border: 4px solid lightblue;
  background: none;
  border-radius: 20px;
  font-size: 1.5rem;
  font-family: Arial, serif;
  font-style: italic;
}

.quoteButton:hover, .quoteButton:focus {
  background-color: lightblue;
}

.quoteButton:active, .quoteButton:focus {
  outline: none;
  border: 4px solid lightblue;
}

.quoteButton:active:before, .quoteButton:focus:before {
  content: ' ';
  border-radius: 20px;
  position: absolute;
  border: 2px dashed white;
  display: block;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
}

import Backbone from 'backbone';

import './quote.css!';
import renderTemplate from './quoteTemplate.hbs!';
import QuoteCollection from './QuoteCollection.js';


export default Backbone.View.extend({

    el: '.view-main',

    events: {
        'click .js-newQuote': 'render'
    },

    initialize() {
        this.quotes = new QuoteCollection();
    },

    render() {
        let quote;
        while((quote = this.quotes.sample()) && quote === this.quote);
        this.quote = quote;

        this.$el.html(renderTemplate({ quote: quote.toJSON() }));
    }

});

src/quote/QuoteView.js

jspm install css
jspm install hbs=github:davis/plugin-hbs

1- Load CSS file

2- Load template

3- Select random quote

4- Render template

'quote/quote.css!css'

import Backbone from 'backbone';

import QuoteView from 'app/quote/QuoteView.js';

let AppRouter = Backbone.Router.extend({

    routes: {
        '': 'quote'
    },

    initialize() {
        this.quoteView = new QuoteView();
    },

    quote() {
        this.quoteView.render();
    }

});

export default new AppRouter();

src/appRouters.js

import Backbone from 'backbone';
import './appRouter.js';

Backbone.history.start();

src/app.js

Let's industrialize our project!

Installation

Very easy, we only need to install npm and JSPM modules

{
  "scripts": {
    "postinstall": "jspm install"
  }
}
npm install

package.json

Serve

What we want

  • Serve our files
  • Transpile ES2015 code
  • Auto-refresh our browser

Simple solution: live-server

live-server
npm install --save-dev live-server
{
  "scripts": {
    "start": "live-server"
  }
}
npm start

package.json

Improvements?

Build

SystemJS is loading

many files
(= far too many requests)

Two ways to bundle

Bundle

Static Build

jspm bundle entryPoint destinationFile
build

or

Group several modules into a bundle

Can be injected into browser configuration

Don't need SystemJS

Can use tree shaking

No lazy-loading

Some options

--minify

command +

--separateCSS

command +

--skip-source-maps

command +

bundle --inject

{
  "bundles": {
    "dist/app.js": [
      "npm:underscore@1.8.3/underscore",
      "github:components/jquery@2.1.4/jquery",
      "npm:process@0.10.1/browser",
      "quote/quote.css!github:systemjs/plugin-css@0.1.13",
      "github:components/handlebars.js@3.0.3/handlebars",
      "config.json!github:systemjs/plugin-json@0.1.0",
      "quote/QuoteModel",
      "npm:underscore@1.8.3",
      "github:components/jquery@2.1.4",
      "npm:process@0.10.1",
      "github:components/handlebars.js@3.0.3",
      "quote/QuoteCollection",
      "github:jspm/nodelibs-process@0.1.1/index",
      "quote/quoteTemplate.hbs!github:davis/plugin-hbs@1.0.0",
      "github:jspm/nodelibs-process@0.1.1",
      "quote/QuoteView",
      "npm:backbone@1.2.1/backbone",
      "appRouter",
      "npm:backbone@1.2.1",
      "startup"
    ]
  }
}

Tree shaking?

ollup

= Dead Code Elimination

Lazy-loading?

=> Use System.import() and create several bundles !

jspm bundle app/app.js dist/app.js

jspm bundle app/quote/QuoteView.js - app/app.js dist/quotes.js
quote() {
    let promise;
    if(this.quoteView) {
        promise = Promise.resolve(this.quoteView);
    } else {
        promise = System.import('quote/QuoteView').then(QuoteView => {
            this.quoteView = new QuoteView['default']();
            return this.quoteView;
        });
    }

    return promise.then(quoteView => quoteView.render());
}

src/appRouter.js

Our build

1.

2.

3.

4.

5.

All!

That's

And

Replace scripts in index.html

JSPM build

Gulp to inject our build

npm install --save-dev gulp gulp-html-replace  
var gulp = require('gulp');
var htmlReplace = require('gulp-html-replace');

gulp.task('html', function () {
    return gulp.src(['index.html'])
        .pipe(htmlReplace({
            js: 'app.js'     
        }))
	.pipe(gulp.dest('dist'));
});

Gulpfile.js

<!-- build:js -->
<script src="/jspm_packages/system.js"></script>
<script src="/jspm.browser.js"></script>
<script src="/jspm.config.js"></script>
<script>
    System['import']('app/app.js');
</script>
<!-- endbuild -->

index.html

Our npm script

{
  "scripts": {
    "build": "jspm build app/app.js dist/app.js --minify && gulp html"
  }
}
npm run build

package.json

Improvements

  • Minify HTML
  • Separate CSS
  • Lazy-loading

Unit tests

npm install --save-dev mocha babel-core
npm install --save-dev babel-preset-es2015
{
  "presets": ["es2015"]
}

.babelrc

npm install --save-dev karma karma-mocha \
        karma-jspm karma-chrome-launcher
module.exports = function(config) {
  config.set({
	
    proxies: {
        '/base': '/base/src',
    },
    frameworks: ['jspm', 'mocha'],
    jspm: {
        config: "src/jspm-config.js",
        packages: '/src/jspm_packages',
        serveFiles: [
            'src/**/*.js',
            'src/**/*.json',
            'src/**/*.hbs',
        ],
        loadFiles: [
            'src/**/tests/**/*.test.js'
        ]
    },

    reporters: ['progress'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false,
    concurrency: Infinity
  })
}

karma.conf.js

import QuoteCollection from 'quote/QuoteCollection';

describe('Test', function() {
    const quoteCollection = new QuoteCollection();
	describe('#get()', function() {
		it('should return 6 quotes', function() {
            expect(quoteCollection.get().length).toEqual(6);
		});
	});
});

src/quote/tests/QuoteCollection.test.js

karma start karma.conf.js

Our npm script

{
  "scripts": {
    "test:unit": "karma start karma.conf.js"
  }
}
npm run test:unit

package.json

End-to-end 
Tests

npm install --save-dev codeceptjs
npm install -g codeceptjs

CodeceptJS

codeceptjs init
Scenario('I have a button to change the quote', (I) => {
  I.amOnPage('/');
  I.waitForText('Another quote!');
});
Feature('Random quotes');
Scenario('I can change the quote', function*(I) {
  I.amOnPage('/');
  I.waitForText('Another quote!');
  const quote1 = yield I.grabTextFrom('.quote');
  I.click('Another quote!');
  const quote2 = yield I.grabTextFrom('.quote');
  assert.notEqual(quote1, quote2);
});

Scenario('I never have the same quote when changing the quote', function*(I) {	
  I.amOnPage('/');
  
  for(let i = 0; i < 30; i++) {
    I.waitForText('Another quote!');
    const quote1 = yield I.grabTextFrom('.quote');
    I.click('Another quote!');
    const quote2 = yield I.grabTextFrom('.quote');
    assert.notEqual(quote1, quote2);  
  }
});

Advantages

  • Really easy to setup
  • Interactive console
  • No need to use complex css selectors

Our npm script

{
  "scripts": {
    "test:e2e": "codeceptjs run",
    "test": "npm run test:unit && npm run test:e2e"
  }
}
npm run test

package.json

Scripts

{
  "scripts": {
    "postinstall": "jspm install",
    "start": "live-server",
    "build": "jspm build src/app.js dist/app.js --minify && gulp html",
    "test:unit": "karma run karma.conf.js",
    "test:e2e": "codeceptjs run",
    "test": "npm run test:unit && npm run test:e2e"
  }
}

package.json

Questions?

Jordane Grenat | @JoGrenat

ES2015 Stack with JSPM and Backbone

By ereold

ES2015 Stack with JSPM and Backbone

  • 2,005