-
-
Save cereallarceny/ee1b86227aabaf4a4b2a3144b84dfaa2 to your computer and use it in GitHub Desktop.
const md5File = require('md5-file'); | |
const path = require('path'); | |
// CSS styles will be imported on load and that complicates matters... ignore those bad boys! | |
const ignoreStyles = require('ignore-styles'); | |
const register = ignoreStyles.default; | |
// We also want to ignore all image requests | |
// When running locally these will load from a standard import | |
// When running on the server, we want to load via their hashed version in the build folder | |
const extensions = ['.gif', '.jpeg', '.jpg', '.png', '.svg']; | |
// Override the default style ignorer, also modifying all image requests | |
register(ignoreStyles.DEFAULT_EXTENSIONS, (mod, filename) => { | |
if (!extensions.find(f => filename.endsWith(f))) { | |
// If we find a style | |
return ignoreStyles.noOp(); | |
} else { | |
// If we find an image | |
const hash = md5File.sync(filename).slice(0, 8); | |
const bn = path.basename(filename).replace(/(\.\w{3})$/, `.${hash}$1`); | |
mod.exports = `/static/media/${bn}`; | |
} | |
}); | |
// Set up babel to do its thing... env for the latest toys, react-app for CRA | |
// Notice three plugins: the first two allow us to use import rather than require, the third is for code splitting | |
// Polyfill is required for Babel 7, polyfill includes a custom regenerator runtime and core-js | |
require('@babel/polyfill'); | |
require('@babel/register')({ | |
ignore: [/\/(build|node_modules)\//], | |
presets: ['@babel/preset-env', '@babel/preset-react'], | |
plugins: [ | |
'@babel/plugin-syntax-dynamic-import', | |
'dynamic-import-node', | |
'react-loadable/babel' | |
] | |
}); | |
// Now that the nonsense is over... load up the server entry point | |
require('./server'); |
// Express requirements | |
import path from 'path'; | |
import fs from 'fs'; | |
// React requirements | |
import React from 'react'; | |
import { renderToString } from 'react-dom/server'; | |
import Helmet from 'react-helmet'; | |
import { Provider } from 'react-redux'; | |
import { StaticRouter } from 'react-router'; | |
import { Frontload, frontloadServerRender } from 'react-frontload'; | |
import Loadable from 'react-loadable'; | |
// Our store, entrypoint, and manifest | |
import createStore from '../src/store'; | |
import App from '../src/app/app'; | |
import manifest from '../build/asset-manifest.json'; | |
// Some optional Redux functions related to user authentication | |
import { setCurrentUser, logoutUser } from '../src/modules/auth'; | |
// LOADER | |
export default (req, res) => { | |
/* | |
A simple helper function to prepare the HTML markup. This loads: | |
- Page title | |
- SEO meta tags | |
- Preloaded state (for Redux) depending on the current route | |
- Code-split script tags depending on the current route | |
*/ | |
const injectHTML = (data, { html, title, meta, body, scripts, state }) => { | |
data = data.replace('<html>', `<html ${html}>`); | |
data = data.replace(/<title>.*?<\/title>/g, title); | |
data = data.replace('</head>', `${meta}</head>`); | |
data = data.replace( | |
'<div id="root"></div>', | |
`<div id="root">${body}</div><script>window.__PRELOADED_STATE__ = ${state}</script>` | |
); | |
data = data.replace('</body>', scripts.join('') + '</body>'); | |
return data; | |
}; | |
// Load in our HTML file from our build | |
fs.readFile( | |
path.resolve(__dirname, '../build/index.html'), | |
'utf8', | |
(err, htmlData) => { | |
// If there's an error... serve up something nasty | |
if (err) { | |
console.error('Read error', err); | |
return res.status(404).end(); | |
} | |
// Create a store (with a memory history) from our current url | |
const { store } = createStore(req.url); | |
// If the user has a cookie (i.e. they're signed in) - set them as the current user | |
// Otherwise, we want to set the current state to be logged out, just in case this isn't the default | |
if ('mywebsite' in req.cookies) { | |
store.dispatch(setCurrentUser(req.cookies.mywebsite)); | |
} else { | |
store.dispatch(logoutUser()); | |
} | |
const context = {}; | |
const modules = []; | |
/* | |
Here's the core funtionality of this file. We do the following in specific order (inside-out): | |
1. Load the <App /> component | |
2. Inside of the Frontload HOC | |
3. Inside of a Redux <StaticRouter /> (since we're on the server), given a location and context to write to | |
4. Inside of the store provider | |
5. Inside of the React Loadable HOC to make sure we have the right scripts depending on page | |
6. Render all of this sexiness | |
7. Make sure that when rendering Frontload knows to get all the appropriate preloaded requests | |
In English, we basically need to know what page we're dealing with, and then load all the appropriate scripts and | |
data for that page. We take all that information and compute the appropriate state to send to the user. This is | |
then loaded into the correct components and sent as a Promise to be handled below. | |
*/ | |
frontloadServerRender(() => | |
renderToString( | |
<Loadable.Capture report={m => modules.push(m)}> | |
<Provider store={store}> | |
<StaticRouter location={req.url} context={context}> | |
<Frontload isServer={true}> | |
<App /> | |
</Frontload> | |
</StaticRouter> | |
</Provider> | |
</Loadable.Capture> | |
) | |
).then(routeMarkup => { | |
if (context.url) { | |
// If context has a url property, then we need to handle a redirection in Redux Router | |
res.writeHead(302, { | |
Location: context.url | |
}); | |
res.end(); | |
} else { | |
// Otherwise, we carry on... | |
// Let's give ourself a function to load all our page-specific JS assets for code splitting | |
const extractAssets = (assets, chunks) => | |
Object.keys(assets) | |
.filter(asset => chunks.indexOf(asset.replace('.js', '')) > -1) | |
.map(k => assets[k]); | |
// Let's format those assets into pretty <script> tags | |
const extraChunks = extractAssets(manifest, modules).map( | |
c => `<script type="text/javascript" src="/${c.replace(/^\//, '')}"></script>` | |
); | |
// We need to tell Helmet to compute the right meta tags, title, and such | |
const helmet = Helmet.renderStatic(); | |
// NOTE: Disable if you desire | |
// Let's output the title, just to see SSR is working as intended | |
console.log('THE TITLE', helmet.title.toString()); | |
// Pass all this nonsense into our HTML formatting function above | |
const html = injectHTML(htmlData, { | |
html: helmet.htmlAttributes.toString(), | |
title: helmet.title.toString(), | |
meta: helmet.meta.toString(), | |
body: routeMarkup, | |
scripts: extraChunks, | |
state: JSON.stringify(store.getState()).replace(/</g, '\\u003c') | |
}); | |
// We have all the final HTML, let's send it to the user already! | |
res.send(html); | |
} | |
}); | |
} | |
); | |
}; |
// Express requirements | |
import bodyParser from 'body-parser'; | |
import compression from 'compression'; | |
import express from 'express'; | |
import morgan from 'morgan'; | |
import path from 'path'; | |
import forceDomain from 'forcedomain'; | |
import Loadable from 'react-loadable'; | |
import cookieParser from 'cookie-parser'; | |
// Our loader - this basically acts as the entry point for each page load | |
import loader from './loader'; | |
// Create our express app using the port optionally specified | |
const app = express(); | |
const PORT = process.env.PORT || 3000; | |
// NOTE: UNCOMMENT THIS IF YOU WANT THIS FUNCTIONALITY | |
/* | |
Forcing www and https redirects in production, totally optional. | |
http://mydomain.com | |
http://www.mydomain.com | |
https://mydomain.com | |
Resolve to: https://www.mydomain.com | |
*/ | |
// if (process.env.NODE_ENV === 'production') { | |
// app.use( | |
// forceDomain({ | |
// hostname: 'www.mydomain.com', | |
// protocol: 'https' | |
// }) | |
// ); | |
// } | |
// Compress, parse, log, and raid the cookie jar | |
app.use(compression()); | |
app.use(bodyParser.json()); | |
app.use(bodyParser.urlencoded({ extended: false })); | |
app.use(morgan('dev')); | |
app.use(cookieParser()); | |
// Set up homepage, static assets, and capture everything else | |
app.use(express.Router().get('/', loader)); | |
app.use(express.static(path.resolve(__dirname, '../build'))); | |
app.use(loader); | |
// We tell React Loadable to load all required assets and start listening - ROCK AND ROLL! | |
Loadable.preloadAll().then(() => { | |
app.listen(PORT, console.log(`App listening on port ${PORT}!`)); | |
}); | |
// Handle the bugs somehow | |
app.on('error', error => { | |
if (error.syscall !== 'listen') { | |
throw error; | |
} | |
const bind = typeof PORT === 'string' ? 'Pipe ' + PORT : 'Port ' + PORT; | |
switch (error.code) { | |
case 'EACCES': | |
console.error(bind + ' requires elevated privileges'); | |
process.exit(1); | |
break; | |
case 'EADDRINUSE': | |
console.error(bind + ' is already in use'); | |
process.exit(1); | |
break; | |
default: | |
throw error; | |
} | |
}); |
This is great
Hi Thanks for providing the good subject with us. But i have one doubt, present i am using ASW EC2 virtual machine for deploy the application. I have created application like create-react-app. My problem is like i am not able to see the changes until refresh the browser when new build is move. by using this concept server side render can I able to solve this problem??? if yes i will start integrate these things in my application. Please suggest me. Thanks in advance!!!.
Just to mention, there's a simpler alternative to the SSR approach provided here, if you just need to fix your SEO:
You can use quite straightforward pre-render solutions like Prerender.io or Rendertron. You set them up to work just for social/search engines and they do the rest of the magic without the need to change your application at all.
I tried it but I am having a problem, when I run on the web there is an error
Uncaught TypeError: Cannot read property '.css' of undefined
located in: ignore-styles.js
oldHandlers[ext] = require.extensions[ext]
require.extensions[ext] = handler
Can you help me, thank you in advance.
Sweet!