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.