Isomorphic JS with Mithril

Andrea Coiutti (@andreacoiutti)

Universal JS Day - Ferrara, 08/04/17

Andrea Coiutti

Andrea Coiutti

ISOMORPHIC JS?

REAL CASE STUDY

browser

server sending
HTML pages

API

Conventional app

page?

data?

browser

server sending
HTML pages

API

Conventional app

HTML

JSON

browser

server sending
HTML pages

API

Conventional app

crawlers

page?

data?

browser

server sending
HTML pages

API

Conventional app

crawlers

JSON

HTML

browser

server sending
HTML pages

API

SPA

HTML

page?

browser

server sending
HTML pages

API

SPA

JSON

data?

browser

server sending
HTML pages

API

SPA

crawlers

???

HTML

page?

browser

server sending
HTML pages

API

Isomorphic app

JSON

data?

page?

HTML

browser

server sending
HTML pages

API

Isomorphic app

JSON

data?

browser

server sending
HTML pages

API

Isomorphic app

crawlers

JSON

data?

HTML

page?

routing

state persistence

view rendering

i18n

resources

time/currency format

BROWSER

SERVER

user events
handling

localStorage,
cookies

logging

static
assets

API

auth

live demo + docs

Advantages:
1. Performance

faster render of the first view
even faster on following views

=

better user experience

Advantages:
2. Maintainability

code shared by server and client
no duplicate logic

=

less code, less bugs

Advantages:
3. SEO availability

server side rendering
good for search engine crawlers

=

full SEO support

Mithril what?

mithril.js.org

 

A modern client-side Javascript framework
for building Single Page Applications

 

Virtual DOM based

Mithril

Download size

Vue + Vue-Router + Vuex + fetch (40kb)

React + React-Router + Redux + fetch (64kb)

Angular (135kb)

Mithril (8kb)

API methods

Performance

Vue (9.8ms)

React (12.1ms)

Angular (11.5ms)

Mithril (6.4ms)

first render time (less is better)

Try Mithril!

  • easy to learn
  • fun to work with
  • great documentation
  • nice community

Common

challenges

RENDERING

Risk of cloaking

Client-side rendering

m()

Mithril's hyperscript function

m('div', { class: 'foo' }, 'hello')

returns a virtual DOM node (or vnode)

JS object representing the DOM element to be created

<div class="foo">hello</div>

selector (required)

attributes object

children array

express any HTML structure using Javascript syntax

Server-side rendering

require('mithril/test-utils/browserMock')(global);
const m = require('mithril');
const render = require('mithril-node-render');

const view = m('div', { class: 'foo' }, 'hello');

render(view).then((html) => {
    // html = '<div class="foo">hello</div>'
})

render Mithril views on server side on Node.js

mithril-node-render

ROUTES

Shared routes

unique set of routes

map URI patterns to route handlers

Express and Mithril use same route definition

http://localhost/
http://localhost/:page
http://localhost/:category/:post
http://localhost/:other

Shared routes

http://localhost/#!/hello
http://localhost/hello

change it to pathname strategy

m.route.prefix('');

Mithril's router uses hashbang strategy as default

Shared routes

// Mithril base components
const Home = require('./pages/Home.js');
const NotFound = require('./pages/NotFound.js');
const Section = require('./pages/Section.js');
// ...

// Plain routes (without language prefix)
const plainRoutes = {
    '/': Home,
    '/sections/:key': Section,
    // ...
    '/:other': NotFound
};

module.exports = plainRoutes;
app/routes.js

Components

encapsulate logic into units later usable as an elements

base for making large, scalable applications

Components in Mithril

POJOs

{   }

Plain Old JavaScript Objects

Components in Mithril

// define a component
const Greeter = {
    view: vnode => m('div', vnode.attrs, ['Hello ', vnode.children])
};

