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…