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?
-
hot-reloader
http://jspm.io/0.17-beta-guide/hot-reloading.html
- ???
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,136