|
import DocumentTitle from 'react-document-title' |
|
import Html from './html' |
|
import MobileDetect from 'mobile-detect' |
|
import Promise from 'bluebird' |
|
import React from 'react' |
|
import Router from 'react-router' |
|
import Transmit from 'react-transmit' |
|
import config from './config' |
|
import initialState from './initialstate' |
|
import renderToString from './react-transmit/render_to_string' |
|
import routes from '../client/routes' |
|
import {state} from '../client/state' |
|
import {storeCookies} from '../client/request_store' |
|
|
|
export default function render(req, res, locale) { |
|
const device = new MobileDetect(req.headers['user-agent']).phone() ? 'mobile' : 'desktop' |
|
const path = req.path |
|
|
|
return renderPage(req, res, initialState, path, device) |
|
} |
|
|
|
// This function sets up the state before first and second round of React.renderToString |
|
// inside of Transmit.renderToString |
|
function beforeRender(req, appState) { |
|
return function beforeRender() { |
|
state.load(appState) |
|
storeCookies(req) |
|
} |
|
} |
|
|
|
// TODO: Refactor. |
|
function renderPage(req, res, appState, path, device) { |
|
return new Promise((resolve, reject) => { |
|
const router = Router.create({ |
|
routes, |
|
location: path, |
|
onError: reject, |
|
onAbort: (abortReason) => { |
|
if (abortReason.constructor.name === 'Redirect') { |
|
const {to, params, query} = abortReason |
|
const path = router.makePath(to, params, query) |
|
res.redirect(path) |
|
resolve() |
|
return |
|
} |
|
reject(abortReason) |
|
} |
|
}) |
|
router.run((Handler, routerState) => { |
|
const notFound = routerState.routes.some(route => route.name === 'not-found') |
|
const status = notFound ? 404 : 200 |
|
|
|
// getPageHtml now returns a Promise |
|
getPageHtml(Handler, appState, device, routerState.routes, beforeRender) |
|
.then(html => { |
|
res.status(status).send(html) |
|
resolve() |
|
}).catch(error => { |
|
console.error('getPageHTML error', error) |
|
reject(error) |
|
}) |
|
}) |
|
}) |
|
} |
|
|
|
function getPageHtml(Handler, appState, device, matchedRoutes, beforeRender) { |
|
// Important! We need to send the matchedRoute as queryParam to the main component |
|
return renderToString(Handler, beforeRender, {queryParams: {matchedRoutes}}).then(result => { |
|
const {reactString, reactData} = result |
|
const appHtml = `<div id="single-page-app"><div id="react-root">${reactString}</div></div>` |
|
const appScriptSrc = config.env.isProduction |
|
? '/app.js?v=' + config.version |
|
: '//localhost:8888/build/public/app.js' |
|
|
|
let scriptHtml = ` |
|
<script> |
|
(function() { |
|
window._appState = ${JSON.stringify(appState)}; |
|
var app = document.createElement('script'); app.type = 'text/javascript'; app.async = true; |
|
var src = '${appScriptSrc}'; |
|
// IE<11 and Safari need Intl polyfill. |
|
if (!window.Intl) src = src.replace('.js', 'intl.js'); |
|
app.src = src; |
|
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(app, s); |
|
})() |
|
</script>` |
|
|
|
if (config.env.isProduction && config.googleAnalyticsId !== 'UA-XXXXXXX-X') |
|
scriptHtml += ` |
|
<script> |
|
(function(b,o,i,l,e,r){b.GoogleAnalyticsObject=l;b[l]||(b[l]= |
|
function(){(b[l].q=b[l].q||[]).push(arguments)});b[l].l=+new Date; |
|
e=o.createElement(i);r=o.getElementsByTagName(i)[0]; |
|
e.src='//www.google-analytics.com/analytics.js'; |
|
r.parentNode.insertBefore(e,r)}(window,document,'script','ga')); |
|
ga('create','${config.googleAnalyticsId}');ga('send','pageview'); |
|
</script>` |
|
|
|
const title = DocumentTitle.rewind() |
|
|
|
const outputHtml = '<!DOCTYPE html>' + React.renderToStaticMarkup( |
|
<Html |
|
bodyHtml={appHtml + scriptHtml} |
|
isProduction={config.env.isProduction} |
|
title={title} |
|
version={config.version} |
|
device={device} |
|
/> |
|
) |
|
|
|
// This adds window.__reactTransmitPacket with results of the queries |
|
// so that client does not need to perform the same queries again |
|
return Transmit.injectIntoMarkup(outputHtml, reactData) |
|
}) |
|
} |
Man, Thank you for sharing! I was just working on adding transmit to my Este+Loopback app, this Gist is magic!