Node.js server side
rendering
Kamil Płaczek
Jestem Kamil
JavaScript Developer
Server-side rendering
• Przeglądarka wykonuje zapytanie, serwer zwraca
przygotowany HTML
Server-side rendering
• W kontekście SPA - uruchomienie kodu aplikacji i renderera
po stronie serwera (kod uniwersalny)
+
src/client/index.js
import React from 'react';
import ReactDOM from ‘react-dom';
import {BrowserRouter} from 'react-router-dom';
import App from './app/app.component';
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
);
początkowy kod - uruchomienie spa
src/server/index.js
import express from 'express';
const app = express();
app.use(express.static('dist'));
app.get('*', (req, res) => {
// doServerSideRenderingPls();
});
app.listen(3000);
początkowy kod - serwer
Problem 1: Routing
• Jak przełożyć routing z przeglądarki na serwer? 🤷
export default class App extends Component {
render() {
return (
<div className="container">
...
<Route exact path="/" component={Home} />
<Route path="/contact" component={Contact} />
</div>
</div>
);
}
}
Problem 1: Routing
• Routing jest uniwersalny.
export default class App extends Component {
render() {
return (
<div className="container">
...
<Route exact path="/" component={Home} />
<Route path="/contact" component={Contact} />
</div>
</div>
);
}
}
src/server/index.js
...
app.get('*', (req, res) => {
const context = {};
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
// redirect was done in client-side routing
res.redirect(301, context.url);
} else {
res.send(`
<!DOCTYPE html>
. . .
<body>
<div id="root">${appString}</div>
<script type="text/javascript" src="client.js"></script>
</body>
</html>
`);
}
});
...
server side rendering z routerem
src/server/index.js
...
app.get('*', (req, res) => {
const context = {};
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
// redirect was done in client-side routing
res.redirect(301, context.url);
} else {
res.send(`
<!DOCTYPE html>
. . .
<body>
<div id="root">${appString}</div>
<script type="text/javascript" src="client.js"></script>
</body>
</html>
`);
}
});
...
<div class="container" data-reactroot="">
<div>
<div class="navbar"><span class="brand">Taylor Swift</span>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<hr/>
<div>
<p>
Taylor Alison Swift (born December 13, 1989) is an
…
</p>
<ul></ul>
</div>
</div>
</div>
server side rendering z routerem
src/server/index.js
...
app.get('*', (req, res) => {
const context = {};
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
// redirect was done in client-side routing
res.redirect(301, context.url);
} else {
res.send(`
<!DOCTYPE html>
. . .
<body>
<div id="root">${appString}</div>
<script type="text/javascript" src="client.js"></script>
</body>
</html>
`);
}
});
...
server side rendering z routerem
<div class="container" data-reactroot="">
<div>
<div class="navbar"><span class="brand">Taylor Swift</span>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<hr/>
<div>
<p>
Taylor Alison Swift (born December 13, 1989) is an
…
</p>
<ul></ul>
</div>
</div>
</div>
src/server/index.js
...
app.get('*', (req, res) => {
const context = {};
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
if (context.url) {
// redirect was done in client-side routing
res.redirect(301, context.url);
} else {
res.send(`
<!DOCTYPE html>
. . .
<body>
<div id="root">${appString}</div>
<script type="text/javascript" src="client.js"></script>
</body>
</html>
`);
}
});
...
server side rendering z routerem
<div class="container" data-reactroot="">
<div>
<div class="navbar"><span class="brand">Taylor Swift</span>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</div>
<hr/>
<div>
<p>
Taylor Alison Swift (born December 13, 1989) is an
…
</p>
<ul></ul>
</div>
</div>
</div>
Problem 2: Dane
• Jak wypełnić dokument danymi podczas SSR? 🌅
async loadData() {
const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', {
headers: {Authorization: ‘Client-ID shAK3-it-0ff’},
});
...
this.setState({pics});
}
Problem 2: Dane
Server-side render
and return HTML
Fetch JS app code Run app client-side loadData()
Problem 2: Dane
Server-side render
and return HTML
Fetch JS app
code
Run app client-
side
loadData()loadData()
src/server/index.js
...
const getTaytayPics = async () => {
const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', {
headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘},
});
. . .
return pics;
};
...
app.get('*', async (req, res) => {
const context = {};
if (req.url === '/') {
context.pics = await getTaytayPics();
}
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
...
server side rendering + pobieranie danych v1
src/server/index.js
...
const getTaytayPics = async () => {
const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', {
headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘},
});
. . .
return pics;
};
...
app.get('*', async (req, res) => {
const context = {};
if (req.url === '/') {
context.pics = await getTaytayPics();
}
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
...
server side rendering + pobieranie danych v1
src/client/app/home/home.component.js
export class Home extends Component {
. . .
componentWillMount() {
if (this.props.staticContext && this.props.staticContext.pics) {
this.setState({pics: this.props.staticContext.pics})
} else {
this.loadData();
}
}
komponent react - wykorzystanie danych podczas ssr
Problem 2: Dane
Server-side render
and return HTML
Fetch JS app
code
Run app client-
side
loadData()loadData()
src/server/index.js
app.get('*', async (req, res) => {
. . .
if (req.url === '/') {
context.pics = await getTaytayPics();
}
. . .
} else {
res.send(`<!DOCTYPE html>
. . .>
<script>
window.APP_STATE = ${JSON.stringify({pics: context.pics})};
</script>
<script type="text/javascript" src="client.js"></script>
</body>
</html>`);
server side rendering - przekazanie informacji
src/client/app/home/home.component.js
componentWillMount() {
if (this.props.staticContext && this.props.staticContext.pics) {
this.setState({pics: this.props.staticContext.pics});
} else if (window && window.APP_STATE && window.APP_STATE.pics) {
this.setState({pics: window.APP_STATE.pics})
}
else {
this.loadData();
}
}
komponent react - wykorzystanie danych z ssr
src/server/index.js
...
const getTaytayPics = async () => {
const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', {
headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘},
});
. . .
return pics;
};
...
app.get('*', async (req, res) => {
const context = {};
if (req.url === '/') {
context.pics = await getTaytayPics();
}
const appString = renderToString(
<StaticRouter location={req.url} context={context}>
<App />
</StaticRouter>
);
...
DRY?
src/client/app/app.routes.js
import {Home} from './home/home.component';
import {Contact} from './contact/contact.component';
export const routes = [
{
component: Home,
path: '/',
exact: true,
},
{
component: Contact,
path: '/contact',
},
];
refactoring routingu
src/client/app/home/home.component.js
import fetch from ‘isomorphic-fetch';
. . .
static async loadData() {
const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', {
headers: {Authorization: 'Client-ID w1ld3est-dr3am5‘},
});
return pics;
}
. . .
}
refactoring komponentu react
src/server/index.js
import {StaticRouter, matchPath} from 'react-router';
import {routes} from '../client/app/app.routes';
. . .
app.get('*', async (req, res) => {
. . .
const matchedRoute = routes.find(route => matchPath(req.path, route));
if (matchedRoute) {
if (matchedRoute.component && matchedRoute.component.loadData) {
context.data = await matchedRoute.component.loadData();
}
} else {
return res.sendStatus(404);
}
const appString = renderToString(
. . .
refactoring pobierania danych na serwerze
Problem 2.5: Dane
• Jak wypełnić danymi store?
src/client/app/redux/taytay/taytay.actions.js
import fetch from 'isomorphic-fetch';
export const fetchPics = () => async dispatch => {
const res = await fetch('https://api.imgur.com/3/gallery/r/
taylorswift', {
headers: {Authorization: 'Client-ID 0447601918a7bb5'},
});
. . .
return dispatch(setPics(pics));
};
export const setPics = pics => ({. . .});
przeniesienie pobierania danych do akcji
src/client/app/home/home.component.js
import {fetchPics} from '../redux/taytay/taytay.actions';
export class Home extends Component {
static loadData = store => {
return store.dispatch(fetchPics());
};
. . .
wykorzystanie akcji w komponencie
src/client/index.js
import {createAppStore} from './create-store';
const store = createAppStore(window.APP_STATE || {});
ReactDOM.hydrate(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById('root')
);
stworzenie store + odtworzenie stanu z ssr
src/server/index.js
import {createAppStore} from '../client/create-store';
. . .
app.get('*', async (req, res) => {
const store = createAppStore({});
const matchedRoute = routes.find(route => matchPath(req.path, route));
if (matchedRoute) {
if (matchedRoute.component && matchedRoute.component.loadData) {
await matchedRoute.component.loadData(store);
}
. . .
} else {
const state = store.getState();
const html = `<!DOCTYPE html>
. . .
<div id="root">${appString}</div>
<script>
window.APP_STATE = ${JSON.stringify(state)};
</script>
. . .
}
});
stworzenie store + inicjalizacja stanu podczas ssr
src/server/index.js
import {createAppStore} from '../client/create-store';
. . .
app.get('*', async (req, res) => {
const store = createAppStore({});
const matchedRoute = routes.find(route => matchPath(req.path, route));
if (matchedRoute) {
if (matchedRoute.component && matchedRoute.component.loadData) {
await matchedRoute.component.loadData(store);
}
. . .
} else {
const state = store.getState();
const html = `<!DOCTYPE html>
. . .
<div id="root">${appString}</div>
<script>
window.APP_STATE = ${JSON.stringify(state)};
</script>
. . .
}
});
stworzenie store + inicjalizacja stanu podczas ssr
src/server/index.js
import {createAppStore} from '../client/create-store';
. . .
app.get('*', async (req, res) => {
const store = createAppStore({});
const matchedRoute = routes.find(route => matchPath(req.path, route));
if (matchedRoute) {
if (matchedRoute.component && matchedRoute.component.loadData) {
await matchedRoute.component.loadData(store);
}
. . .
} else {
const state = store.getState();
const html = `<!DOCTYPE html>
. . .
<div id="root">${appString}</div>
<script>
window.APP_STATE = ${JSON.stringify(state)};
</script>
. . .
}
});
stworzenie store + inicjalizacja stanu podczas ssr
Problem 3: Wydajność
• Jak szybko będzie działał serwer?
Problem 3: Wydajność
• Jak szybko będzie działał serwer?
src/server/index.js
import cache from 'memory-cache';
. . .
app.use('*', (req, res, next) => {
const cachedHtml = cache.get(req.originalUrl);
if (cachedHtml) {
res.send(cachedHtml);
} else {
next();
}
});
app.get('*', async (req, res) => {
. . .
cache.put(req.path, html);
res.send(html);
}
});
dodanie cache
Problem 3: Wydajność
src/server/index.js
import {createCacheStream} from ‘./cache-stream';
app.get('*', async (req, res) => {
. . .
const cacheStream = createCacheStream(req.path, cache);
cacheStream.pipe(res);
. . .
cacheStream.write(`<!DOCTYPE html>
<html>
. . .
<body>
<div id="root">`);
const appStream = renderToNodeStream(
. . .
);
appStream.pipe(cacheStream, {end: false});
appStream.on('end', () => {
cacheStream.end(`
. . .
</body>
</html>`);
});
}
});
wykorzystanie renderToNodeStream
src/server/cache-stream.js
import {Transform} from 'stream';
export const createCacheStream = (key, cache) => {
const bufferedChunks = [];
return new Transform({
transform(data, enc, cb) {
bufferedChunks.push(data);
cb(null, data);
},
flush(cb) {
cache.put(key, Buffer.concat(bufferedChunks).toString());
cb();
},
});
};
stream pomocniczy - cache
Problem 4: Uwierzytelnianie
• Jak renderować zawartość wymagającą autoryzacji? 🔐
Fetch token from the
server
Save token in
persistent storage
Pass token to the
server on requests
Authenticate &
authorize server-side
Problem 4: Uwierzytelnianie
app.post('/api/login', (req, res) => {
res.json({
token: '... ready for it?',
});
});
src/client/app/redux/auth/auth.actions.js
import fetch from 'isomorphic-fetch';
export const login = () => async dispatch => {
const res = await fetch(API_URL + '/api/login', {
method: 'POST',
});
const auth = await res.json();
localStorage.setItem('taytayAuth', auth.token);
return dispatch(setToken(auth.token));
};
akcja logowania
src/client/app/redux/auth/auth.actions.js
import fetch from 'isomorphic-fetch';
export const login = () => async dispatch => {
const res = await fetch(API_URL + '/api/login', {
method: 'POST',
});
const auth = await res.json();
localStorage.setItem('taytayAuth', auth.token);
return dispatch(setToken(auth.token));
};
akcja logowania
src/client/index.js
. . .
const token = localStorage.getItem('taytayAuth');
const store = createAppStore({
...(window.APP_STATE || {}),
auth: {
token,
},
});
ReactDOM.hydrate(
. . .
);
inicjalizacja store tokenem
src/client/app/redux/auth/auth.actions.js
const withAuthHoc = WrappedComponent => {
return class extends Component {
render() {
return this.props.isAuth ? (
<WrappedComponent {...this.props} />
) : (
<Redirect
to={{
pathname: '/login',
}}
/>
);
}
};
};
prosty guard na route
Problem 4: Uwierzytelnianie
• Jak renderować zawartość wymagającą autoryzacji? 🔐
🍪
src/client/app/redux/auth/auth.actions.js
import fetch from 'isomorphic-fetch';
export const login = () => async dispatch => {
const res = await fetch(API_URL + '/api/login', {
method: 'POST',
});
const auth = await res.json();
Cookies.set('taytayAuth', auth.token, {expires: 7, path: '/'});
return dispatch(setToken(auth.token));
};
zamiana localStorage na cookies
src/client/index.js
. . .
const token = Cookies.get('taytayAuth');
const store = createAppStore({
...(window.APP_STATE || {}),
auth: {
token,
},
});
ReactDOM.hydrate(
. . .
);
zamiana localStorage na cookies
src/server/index.js
import cookieParser from 'cookie-parser';
. . .
app.use(cookieParser());
. . .
app.get('*', async (req, res) => {
const token = req.cookies.taytayAuth;
const store = createAppStore({
auth: {
token,
},
});
if (matchedRoute) {
if (matchedRoute.private && !token) {
return res.redirect(301, ‘/login');
. . .
obsługa cookie na serwerze + inicjalizacja store
src/server/index.js
import cookieParser from 'cookie-parser';
. . .
app.use(cookieParser());
. . .
app.get('*', async (req, res) => {
const token = req.cookies.taytayAuth;
const store = createAppStore({
auth: {
token,
},
});
if (matchedRoute) {
if (matchedRoute.private && !token) {
return res.redirect(301, ‘/login');
. . .
obsługa cookie na serwerze + inicjalizacja store
Podsumowując
✅ Komunikacja z API
✅ Integracja z systemem zarządzania stanem
✅ Cache & streaming
✅ Uwierzytelnianie
Dzięki!
kamil.placzek@tsh.io

github.com/kamilplaczek/taytay-ssr

Node.js server-side rendering

  • 1.
  • 2.
  • 3.
    Server-side rendering • Przeglądarkawykonuje zapytanie, serwer zwraca przygotowany HTML
  • 5.
    Server-side rendering • Wkontekście SPA - uruchomienie kodu aplikacji i renderera po stronie serwera (kod uniwersalny)
  • 8.
  • 10.
    src/client/index.js import React from'react'; import ReactDOM from ‘react-dom'; import {BrowserRouter} from 'react-router-dom'; import App from './app/app.component'; ReactDOM.render( <BrowserRouter> <App /> </BrowserRouter>, document.getElementById('root') ); początkowy kod - uruchomienie spa
  • 11.
    src/server/index.js import express from'express'; const app = express(); app.use(express.static('dist')); app.get('*', (req, res) => { // doServerSideRenderingPls(); }); app.listen(3000); początkowy kod - serwer
  • 12.
    Problem 1: Routing •Jak przełożyć routing z przeglądarki na serwer? 🤷 export default class App extends Component { render() { return ( <div className="container"> ... <Route exact path="/" component={Home} /> <Route path="/contact" component={Contact} /> </div> </div> ); } }
  • 13.
    Problem 1: Routing •Routing jest uniwersalny. export default class App extends Component { render() { return ( <div className="container"> ... <Route exact path="/" component={Home} /> <Route path="/contact" component={Contact} /> </div> </div> ); } }
  • 14.
    src/server/index.js ... app.get('*', (req, res)=> { const context = {}; const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); if (context.url) { // redirect was done in client-side routing res.redirect(301, context.url); } else { res.send(` <!DOCTYPE html> . . . <body> <div id="root">${appString}</div> <script type="text/javascript" src="client.js"></script> </body> </html> `); } }); ... server side rendering z routerem
  • 15.
    src/server/index.js ... app.get('*', (req, res)=> { const context = {}; const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); if (context.url) { // redirect was done in client-side routing res.redirect(301, context.url); } else { res.send(` <!DOCTYPE html> . . . <body> <div id="root">${appString}</div> <script type="text/javascript" src="client.js"></script> </body> </html> `); } }); ... <div class="container" data-reactroot=""> <div> <div class="navbar"><span class="brand">Taylor Swift</span> <ul> <li><a href="/">Home</a></li> <li><a href="/contact">Contact</a></li> </ul> </div> <hr/> <div> <p> Taylor Alison Swift (born December 13, 1989) is an … </p> <ul></ul> </div> </div> </div> server side rendering z routerem
  • 16.
    src/server/index.js ... app.get('*', (req, res)=> { const context = {}; const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); if (context.url) { // redirect was done in client-side routing res.redirect(301, context.url); } else { res.send(` <!DOCTYPE html> . . . <body> <div id="root">${appString}</div> <script type="text/javascript" src="client.js"></script> </body> </html> `); } }); ... server side rendering z routerem <div class="container" data-reactroot=""> <div> <div class="navbar"><span class="brand">Taylor Swift</span> <ul> <li><a href="/">Home</a></li> <li><a href="/contact">Contact</a></li> </ul> </div> <hr/> <div> <p> Taylor Alison Swift (born December 13, 1989) is an … </p> <ul></ul> </div> </div> </div>
  • 17.
    src/server/index.js ... app.get('*', (req, res)=> { const context = {}; const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); if (context.url) { // redirect was done in client-side routing res.redirect(301, context.url); } else { res.send(` <!DOCTYPE html> . . . <body> <div id="root">${appString}</div> <script type="text/javascript" src="client.js"></script> </body> </html> `); } }); ... server side rendering z routerem <div class="container" data-reactroot=""> <div> <div class="navbar"><span class="brand">Taylor Swift</span> <ul> <li><a href="/">Home</a></li> <li><a href="/contact">Contact</a></li> </ul> </div> <hr/> <div> <p> Taylor Alison Swift (born December 13, 1989) is an … </p> <ul></ul> </div> </div> </div>
  • 18.
    Problem 2: Dane •Jak wypełnić dokument danymi podczas SSR? 🌅 async loadData() { const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', { headers: {Authorization: ‘Client-ID shAK3-it-0ff’}, }); ... this.setState({pics}); }
  • 19.
    Problem 2: Dane Server-siderender and return HTML Fetch JS app code Run app client-side loadData()
  • 20.
    Problem 2: Dane Server-siderender and return HTML Fetch JS app code Run app client- side loadData()loadData()
  • 21.
    src/server/index.js ... const getTaytayPics =async () => { const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', { headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘}, }); . . . return pics; }; ... app.get('*', async (req, res) => { const context = {}; if (req.url === '/') { context.pics = await getTaytayPics(); } const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); ... server side rendering + pobieranie danych v1
  • 22.
    src/server/index.js ... const getTaytayPics =async () => { const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', { headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘}, }); . . . return pics; }; ... app.get('*', async (req, res) => { const context = {}; if (req.url === '/') { context.pics = await getTaytayPics(); } const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); ... server side rendering + pobieranie danych v1
  • 23.
    src/client/app/home/home.component.js export class Homeextends Component { . . . componentWillMount() { if (this.props.staticContext && this.props.staticContext.pics) { this.setState({pics: this.props.staticContext.pics}) } else { this.loadData(); } } komponent react - wykorzystanie danych podczas ssr
  • 24.
    Problem 2: Dane Server-siderender and return HTML Fetch JS app code Run app client- side loadData()loadData()
  • 25.
    src/server/index.js app.get('*', async (req,res) => { . . . if (req.url === '/') { context.pics = await getTaytayPics(); } . . . } else { res.send(`<!DOCTYPE html> . . .> <script> window.APP_STATE = ${JSON.stringify({pics: context.pics})}; </script> <script type="text/javascript" src="client.js"></script> </body> </html>`); server side rendering - przekazanie informacji
  • 26.
    src/client/app/home/home.component.js componentWillMount() { if (this.props.staticContext&& this.props.staticContext.pics) { this.setState({pics: this.props.staticContext.pics}); } else if (window && window.APP_STATE && window.APP_STATE.pics) { this.setState({pics: window.APP_STATE.pics}) } else { this.loadData(); } } komponent react - wykorzystanie danych z ssr
  • 27.
    src/server/index.js ... const getTaytayPics =async () => { const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', { headers: {Authorization: ‘Client-ID l00K-wH4t-Y0u-m4DE-m3-D0‘}, }); . . . return pics; }; ... app.get('*', async (req, res) => { const context = {}; if (req.url === '/') { context.pics = await getTaytayPics(); } const appString = renderToString( <StaticRouter location={req.url} context={context}> <App /> </StaticRouter> ); ... DRY?
  • 28.
    src/client/app/app.routes.js import {Home} from'./home/home.component'; import {Contact} from './contact/contact.component'; export const routes = [ { component: Home, path: '/', exact: true, }, { component: Contact, path: '/contact', }, ]; refactoring routingu
  • 29.
    src/client/app/home/home.component.js import fetch from‘isomorphic-fetch'; . . . static async loadData() { const res = await fetch('https://api.imgur.com/3/gallery/r/taylorswift', { headers: {Authorization: 'Client-ID w1ld3est-dr3am5‘}, }); return pics; } . . . } refactoring komponentu react
  • 30.
    src/server/index.js import {StaticRouter, matchPath}from 'react-router'; import {routes} from '../client/app/app.routes'; . . . app.get('*', async (req, res) => { . . . const matchedRoute = routes.find(route => matchPath(req.path, route)); if (matchedRoute) { if (matchedRoute.component && matchedRoute.component.loadData) { context.data = await matchedRoute.component.loadData(); } } else { return res.sendStatus(404); } const appString = renderToString( . . . refactoring pobierania danych na serwerze
  • 31.
    Problem 2.5: Dane •Jak wypełnić danymi store?
  • 32.
    src/client/app/redux/taytay/taytay.actions.js import fetch from'isomorphic-fetch'; export const fetchPics = () => async dispatch => { const res = await fetch('https://api.imgur.com/3/gallery/r/ taylorswift', { headers: {Authorization: 'Client-ID 0447601918a7bb5'}, }); . . . return dispatch(setPics(pics)); }; export const setPics = pics => ({. . .}); przeniesienie pobierania danych do akcji
  • 33.
    src/client/app/home/home.component.js import {fetchPics} from'../redux/taytay/taytay.actions'; export class Home extends Component { static loadData = store => { return store.dispatch(fetchPics()); }; . . . wykorzystanie akcji w komponencie
  • 34.
    src/client/index.js import {createAppStore} from'./create-store'; const store = createAppStore(window.APP_STATE || {}); ReactDOM.hydrate( <Provider store={store}> <BrowserRouter> <App /> </BrowserRouter> </Provider>, document.getElementById('root') ); stworzenie store + odtworzenie stanu z ssr
  • 35.
    src/server/index.js import {createAppStore} from'../client/create-store'; . . . app.get('*', async (req, res) => { const store = createAppStore({}); const matchedRoute = routes.find(route => matchPath(req.path, route)); if (matchedRoute) { if (matchedRoute.component && matchedRoute.component.loadData) { await matchedRoute.component.loadData(store); } . . . } else { const state = store.getState(); const html = `<!DOCTYPE html> . . . <div id="root">${appString}</div> <script> window.APP_STATE = ${JSON.stringify(state)}; </script> . . . } }); stworzenie store + inicjalizacja stanu podczas ssr
  • 36.
    src/server/index.js import {createAppStore} from'../client/create-store'; . . . app.get('*', async (req, res) => { const store = createAppStore({}); const matchedRoute = routes.find(route => matchPath(req.path, route)); if (matchedRoute) { if (matchedRoute.component && matchedRoute.component.loadData) { await matchedRoute.component.loadData(store); } . . . } else { const state = store.getState(); const html = `<!DOCTYPE html> . . . <div id="root">${appString}</div> <script> window.APP_STATE = ${JSON.stringify(state)}; </script> . . . } }); stworzenie store + inicjalizacja stanu podczas ssr
  • 37.
    src/server/index.js import {createAppStore} from'../client/create-store'; . . . app.get('*', async (req, res) => { const store = createAppStore({}); const matchedRoute = routes.find(route => matchPath(req.path, route)); if (matchedRoute) { if (matchedRoute.component && matchedRoute.component.loadData) { await matchedRoute.component.loadData(store); } . . . } else { const state = store.getState(); const html = `<!DOCTYPE html> . . . <div id="root">${appString}</div> <script> window.APP_STATE = ${JSON.stringify(state)}; </script> . . . } }); stworzenie store + inicjalizacja stanu podczas ssr
  • 38.
    Problem 3: Wydajność •Jak szybko będzie działał serwer?
  • 39.
    Problem 3: Wydajność •Jak szybko będzie działał serwer?
  • 40.
    src/server/index.js import cache from'memory-cache'; . . . app.use('*', (req, res, next) => { const cachedHtml = cache.get(req.originalUrl); if (cachedHtml) { res.send(cachedHtml); } else { next(); } }); app.get('*', async (req, res) => { . . . cache.put(req.path, html); res.send(html); } }); dodanie cache
  • 41.
  • 42.
    src/server/index.js import {createCacheStream} from‘./cache-stream'; app.get('*', async (req, res) => { . . . const cacheStream = createCacheStream(req.path, cache); cacheStream.pipe(res); . . . cacheStream.write(`<!DOCTYPE html> <html> . . . <body> <div id="root">`); const appStream = renderToNodeStream( . . . ); appStream.pipe(cacheStream, {end: false}); appStream.on('end', () => { cacheStream.end(` . . . </body> </html>`); }); } }); wykorzystanie renderToNodeStream
  • 43.
    src/server/cache-stream.js import {Transform} from'stream'; export const createCacheStream = (key, cache) => { const bufferedChunks = []; return new Transform({ transform(data, enc, cb) { bufferedChunks.push(data); cb(null, data); }, flush(cb) { cache.put(key, Buffer.concat(bufferedChunks).toString()); cb(); }, }); }; stream pomocniczy - cache
  • 44.
    Problem 4: Uwierzytelnianie •Jak renderować zawartość wymagającą autoryzacji? 🔐 Fetch token from the server Save token in persistent storage Pass token to the server on requests Authenticate & authorize server-side
  • 45.
    Problem 4: Uwierzytelnianie app.post('/api/login',(req, res) => { res.json({ token: '... ready for it?', }); });
  • 46.
    src/client/app/redux/auth/auth.actions.js import fetch from'isomorphic-fetch'; export const login = () => async dispatch => { const res = await fetch(API_URL + '/api/login', { method: 'POST', }); const auth = await res.json(); localStorage.setItem('taytayAuth', auth.token); return dispatch(setToken(auth.token)); }; akcja logowania
  • 47.
    src/client/app/redux/auth/auth.actions.js import fetch from'isomorphic-fetch'; export const login = () => async dispatch => { const res = await fetch(API_URL + '/api/login', { method: 'POST', }); const auth = await res.json(); localStorage.setItem('taytayAuth', auth.token); return dispatch(setToken(auth.token)); }; akcja logowania
  • 48.
    src/client/index.js . . . consttoken = localStorage.getItem('taytayAuth'); const store = createAppStore({ ...(window.APP_STATE || {}), auth: { token, }, }); ReactDOM.hydrate( . . . ); inicjalizacja store tokenem
  • 49.
    src/client/app/redux/auth/auth.actions.js const withAuthHoc =WrappedComponent => { return class extends Component { render() { return this.props.isAuth ? ( <WrappedComponent {...this.props} /> ) : ( <Redirect to={{ pathname: '/login', }} /> ); } }; }; prosty guard na route
  • 50.
    Problem 4: Uwierzytelnianie •Jak renderować zawartość wymagającą autoryzacji? 🔐 🍪
  • 51.
    src/client/app/redux/auth/auth.actions.js import fetch from'isomorphic-fetch'; export const login = () => async dispatch => { const res = await fetch(API_URL + '/api/login', { method: 'POST', }); const auth = await res.json(); Cookies.set('taytayAuth', auth.token, {expires: 7, path: '/'}); return dispatch(setToken(auth.token)); }; zamiana localStorage na cookies
  • 52.
    src/client/index.js . . . consttoken = Cookies.get('taytayAuth'); const store = createAppStore({ ...(window.APP_STATE || {}), auth: { token, }, }); ReactDOM.hydrate( . . . ); zamiana localStorage na cookies
  • 53.
    src/server/index.js import cookieParser from'cookie-parser'; . . . app.use(cookieParser()); . . . app.get('*', async (req, res) => { const token = req.cookies.taytayAuth; const store = createAppStore({ auth: { token, }, }); if (matchedRoute) { if (matchedRoute.private && !token) { return res.redirect(301, ‘/login'); . . . obsługa cookie na serwerze + inicjalizacja store
  • 54.
    src/server/index.js import cookieParser from'cookie-parser'; . . . app.use(cookieParser()); . . . app.get('*', async (req, res) => { const token = req.cookies.taytayAuth; const store = createAppStore({ auth: { token, }, }); if (matchedRoute) { if (matchedRoute.private && !token) { return res.redirect(301, ‘/login'); . . . obsługa cookie na serwerze + inicjalizacja store
  • 55.
    Podsumowując ✅ Komunikacja zAPI ✅ Integracja z systemem zarządzania stanem ✅ Cache & streaming ✅ Uwierzytelnianie
  • 56.