Client rendering

Роман Парадеев

Awesome notes на Redux

Server rendering

На сервере данные подставляются в шаблоны, на клиент приходит готовая разметка.

Client rendering

С сервера приходят данные и шаблоны, разметка генерируется на клиенте.

Зачем?

Сеть – медленная

Handlebars в браузере

bundles/note/note.hbs


<body>
    {{>header}}
    <ul>
    {{#each notes}}
        <li><a>{{name}}</a></li>
    {{/each}}
    </ul>
    {{>footer}}
</body>
    

bundles/note/note.hbs


<body>
    <div id="root"></div>
</body>
    

bundles/note/note.js


var template = ...; // Шаблон
var data = ...;     // Данные

var rootEl = document.getElementById('root');

rootEl.innerHTML = template(data);
    

bundles/note/note.hbs


<script src="/handlebars.js"></script>

<script id="template" type="text/x-handlebars-template">
    {{>header}}
    <ul>
        {{#each notes}}
            <li><a>{{name}}</a></li>
        {{/each}}
    </ul>
    {{>footer}}
</script>

<body>
    <div id="root"></div>
</body>
    

bundles/note/note.hbs


<script id="template" type="text/x-handlebars-template">
    ...
</script>

<body>
    <div id="root"></div>
</body>
    

bundles/note/note.js


var rootEl = document.getElementById('root');
var source = document.getElementById('entry-template');
var template = Handlebars.compile(source);

rootEl.innerHTML = template(data);
    

bundles/note/note.hbs


<body>
    <div id="root"></div>
</body>
    

blocks/note/note.hbs


{{>header}}

{{>footer}}
    

bundles/note/note.hbs


<body>
    <div id="root"></div>
</body>
    

blocks/note/note.hbs


...
    

bundles/note/note.js


const rootEl = document.getElementById('root');
const template = require('./blocks/note/note.hbs');

rootEl.innerHTML = template(data);
    

npm install --save-dev handlebars handlebars-loader
    

webpack.config.js


module: {
    loaders: [
        ...,
        {
            test: /\.hbs$/,
            loader: "handlebars-loader"
        }
    ]
}
    

<ul class="note__navigation">
    {{#each notes}}
        <li>
            <a href="#">{{ name }}</a>
        </li>
    {{/each}}
</ul>

{{#if selectedNote}}
<div class="note">
    <div class="note__name">{{ selectedNote.name }}</div>
    <div class="note__text">{{ selectedNote.text }}</div>
</div>
{{/if}}
    

function render(notes, selectedNote) {
    var template = require('./blocks/note/note.hbs');
    var rootEl = document.getDocumentById('root');

    rootEl.innerHTML = template({
        notes: notes,
        selectedNote: selectedNote
    });
}
    

function render(notes, selectedNote) {
    ...
}

render(notes, null);
document.addEventListner('click', function(event) {
if (event.target.tagName === 'a') { event.preventDefault();
var selectedNoteName = event.target.textContent; var selectedNote = _.findWhere(notes, { name: selectedNoteName });
render(notes, selectedNote);
}
});

REST на сервере


app.get('/api/notes', function (req, res) {
    var Note = require('./models/note');
    var notes = Note.findAll();

    res.json({ notes: notes });
});
    

fetch в браузере


fetch('/api/notes')
    .then(function(response) {
        return response.json();
    })
    .then(function(json) {
        render(json.notes, null); 
    });
    

.
├── blocks
├── bundles
├── controllers
├── models
└── router.js
                
=>

.
├── server
│   ├── bundles
│   ├── controllers
│   ├── models
│   └── router.js
└── client
    └── blocks
                

Демо


ಠ_ಠ


Вопросы

Ускорили?

Пустой экран

до тех пор, пока:

  • загружаются все ресурсы
  • парсится и выполняется js
  • выполняется запрос за данными
  • генерируется и вставляется html

Universal JavaScript

Первый запрос обрабатываем на сервере, последующие – в браузере.

Но – сложно :(

Заглушка!


<body>
    <div id="root">
        Пожалуйста, подождите
        [===== 69% ====>    ] 
    </div>
</body>
    

И ещё кое-что...

Paint – медленный


render(notes, null);

document.addEventListner('click', function(event) {
    if (event.target.tagName === 'a') {
        event.preventDefault();

        var selectedNoteName = event.target.textContent;
        var selectedNote = _.findWhere(notes, {
            name: selectedNoteName
        });

        render(notes, selectedNote);
    }
});
    

Handlebars


 
render(notes, selectedNote);
 
    

Вручную


var noteName = document.querySelector('.note__name');
var noteText = document.querySelector('.note__text');

noteName.textContent = selectedNote.name;
noteText.textContent = selectedNote.text;
    

<div class="note">
    <div class="note__name">Films</div>
    <div class="note__text">Films to Watch</div>
</div>
    

<div class="note">
    <div class="note__name">Books</div>
    <div class="note__text">Books To Read</div>
</div>
    

Алгоритм render: naive


// псевдокод

var oldElem = document.querySelector('.note');
var newElem = document.createElement('div');

newElem.insertBefore(oldElem);
oldElem.parentNode.removeChild(node);
    

<div class="note">
    <div class="note__name">Films</div>
    <div class="note__text">Films to Watch</div>
</div>
    

<div class="note">
    <div class="note__name">Books</div>
    <div class="note__text">Books To Read</div>
</div>
    

Алгоритм render: diff


// псевдокод

document.querySelector('.note_name')
        .textContent('Books');

document.querySelector('.note__text')
        .textContent('Books To Read');
    

ES2015

Constants


var TEMPLATE_NAME = './blocks/note.hbs';
    

const templateName = './blocks/note.hbs';
    

Arrow functions


var onClick = function (event) {
    render(notes, selectedNote);
};
    

const onClick = (event) => {
    render(notes, selectedNote);
}  
    

Enchanced object literals


var state = {
    notes: notes,
    selectedNoteName: selectedNoteName
};
    

const state = {
    notes,
    selectedNoteName
};
    

Destructuring


var state = {
    notes: [],
    selectedNoteName: 'Book'
};
var notes = state.notes;
var selectedNoteName = state.selectedNoteName;
    

const state = {
    notes: [],
    selectedNoteName: 'Book'
};
const {note, selectedNoteName} = state;
    

Default function parameters


function notesApp(state, action) {
    state = state || {};
    ...
}
    

const notesApp = (state = {}, action) => {
    ...
};
    

Spread operator


var numbers = [1, 2, 3];
var maxNumber = Math.max.apply(null, numbers);
    

const numbers = [1, 2, 3];
const maxNumber = Math.max(...numbers);
    

Modules


var _ = require('underscore');

module.exports = function () {
    ...
};
    

import _ from 'underscore';

export default () => {
    ...
};
    

React

JSX


const HelloMessage = ({name}) => (
    <div>Hello {name}</div>
);

ReactDOM.render(
    <HelloMessage name="John" />,
    document.getElementById('root')
);
    

JSX + ES2015 -> ECMAScript 5

Babel


npm install --save-dev \
    babel-core \
    babel-loader \
    babel-preset-react \
    babel-preset-es2015
    

.babelrc


{
  "presets": ["es2015", "react"]
}
    

webpack.config.js


loaders: [
  {
    test: /\.js$/,
    loader: 'babel',
    exclude: /node_modules/
  }
]
    

blocks/header.js


import React from 'react';

export default () => (
    <header></header>
);
    

blocks/footer.js


import React from 'react';

export default () => (
    <footer>© 2016</footer>
);
    

blocks/navigation.js


import React from 'react';

const onClick = (event) => { ... };

export default ({notes}) => (
    <ul>
        {notes.map(note => (
            <li>
                <a href="#" onClick={onClick}>
                    {note.name}
                </a>
            </li>
        ))}
    </ul>
);
    

blocks/note.js


import React from 'react';
import Header from './blocks/header';
import Footer from './blocks/footer';
import Navigaion from './blocks/navigation';
import SelectedNote from './blocks/selectedNote';

export default ({notes, selectedNote}) => (
    <div>
        <Header />
        <Navigation notes={notes} />
        <SelectedNote {...selectedNote} />
        <Footer />
    </div>
);
    

ಠ_ಠ

Архитектура

Состояние приложения

  • ответы сервера и закэшированные данные
  • данные, созданные локально (в том числе не сохранённые на сервер)
  • состояние интерфейса: индикатор загрузки, открытая вкладка и т. д.

1. Единый источник истины

Состояние всего приложения хранится в виде дерева в единственном хранилище.

State


{
    selectedNoteName: 'Films',
    notes: [
        {
            name: 'Films',
            text: 'Films to watch'
        },
        {
            name: 'Books',
            text: 'Books to read'
        }
    ]
}
    

2. Состояние – read-only

Чтобы изменить состояние, нужно вызвать действие.

Actions


{
    type: 'ADD_NOTE',
    note: { text: 'Купи слона' }
}

{
    type: 'SELECT_NOTE',
    selectedNoteName: 'book'
}
    

3. sn = f(sn-1, a)

Преобразования выполняются чистыми функциями.

Чистая функция

  • детерминирована
  • не обладает побочными эффектами

Не являются таковыми


function getRandomArbitrary(min, max) {
    return Math.random() * (max - min) + min;
}

function sayHello(name) {
    console.log('Hello ' + name);
}

function incrementCounter() {
    window.counter += 1;
}
        

function addNote(notes, note) {
    notes.push(note);
    return notes;
}
    

v


function addNote(notes, note) {
    const notesCopy = notes.slice();
    notesCopy.push(number);
    return notesCopy;
}
    

function addNote(notes, note) {
    notes.push(note);
    return notes;
}
    

v


function addNote(notes, note) {
    const notesCopy = notes.concat([note]);
    return notesCopy;
}
    

function addNote(notes, note) {
    notes.push(note);
    return notes;
}
    

v


function addNote(notes, note) {
    const notesCopy = [...notes, note];
    return notesCopy;
}
    

function selectNote(state, selectedNoteName) {
    state.selectedNoteName = selectedNoteName;
    return state;
}
    

v


function selectNote(state, selectedNoteName) {
    const stateCopy = {
        notes: state.notes,
        selectedNoteName: selectedNoteName
    };
    return stateCopy;
}
    

function selectNote(state, selectedNoteName) {
    state.selectedNoteName = selectedNoteName;
    return state;
}
    

v


function selectNote(state, selectedNoteName) {
    const stateCopy = Object.assign({}, state, {
        selectedNoteName: selectedNoteName
    };
    return stateCopy;
}
    

Reducer


function noteApp(state, action) {
    switch (action.type) {
        case 'ADD_NOTE':
            return ...
        case 'SELECT_NOTE':
            return ...
        default:
            return state;
    }
};
    

Reducer


function noteApp(state, action) {
    switch (action.type) {
        case 'ADD_NOTE':
            return Object.assign({}, state, {
                notes: addNote(state.notes, action.note);
            });
        case 'SELECT_NOTE':
            return selectNote(state,
                              action.selectedNoteName);
        default:
            return state;
    }
};
    

Хранилище (Store)


import {createStore} from 'redux';
import {noteApp} from './reducers';

const store = createStore(noteApp);

// никогда так не делайте,
// используйте Provider
window.store = store;
    

Хранилище – Event Emitter


store.subscribe(function () {
   const state = store.getState(); 
});

store.dispatch({
    type: 'ADD_NOTE',
    note: {}
});
    

bundles/note/note.js


const store = createStore(noteApp);

function render() {
    const state = store.getState();

    ReactDom.render(
        <Note state={state} />,
        document.getElementById('root')
    );
}

render();
store.subscribe(render);
    

bundles/note/note.js


fetch('/api/notes')
    .then(response => response.json())
    .then(json => {
        json.notes.forEach(note => {
            store.dispatch({
                type: 'ADD_NOTE',
                note: note
            });
        });
    });
    

blocks/navigation.js


import React from 'react';

const onClick = (event) => { ... };

export default ({notes}) => (
    <ul>
        {notes.map(note => (
            <li>
                <a href="#" onClick={onClick}>
                    {note.name}
                </a>
            </li>
        ))}
    </ul>
);
    

blocks/navigation.js


...

const onClick = (event) => {
    const selectedNoteName = event.target.textContent;

    store.dispatch({
        type: 'SELECT_NOTE',
        selectedNoteName: selectedNoteName
    });
};

...
    

ಠ_ಠ

Ресурсы