// consume it
m(Greeter, { style: 'color:red;' }, 'world');
<div style="color:red;">Hello world</div>
const ComponentWithHooks = {
    oninit: (vnode) => {
        console.log('initialized');
    },
    oncreate: (vnode) => {
        console.log('DOM created');
    },
    onupdate: (vnode) => {
        console.log('DOM updated');
    },
    onbeforeremove: (vnode) => {
        console.log('exit animation can start');
        return new Promise(function(resolve) {
            // call after animation completes
            resolve();
        })
    },
    onremove: (vnode) => {
        console.log('removing DOM element');
    },
    onbeforeupdate: (vnode, old) => {
        return true;
    },
    view: (vnode) => {
        return 'hello';
    }
}

Lifecycle
methods

const ComponentWithHooks = {
    oninit: (vnode) => {
        console.log('initialized');
    },
    oncreate: (vnode) => {
        console.log('DOM created');
    },
    onupdate: (vnode) => {
        console.log('DOM updated');
    },
    onbeforeremove: (vnode) => {
        console.log('exit animation can start');
        return new Promise(function(resolve) {
            // call after animation completes
            resolve();
        })
    },
    onremove: (vnode) => {
        console.log('removing DOM element');
    },
    onbeforeupdate: (vnode, old) => {
        return true;
    },
    view: (vnode) => {
        return 'hello';
    }
}

Lifecycle
methods

const ComponentWithHooks = {
    oninit: (vnode) => {
        console.log('initialized');
    },
    oncreate: (vnode) => {
        console.log('DOM created');
    },
    onupdate: (vnode) => {
        console.log('DOM updated');
    },
    onbeforeremove: (vnode) => {
        console.log('exit animation can start');
        return new Promise(function(resolve) {
            // call after animation completes
            resolve();
        })
    },
    onremove: (vnode) => {
        console.log('removing DOM element');
    },
    onbeforeupdate: (vnode, old) => {
        return true;
    },
    view: (vnode) => {
        return 'hello';
    }
}

Lifecycle
methods

const ComponentWithHooks = {
    oninit: (vnode) => {
        console.log('initialized');
    },
    oncreate: (vnode) => {
        console.log('DOM created');
    },
    onupdate: (vnode) => {
        console.log('DOM updated');
    },
    onbeforeremove: (vnode) => {
        console.log('exit animation can start');
        return new Promise(function(resolve) {
            // call after animation completes
            resolve();
        })
    },
    onremove: (vnode) => {
        console.log('removing DOM element');
    },
    onbeforeupdate: (vnode, old) => {
        return true;
    },
    view: (vnode) => {
        return 'hello';
    }
}

Lifecycle
methods

