Single Page Apps
a framework agnostic approach
Why Single Page Apps
without frameworks?
BATMAN.JS
How to structure
a Frontend Application?
Libraries/Frameworks are taught
never best practices
What makes a
Single Page App?
Once upon a time...
Multi Page Web Application
aka the »traditional website«
Server
https://www.awesome-site.io
request page
Server
https://www.awesome-site.io
request page
respond with Markup / CSS / JS / ...
Server
https://www.awesome-site.io/navigate/to/other/url
request another url
Server
https://www.awesome-site.io/navigate/to/other/url
request another url
respond with Markup / CSS / JS / ...
Single Page Application
aka »Web-App«
Server
https://www.awesome-site.io
request page
Server
https://www.awesome-site.io
respond with Markup / CSS / JS / ...
request page
Server
https://www.awesome-site.io/#/navigate/to/other/url
request data
Server
https://www.awesome-site.io/#/navigate/to/other/url
respond with JSON
request data
The role of the server
-
Authentication
-
Loading the initial page (resources)
-
Providing data
The main parts
of a »Single Page App«
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
<!-- entry point -->
<div id="app"/>
<script src="/path/to/bundle.js"></script>
</body>
</html>
Routing
Application Logic (Structure)
Rendering
Data Layer
Asynchronicity
Tooling
Routing
entry point
entry point
entry point
entry point
https://www.awesome-site.io/#/navigate/to/other/url
#
window.addEventListener('hashchange', () => {
// route changed, handle it!
});
#
HTML5 History API (without #)
window.onpopstate = function(event) {
// route changed, handle it!
};
history.pushState({page: 1}, "title 1", "?page=1");
history.pushState({page: 2}, "title 2", "?page=2");
history.replaceState({page: 3}, "title 3", "?page=3");
Application Logic
(Structure)
MVC
HTML
function() {}
function() {}
function() {}
JSON
MVVM
One-Way DataBinding
Model => View
Two-Way DataBinding
Model <=> View
JSON
HTML
function() {}
JSON
function() {}
MVW (Whatever)
explicit one-way data-flow
reason about your code
Tell don't ask
const result = askForData();
this.state = result.X
FireAndForget();
FLUX
- intention to do something
- simple description (string)
- optional: payload
- receives actions
- dispatches actions to stores
- single source of truth
- updates state when action appears
- fires event when state changes
- triggers actions
- acts on change event from store
REDUX
»Redux is a predictable state container for JavaScript apps.«
ACTION
STORE
Reducers
ACTION
STORE
Reducers
State is read-only
The only way to mutate state is to emit an action, an object describing what happened
ACTION
STORE
Reducers
ACTION
STORE
Reducers
Changes are made with pure functions
To specify how the state tree is transformed by actions, you write pure reducers
f(initialState, action) => newState
UCER FL
RED
UX
RED
UX
const todos = (state = [], action) => {
switch(action.type) {
case 'ADD_TODO':
return [
...state,
{
id: state.length + 1,
text: action.text,
completed: false
}
];
default:
return state;
}
};
const todoApp = combineReducers({todos});
const store = createStore(todoApp);
store.subscribe(() => {
console.log(store.getState());
});
store.dispatch({
type: 'ADD_TODO',
text: 'Mett kaufen'
});
store.dispatch({
type: 'ADD_TODO',
text: 'Mate trinken'
});
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({}); // initial state
return { getState, dispatch, subscribe};
};
const combineReducers = (reducers) => {
return (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
}, {});
};
};
a single state tree
makes things simple...
- Testability
- Debugging
- Storage
REACTIVE
»Reactive programming is programming with asynchronous data streams.«
With asynchronous ..... ?
»Think of it as an immutable array.«
[ 14, 9, 5, 2, 10, 13, 4 ]
[ 14, 9, 5, 2, 10, 13, 4 ]
.filter(x => x % 2 === 0)
[ 14, 2, 10, 4 ]
[ 1, 2, 3 ]
.map(x => x * 2)
[ 2, 4, 6]
a stream of click events
aka »Observable«
.filter(event => event.x < 250)
»Rx is the underscore.js for events.«
RX Example
(detect the number of button clicks)
// Make the raw clicks stream
var button = document.querySelector('.this');
var clickStream = Rx.Observable.fromEvent(button, 'click');
// HERE
// The 4 lines of code that make the multi-click logic
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
// Same as above, but detects single clicks
var singleClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x === 1; });
// Listen to both streams and render the text label accordingly
singleClickStream.subscribe(function (event) {
document.querySelector('h2').textContent = 'click';
});
multiClickStream.subscribe(function (numclicks) {
document.querySelector('h2').textContent = ''+numclicks+'x click';
});
Rx.Observable.merge(singleClickStream, multiClickStream)
.throttle(1000)
.subscribe(function (suggestion) {
document.querySelector('h2').textContent = '';
});
Rendering
<!DOCTYPE html>
<html>
<head>
...
</head>
<body>
...
<!-- entry point -->
<div id="app"/>
<script src="/path/to/bundle.js"></script>
</body>
</html>
f(data) => view
data changing over time...
»Change and its detection in JavaScript frameworks«
Projecting Data
f(json) => view
Manual Re-Rendering
»I have no idea what I should re-render. You figure it out.«
Data-Binding
»I know exactly what changed and what should be re-rendered because I control your models and views.«
Two-Way Data-Binding
aka Dirty Checking
»I have no idea what changed, so I'll just check everything that may need updating.«
Virtual DOM
»I have no idea what changed so I'll just re-render everything and see what's different now.«
Data Layer
Server
Data-Layer
HTTP
JavaScript
var request = new XMLHttpRequest();
request.open('GET', '/url/to/get/json', true);
request.onload = function() {
if (request.status >= 200 && request.status < 400) {
var data = JSON.parse(request.responseText);
console.log(data);
}
};
request.send();
jQuery
$.ajax({
url: this.props.url,
dataType: 'json',
cache: false,
success: function(data) {
console.log(data);
});
Fetch API
fetch('https://api/url/to/get/json').then(function(response) {
return response.json();
}).then(function(jsonResponse) {
console.log(jsonResponse);
});
There are other data sources...
-
Local Storage
-
WebSockets
Asynchronicity
It's the nature of the web
Callbacks
asyncFunction(function (response) {
// do something
});
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
asyncFunction(function (response) {
// do something
});
});
});
});
});
});
});
});
});
});
Pyramid of doom
Promises
const promise = new Promise((resolve, reject) => {
if (/* success */) {
resolve("success");
}
else {
reject(Error("error"));
}
});
promise.then((result) => {
console.log(result); // "success"
}, (err) {
console.log(err); // Error: "error"
});
Observables (Rx.js)
var clickStream = Rx.Observable.fromEvent(button, 'click');
// HERE
// The 4 lines of code that make the multi-click logic
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
Let's code your own SPA
from scratch
Routing
Application Logic (Structure)
Rendering
Data Layer
Asynchronicity
Tooling
Data Layer
MVVM
Two-Way Data-Binding
#
Promises
Fetch API
Conclusion
BATMAN.JS
There are so much possibilities!
Focus on your product first!
Then use the simplest technology!
Resilience - Jeremy Keith @adactio (Beyond Tellerrand 2016)
»Simplicity is prerequisite for reliablity«
Edsger W. Dijkstra
Learn JavaScript
The end
Single Page Apps
By René Viering
Single Page Apps
without frameworks
- 1,820