In real life projects, most of these will require server-side rendering.

Advantages of server-side rendering:

  • 1. Good performance of the first screen, you do not need to wait for the completion of JS loading to see the page

  • 2. It’s good for SEO

There are many tutorials for server-side rendering on the web, but they are too fragmented or too low. A good example can save you a lot of time!


This is the simplest server-side rendering example

Making address:Github.com/tzuser/ssr_…

Project directory

  • Server indicates the server directory. Because this is the most basic server-side rendering, servers share only front-end components for code clarity and learning.
  • Server /index.js is the server entry file
  • Static Stores static files

The tutorial begins with the Webpack configuration

First distinguish between production and development environments. The development environment uses Webpack-dev-server as the server

Webpack.config.js base configuration file

const path=require('path');
const webpack=require('webpack');
const HTMLWebpackPlugin = require('html-webpack-plugin');/ / HTML generates
module.exports={
	entry: {
		main:path.join(__dirname,'./src/index.js'),
		vendors: ['react'.'react-redux']// Component separation
	},
	output: {path: path.resolve(__dirname,'build'),
		publicPath: '/'.filename:'[name].js'.chunkFilename:'[name].[id].js'
	},
	context:path.resolve(__dirname,'src'),
	module: {rules:[
			{
				test:/\.(js|jsx)$/.use: [{loader:'babel-loader'.options: {presets: ['env'.'react'.'stage-0'],},}]},resolve: {extensions: ['.js'.'.jsx'.'.less'.'.scss'.'.css']},
	plugins: [new HTMLWebpackPlugin({// Generate the index.html file according to index.ejs
			title:'Webpack configuration'.inject: true.filename: 'index.html'.template: path.join(__dirname,'./index.ejs')}),new webpack.optimize.CommonsChunkPlugin({// Public component separation
			  names: ['vendors'.'manifest']}),],}Copy the code

Development environment webpack.dev.js

Hot updates are required in the development environment to facilitate development, but not in the release environment!

In the production environment, react-loadable is required for module loading to improve user access speed, but not for development.

const path=require('path');
const webpack=require('webpack');
const config=require('./webpack.config.js');// Load the base configuration

config.plugins.push(// Add a plug-in
	new webpack.HotModuleReplacementPlugin()/ / thermal load
)

let devConfig={
	context:path.resolve(__dirname,'src'),
	devtool: 'eval-source-map'.devServer: {/ / dev - server parameter
		contentBase: path.join(__dirname,'./build'),
		inline:true.hot:true.// Start hot loading
		open : true.// Run open browser
		port: 8900.historyApiFallback:true.watchOptions: {// Listen for configuration changes
			aggregateTimeout: 300.poll: 1000}}},module.exports=Object.assign({},config,devConfig)
Copy the code

Production environment webpack.build.js

Delete the previously packaged files using the clean-webpack-plugin before packaging. Using react-loadable/webpack to lazy-load the ReactLoadablePlugin generates a react-loadable.json file, which is used in the background

const config=require('./webpack.config.js');
const path=require('path');
const {ReactLoadablePlugin}=require('react-loadable/webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');// Copy the file
const CleanWebpackPlugin = require("clean-webpack-plugin");// Delete files

let buildConfig={

}
let newPlugins=[
    new CleanWebpackPlugin(['./build']),
    // Copy files
    new CopyWebpackPlugin([
      {from:path.join(__dirname,'./static'),to:'static'}]),// lazy loading
	new ReactLoadablePlugin({
	      filename: './build/react-loadable.json',
	})
]

config.plugins=config.plugins.concat(newPlugins);
module.exports=Object.assign({},config,buildConfig)
Copy the code

The template file index.ejs

In the base configuration webpack.config.js, the HTMLWebpackPlugin generates index.html from this template file and adds the required JS to the bottom

Pay attention to

  • The template file is only used for front-end development or packaging, and the back end reads the index.html generated by HTMLWebpackPlugin.
  • There is a window. Main () under the body, which is used to ensure that all js loads are completed before the react render is called. The window.

      
<html>
<head>
	<meta charset="utf-8">
	<meta http-equiv="X-UA-Compatible" content="IE=edge">
	<link rel="icon" href="/static/favicon.ico" mce_href="/static/favicon.ico" type="image/x-icon">
	<link rel="manifest" href="/static/manifest.json">
	<meta name="viewport" content="width=device-width,user-scalable=no" >
	<title><% = htmlWebpackPlugin.options.title% ></title>
</head>
<body>
	<div id="root"></div>
</body>
<script>window.main();</script>
</html>
Copy the code

Entry file SRC /index.js

Module.hot. accept will listen for changes in the app.jsx file and the files referenced in the App, which will need to be reloaded and rendered. So encapsulate render as render method, easy to call.

Exposes the main method to the window and ensures that Loadable. PreloadReady is preloaded before rendering

import React,{Component} from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import {createStore,applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
// Browser development tools
import {composeWithDevTools} from 'redux-devtools-extension/developmentOnly';
import reducers from './reducers/index';

import createHistory from 'history/createBrowserHistory';
import {ConnectedRouter,routerMiddleware} from 'react-router-redux';
import {  Router } from 'react-router-dom';
import Loadable from 'react-loadable';

const history = createHistory()
const middleware=[thunk,routerMiddleware(history)];
conststore=createStore( reducers, composeWithDevTools(applyMiddleware(... middleware)) )if(module.hot) {// Check whether hot loading is enabled
		module.hot.accept('./reducers/index.js', () = > {// Listen to the reducers file
			import('./reducers/index.js').then(({default:nextRootReducer}) = >{
				store.replaceReducer(nextRootReducer);
			});
		});
		module.hot.accept('./Containers/App.jsx', () = > {// Listen on the app.jsx file
			render(store)
		});
	}

const render=(a)= >{
	const App = require("./Containers/App.jsx").default;
	ReactDOM.hydrate(
		<Provider store={store}>
			<ConnectedRouter history={history}>
				<App />
			</ConnectedRouter>
		</Provider>.document.getElementById('root'))}window.main = (a)= > {// Expose the main method to window
  Loadable.preloadReady().then((a)= > {
	render()
  });
};

Copy the code

APP. JSX container

import React,{Component} from 'react';
import {Route,Link} from 'react-router-dom';
import Loadable from 'react-loadable';
const loading=(a)= ><div>Loading...</div>;
const LoadableHome=Loadable({
	loader:(a)= > import(/* webpackChunkName: 'Home' */ './Home'),
	loading
});
const LoadableUser = Loadable({
  loader: (a)= > import(/* webpackChunkName: 'User' */ './User'),
  loading
});
const LoadableList = Loadable({
  loader: (a)= > import(/* webpackChunkName: 'List' */ './List'),
  loading
});
class App extends Component{
	render(){
		return(
			<div>
				<Route exact path="/"  component={LoadableHome}/>
				<Route path="/user" component={LoadableUser}/>
				<Route path="/list" component={LoadableList}/>

				<Link to="/user">user</Link>
				<Link to="/list">list</Link>
			</div>
		)
	}
};
export default App
Copy the code

Note that the Home, User, and List pages are all referenced here

const LoadableHome=Loadable({
	loader:(a)= > import(/* webpackChunkName: 'Home' */ './Home'),
	loading
});
Copy the code

This lazily loads the file instead of importing Home from ‘./Home’.

/* webpackChunkName: ‘Home’ */ specifies the chunk name when packaging

Home. The JSX container

Home is just a normal container that doesn’t require any special handling

import React,{Component} from 'react';
const Home=(a)= ><div>Changes to the home page</div>
export default Home
Copy the code

Next – the server side

server/index.js

Loads a bunch of plugins to support es6 syntax and front-end components

require('babel-polyfill')
require('babel-register') ({ignore: /\/(build|node_modules)\//.presets: ['env'.'babel-preset-react'.'stage-0'].plugins: ['add-module-exports'.'syntax-dynamic-import'."dynamic-import-node"."react-loadable/babel"]});require('./server');
Copy the code

server/server.js

Note that the route matches the route first, then the static file, and app.use(render) then points to render. Why would you do that?

For example, the user access root path/route match successfully renders the home page. The static file will be matched again. If the file is matched successfully, main.js will be returned.

If the user visits the url /user and the route and static file do not match, then the user page will be rendered successfully.

const Loadable=require('react-loadable');
const Router = require('koa-router');
const router = new Router();

const path= require('path')
const staticServer =require('koa-static')
const Koa = require('koa')
const app = new Koa()
const render = require('./render.js')

router.get('/', render);

app.use(router.routes())
.use(router.allowedMethods())
.use(staticServer(path.resolve(__dirname, '.. /build')));
app.use(render);


Loadable.preloadAll().then((a)= > {
  app.listen(3000, () = > {console.log('Running on http://localhost:3000/');
  });
});

Copy the code

The most important server/render.js

Write a prepHTML method to facilitate processing of index.html. Render first loads index.html to get store and history via createServerStore incoming route.

[‘./Tab’, ‘./Home’] [‘./Tab’, ‘./Home’]

The true path of the component is retrieved using the getBundles(Stats, Modules) method. Stats is react-loadable. Json generated when webPack is packaged

[{id: 1050.name: '.. / node_modules /. 1.0.0 - beta. @ 25 material - UI/Tabs/Tab. Js'.file: 'User.3.js' },
  { id: 1029.name: './Containers/Tab.jsx'.file: 'Tab.6.js' },
  { id: 1036.name: './Containers/Home.jsx'.file: 'Home.5.js'}]Copy the code

Use bundles. Filter to distinguish BETWEEN CSS and JS files. The files loaded on the first screen are packed into THE HTML.

import React from 'react'
import Loadable from 'react-loadable';
import { renderToString } from 'react-dom/server';
import App from '.. /src/Containers/App.jsx';
import {ConnectedRouter,routerMiddleware} from 'react-router-redux';
import { StaticRouter } from 'react-router-dom'
import createServerStore from './store';
import {Provider} from 'react-redux';
import path from 'path';
import fs from 'fs';
import Helmet from 'react-helmet';
import { getBundles } from 'react-loadable/webpack'
import stats from '.. /build/react-loadable.json';

/ / HTML processing
const prepHTML=(data,{html,head,style,body,script}) = >{
	data=data.replace('<html'.`<html ${html}`);
	data=data.replace('</head>'.`${head}${style}</head>`);
	data=data.replace('<div id="root"></div>'.`<div id="root">${body}</div>`);
	data=data.replace('</body>'.`${script}</body>`);
	return data;
}

const render=async (ctx,next)=>{
		const filePath=path.resolve(__dirname,'.. /build/index.html')
		let html=await new Promise((resolve,reject) = >{
			fs.readFile(filePath,'utf8',(err,htmlData)=>{// Read the index.html file
				if(err){
					console.error('Error reading file! ',err);
					return res.status(404).end()
				}
				/ / for the store
				const { store, history } = createServerStore(ctx.req.url);

				let modules=[];
				letrouteMarkup =renderToString( <Loadable.Capture report={moduleName => modules.push(moduleName)}> <Provider store={store}>  <ConnectedRouter history={history}> <App/> </ConnectedRouter> </Provider> </Loadable.Capture> ) let bundles = getBundles(stats, modules); let styles = bundles.filter(bundle => bundle.file.endsWith('.css')); let scripts = bundles.filter(bundle => bundle.file.endsWith('.js')); let styleStr=styles.map(style => { return `<link href="/dist/${style.file}" rel="stylesheet"/>` }).join('\n') let scriptStr=scripts.map(bundle => { return `<script src="/${bundle.file}"></script>` }).join('\n') const helmet=Helmet.renderStatic(); const html=prepHTML(htmlData,{ html:helmet.htmlAttributes.toString(), head:helmet.title.toString()+helmet.meta.toString()+helmet.link.toString(), style:styleStr, body:routeMarkup, script:scriptStr, }) resolve(html) }) }) ctx.body=html; } export default render;Copy the code

server/store.js

Create store and history similar to the front end, createHistory({initialEntries: [path]}) where path is the routing address

import { createStore, applyMiddleware, compose } from 'redux';
import { routerMiddleware } from 'react-router-redux';
import thunk from 'redux-thunk';

import createHistory from 'history/createMemoryHistory';
import rootReducer from '.. /src/reducers/index';

// Create a store and history based on a path
const createServerStore = (path = '/') => {
  const initialState = {};

  // We don't have a DOM, so let's create some fake history and push the current path
  let history = createHistory({ initialEntries: [path] });

  // All the middlewares
  const middleware = [thunk, routerMiddleware(history)]; const composedEnhancers = compose(applyMiddleware(... middleware)); // Store it all const store = createStore(rootReducer, initialState, composedEnhancers); // Return all that I needreturn {
    history,
    store
  };
};

export default createServerStore;
Copy the code

Senior advanced

The following is an exercise that I am writing.

More mature than above! I’ll do a tutorial later

preview

Click preview

Making address:github.com/tzuser/ssr

reference

  • React-loadable is loaded in modules
  • The Material – UI server rendering configuration

This is a server rendering tutorial written by my colleague, also very good

Juejin. Cn/post / 684490…