Single Page Apps

a framework agnostic approach

René Viering

Frontend Developer


working for @micromata

@rvrng

http://github.com/revrng

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

Single source of truth

The state of your whole application is stored in an object tree within a single store.

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