const MainComponent = {
    oninit: (vnode) => {
        getSomething() // async request, returns a Promise
            .then((content) => {
                vnode.state.content = content;
            });
    },

    view: vnode => m(Layout, [
        m(Header),
        m('main', vnode.state.content ? vnode.state.content : m(Loading),
        m(Footer)
    ])
};

Data flow

app/pages/MainComponent.js

Data flow

m(Header)
m(Footer)
m(Layout)
m('main')
m(Loading)
const MainComponent = {
    oninit: (vnode) => {
        getSomething() // async request, returns a Promise
            .then((content) => {
                vnode.state.content = content;
            });
    },

    view: vnode => m(Layout, [
        m(Header),
        m('main', vnode.state.content ? vnode.state.content : m(Loading),
        m(Footer)
    ])
};

Data flow

app/pages/MainComponent.js
const MainComponent = {
    oninit: (vnode) => {
        getSomething() // async request, returns a Promise
            .then((content) => {
                vnode.state.content = content;
            });
    },

    view: vnode => m(Layout, [
        m(Header),
        m('main', vnode.state.content ? vnode.state.content : m(Loading),
        m(Footer)
    ])
};

Data flow

app/pages/MainComponent.js

Data flow

m(Header)
m(Footer)
m(Layout)
m('main')

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

const MainComponent = {
    oninit: (vnode) => new Promise((resolve) => {
        getSomething() // async request, returns a Promise
            .then((content) => {
                vnode.state.content = content;
                resolve();
            });
    }),

    view: vnode => m(Layout, [
        m(Header),
        m('main', vnode.state.content ? vnode.state.content : m(Loading),
        m(Footer)
    ])
};

Async components

app/pages/MainComponent.js
const MainComponent = {
    oninit: (vnode) => {
        getSomething() // async request, returns a Promise
            .then((content) => {
                vnode.state.content = content;
            });
    },

    view: vnode => m(Layout, [
        m(Header),
        m('main', vnode.state.content ? vnode.state.content : m(Loading),
        m(Footer)
    ])
};

Requests

makes XHR (aka AJAX) requests, returns a Promise

m.request()

m.request({
    method: 'PUT',
    url: '/api/v1/users/:id',
    data: {id: 1, name: 'test'}
})
.then((result) => {
    console.log(result)
})

Requests

make m.request() work server side

global.window.XMLHttpRequest = require('w3c-xmlhttprequest').XMLHttpRequest;
server.js

SHARED STATE

State manager

const state = {
    sections: {
        home: {
            title: 'Isomorphic JS with Mithril',
            slug: 'index',
            content: 'Isomorphic JavaScript webapps have been around...'
        }
    },
    lang: 'en'
};

Shared state

m('script', `window.__preloadedState = ${JSON.stringify(state)}`)

app/components/Layout.js

<script>window.__preloadedState = {"sections":{...},"lang":"en"}</script>
const sharedState = window.__preloadedState || {};

app/index.js

AUTH

API

CLIENT

[POST] credentials

[HTTP 200 OK] signed JSON token

credentials
validation

[GET] resource

[HTTP 200 OK] data

token
validation

 

 

store token

Token based auth

JWT (JSON Web Tokens)

browser

server sending
HTML pages

API

Isomorphic app

cookies, localStorage

single request

MULTILANGUAGE

Multilanguage

requires changes almost everywhere in the code

resources

server init

routes

client init

views

i18n

Micro library for translations
support for placeholders and multiple plural forms
plays well with VDOM-libraries such as Mithril

translate.js
const translate = require('translate.js');

const messages = {
    like: 'I like this.',
    likeThing: 'I like {thing}!',
    likeTwoThings: 'I like {0} and {1}!',
    simpleCounter: 'The count is {n}.',
    hits: {
        0: 'No Hits',
        1: '{n} Hit',
        n: '{n} Hits', // default
    }
};

const t = translate(messages);


// Simple
t('like') => 'I like this.'
t('Prosa Key') => 'This is prosa!'

// Placeholders - named
t('likeThing', {thing: 'the Sun'}) => 'I like the Sun!'
//placeholders - array
t('likeTwoThings', ['Alice', 'Bob']) => 'I like Alice and Bob!'

// Numerical subkeys (count)
t('simpleCounter', 25) => 'The count is 25'
t('hits', 0) => 'No Hits'
t('hits', 99) => '99 Hits'

Integrations

easy integration of third party libraries
on the client side

const Prism = process.browser ? require('prismjs') : null;

const MainComponent = {
    oninit: (vnode) => /*...*/,

    view: vnode => m(Layout, [
        m(Header),
        vnode.state.content ? m('div', {
            oncreate: () => Prism.highlightAll()
        }, vnode.state.content) : m(Loading),
        m(Footer)
    ])
};
app/pages/MainComponent.js

THANK YOU!

Isomorphic JS with Mithril

By Andrea Coiutti

Isomorphic JS with Mithril

A showcase of the challenges that we faced implementing an isomorphic web application. The aim is to present a real case study where we chose Mithril for its speed, simplicity and elegance. We will show the solutions we used to solve the common problems for this kind of app.

  • 2,017