-
-
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!