Роман Парадеев
Server rendering
На сервере данные подставляются в шаблоны, на клиент приходит готовая разметка.
Client rendering
С сервера приходят данные и шаблоны, разметка генерируется на клиенте.
<body>
{{>header}}
<ul>
{{#each notes}}
<li><a>{{name}}</a></li>
{{/each}}
</ul>
{{>footer}}
</body>
<body>
<div id="root"></div>
</body>
var template = ...; // Шаблон
var data = ...; // Данные
var rootEl = document.getElementById('root');
rootEl.innerHTML = template(data);
<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>
<script id="template" type="text/x-handlebars-template">
...
</script>
<body>
<div id="root"></div>
</body>
var rootEl = document.getElementById('root');
var source = document.getElementById('entry-template');
var template = Handlebars.compile(source);
rootEl.innerHTML = template(data);
<body>
<div id="root"></div>
</body>
{{>header}}
{{#each notes}}
- {{ name }}
{{/each}}
{{>footer}}
<body>
<div id="root"></div>
</body>
...
const rootEl = document.getElementById('root');
const template = require('./blocks/note/note.hbs');
rootEl.innerHTML = template(data);
npm install --save-dev handlebars handlebars-loader
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);
});
|
=> |
|
Демо
Вопросы
до тех пор, пока:
Первый запрос обрабатываем на сервере, последующие – в браузере.
Но – сложно :(
<body>
<div id="root">
Пожалуйста, подождите
[===== 69% ====> ]
</div>
</body>
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);
}
});
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>
// псевдокод
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>
// псевдокод
document.querySelector('.note_name')
.textContent('Books');
document.querySelector('.note__text')
.textContent('Books To Read');
var TEMPLATE_NAME = './blocks/note.hbs';
const templateName = './blocks/note.hbs';
var onClick = function (event) {
render(notes, selectedNote);
};
const onClick = (event) => {
render(notes, selectedNote);
}
var state = {
notes: notes,
selectedNoteName: selectedNoteName
};
const state = {
notes,
selectedNoteName
};
var state = {
notes: [],
selectedNoteName: 'Book'
};
var notes = state.notes;
var selectedNoteName = state.selectedNoteName;
const state = {
notes: [],
selectedNoteName: 'Book'
};
const {note, selectedNoteName} = state;
function notesApp(state, action) {
state = state || {};
...
}
const notesApp = (state = {}, action) => {
...
};
var numbers = [1, 2, 3];
var maxNumber = Math.max.apply(null, numbers);
const numbers = [1, 2, 3];
const maxNumber = Math.max(...numbers);
var _ = require('underscore');
module.exports = function () {
...
};
import _ from 'underscore';
export default () => {
...
};
const HelloMessage = ({name}) => (
<div>Hello {name}</div>
);
ReactDOM.render(
<HelloMessage name="John" />,
document.getElementById('root')
);
npm install --save-dev \
babel-core \
babel-loader \
babel-preset-react \
babel-preset-es2015
{
"presets": ["es2015", "react"]
}
loaders: [
{
test: /\.js$/,
loader: 'babel',
exclude: /node_modules/
}
]
import React from 'react';
export default () => (
<header></header>
);
import React from 'react';
export default () => (
<footer>© 2016</footer>
);
import React from 'react';
const onClick = (event) => { ... };
export default ({notes}) => (
<ul>
{notes.map(note => (
<li>
<a href="#" onClick={onClick}>
{note.name}
</a>
</li>
))}
</ul>
);
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>
);
Состояние всего приложения хранится в виде дерева в единственном хранилище.
{
selectedNoteName: 'Films',
notes: [
{
name: 'Films',
text: 'Films to watch'
},
{
name: 'Books',
text: 'Books to read'
}
]
}
Чтобы изменить состояние, нужно вызвать действие.
{
type: 'ADD_NOTE',
note: { text: 'Купи слона' }
}
{
type: 'SELECT_NOTE',
selectedNoteName: 'book'
}
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;
}
function noteApp(state, action) {
switch (action.type) {
case 'ADD_NOTE':
return ...
case 'SELECT_NOTE':
return ...
default:
return state;
}
};
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;
}
};
import {createStore} from 'redux';
import {noteApp} from './reducers';
const store = createStore(noteApp);
// никогда так не делайте,
// используйте Provider
window.store = store;
store.subscribe(function () {
const state = store.getState();
});
store.dispatch({
type: 'ADD_NOTE',
note: {}
});
const store = createStore(noteApp);
function render() {
const state = store.getState();
ReactDom.render(
<Note state={state} />,
document.getElementById('root')
);
}
render();
store.subscribe(render);
fetch('/api/notes')
.then(response => response.json())
.then(json => {
json.notes.forEach(note => {
store.dispatch({
type: 'ADD_NOTE',
note: note
});
});
});
import React from 'react';
const onClick = (event) => { ... };
export default ({notes}) => (
<ul>
{notes.map(note => (
<li>
<a href="#" onClick={onClick}>
{note.name}
</a>
</li>
))}
</ul>
);
...
const onClick = (event) => {
const selectedNoteName = event.target.textContent;
store.dispatch({
type: 'SELECT_NOTE',
selectedNoteName: selectedNoteName
});
};
...