Andrea Coiutti (@andreacoiutti)
XE One Day - Mestre, 18/11/17
browser
server sending
HTML pages
API
page?
data?
browser
server sending
HTML pages
API
HTML
JSON
browser
server sending
HTML pages
API
crawlers
page?
data?
browser
server sending
HTML pages
API
crawlers
JSON
HTML
well known and tested technologies
SEO friendly
slow render time
different programming languages / teams?
old school DOM manipulation
browser
server sending
HTML pages
API
HTML
page?
browser
server sending
HTML pages
API
JSON
data?
browser
server sending
HTML pages
API
crawlers
???
HTML
page?
quick response time
better user experience
hits the server only on first render
modern JS frameworks
(very) slow initial load
not SEO friendly
browser
server sending
HTML pages
API
JSON
data?
page?
HTML
browser
server sending
HTML pages
API
JSON
data?
browser
server sending
HTML pages
API
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
faster render of the first view
even faster on following views
=
better user experience
code shared by server and client
no duplicate logic
=
less code, less bugs
server side rendering
good for search engine crawlers
=
full SEO support
A modern client-side Javascript framework
for building Single Page Applications
Virtual DOM based
Vue + Vue-Router + Vuex + fetch (40kb)
React + React-Router + Redux + fetch (64kb)
Angular (135kb)
Mithril (8kb)
Vue (9.8ms)
React (12.1ms)
Angular (11.5ms)
Mithril (6.4ms)
first render time (less is better)
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
// use a mock DOM so we can run mithril on the server
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
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
http://localhost/#!/hello
http://localhost/hello
change it to pathname strategy
m.route.prefix('');
Mithril's router uses hashbang strategy as default
// Mithril base components
const Home = require('./pages/Home.js');
const Login = require('./pages/Login.js');
const NotFound = require('./pages/NotFound.js');
const Section = require('./pages/Section.js');
// ...
// Plain routes (without language prefix)
const plainRoutes = {
'/': Home,
'/login': Login,
'/sections/:key': Section,
// ...
'/:other': NotFound
};
module.exports = plainRoutes;
app/routes.js
encapsulate logic into units later usable as an elements
base for making large, scalable applications
POJOs
{ }
Plain Old JavaScript Objects
// define a component
const MyComponent = {
view: vnode => m('div', vnode.attrs, ['Hello ', vnode.children])
};
// consume it
m(MyComponent, { 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';
}
}
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';
}
}
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';
}
}
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';
}
}
const MainComponent = {
oninit: (vnode) => {
vnode.state.content = null;
getSomething() // async request, returns a Promise
.then((content) => {
vnode.state.content = content;
});
},
view: vnode => m(Layout, [
m(Header),
m('main', vnode.state.content || m(Loading),
m(Footer)
])
};
app/pages/MainComponent.js
m(Header)
m(Footer)
m(Layout)
m('main')
m(Loading)
const MainComponent = {
oninit: (vnode) => {
vnode.state.content = null;
getSomething() // async request, returns a Promise
.then((content) => {
vnode.state.content = content;
});
},
view: vnode => m(Layout, [
m(Header),
m('main', vnode.state.content || m(Loading),
m(Footer)
])
};
app/pages/MainComponent.js
const MainComponent = {
oninit: (vnode) => {
vnode.state.content = null;
getSomething() // async request, returns a Promise
.then((content) => {
vnode.state.content = content;
});
},
view: vnode => m(Layout, [
m(Header),
m('main', vnode.state.content || m(Loading),
m(Footer)
])
};
app/pages/MainComponent.js
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) => {
vnode.state.content = null;
getSomething() // async request, returns a Promise
.then((content) => {
vnode.state.content = content;
resolve();
});
}),
view: vnode => m(Layout, [
m(Header),
m('main', vnode.state.content || m(Loading),
m(Footer)
])
};
app/pages/MainComponent.js
const MainComponent = {
oninit: (vnode) => {
vnode.state.content = null;
getSomething() // async request, returns a Promise
.then((content) => {
vnode.state.content = content;
});
},
view: vnode => m(Layout, [
m(Header),
m('main', vnode.state.content || m(Loading),
m(Footer)
])
};
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);
});
make m.request() work server side
global.window.XMLHttpRequest = require('w3c-xmlhttprequest').XMLHttpRequest;
server.js
const state = {
sections: {
home: {
title: 'Isomorphic JS with Mithril',
slug: 'index',
content: 'Isomorphic JavaScript webapps have been around...'
},
intro: {
// ...
},
// ...
},
lang: 'en'
};
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
API
CLIENT
[POST] credentials
[HTTP 200 OK] signed JSON token
credentials
validation
[GET] resource
[HTTP 200 OK] data
token
validation
store token
JWT (JSON Web Tokens)
browser
server sending
HTML pages
API
cookies, localStorage
single request
requires changes almost everywhere in the code
resources
server init
routes
client init
views
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'
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('section', {
oncreate: () => Prism.highlightAll()
}, vnode.state.content) : m(Loading),
m(Footer)
])
};
app/pages/MainComponent.js
docs and demo:
http://isomorphic-mithril.mvlabs.it