Most of you have heard of this concept, and many of you may have done server-side rendering projects in your company. Mainstream single-page applications such as Vue or React development projects generally adopt client-side rendering mode, which is also called CSR.
However, this mode will bring two obvious problems. The first one is the long TTFP time, TTFP refers to the first screen display time, and it does not have the conditions of SEO ranking, so the search engine ranking is not very good. So there are tools that we can use to improve our projects and use single-page application programming to render projects on the server side to solve these problems.
At present, the mainstream server-side rendering framework, namely SSR framework, includes nuxt.js for Vue and next.js for React. Instead of using these SSR frameworks, we built a complete SET of SSR frameworks from scratch to familiarize ourselves with its underlying principles.
Write the React component on the server
In the case of client rendering, the browser first sends a request to the browser, and the server returns an HTML file. The server then sends a request from the HTML to the server, and the server returns a JS file. The JS file is executed in the browser to render the page structure and the browser completes the page rendering.
This process is different with server-side rendering. The browser sends the request, the server runs the React code to generate the page, and the server returns the resulting page to the browser, which renders it. In this case the React code is part of the server rather than the front end.
NPM init initializes the project, then installs react, Express, webpack, webpack-cli, webpack-node-externals.
Let’s start by writing a React component. . The SRC/components/Home/index, js, because our js is executed in the node environment so we should follow CommonJS standard, using the require and module. Exports to import and export.
const React = require('react');
const Home = () = > {
return <div>home</div>
}
module.exports = {
default: Home
};
Copy the code
The Home component we developed here cannot run directly in Node. We need to use the Webpack tool to package JSX syntax into JS syntax, so that NodeJS can be recognized. We need to create a webpack.server.js file.
Using Webpack on the server side requires adding a key-value pair whose target is Node. We know that if path is used on the server side, it does not need to be packaged into JS. If path is used on the browser side, it needs to be packaged into JS. Therefore, js compiled on the server side and browser side are completely different. So we need to tell WebPack when we package whether we are packaging server-side code or browser-side code.
/ SRC /index.js. The output file is called bundle, and the directory is in the build folder of the same directory.
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // The server running webPack needs to run NodeExternals, which is used to keep node modules such as Express from being packaged into JS.
module.exports = {
target: 'node'.mode: 'development'.entry: './src/server/index.js'.output: {
filename: 'bundle.js'.path: Path.resolve(__dirname, 'build')},externals: [NodeExternals()],
module: {
rules: [{test: /.js? $/,
loader: 'babel-loader'.exclude: /node_modules/,
options: {
presets: ['react'.'stage-0'['env', {
targets: {
browsers: ['last 2 versions']}}]]}}Copy the code
Installing a Dependency Module
npm install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --save
Copy the code
Then we write a simple service based on the Express module. ./src/server/index.js
var express = require('express');
var app = express();
const Home = require('.. /Components/Home');
app.get(The '*'.function(req, res) {
res.send(`<h1>hello</h1>`);
})
var server = app.listen(3000);
Copy the code
Run webpack using the webpack.server.js configuration file.
webpack --config webpack.server.js
Copy the code
After packaging, there will be a bundle.js in our directory, and this JS is the code that we generated as a package that will eventually run. We can use Node to run this file and start a 3000 port server. We can access this service by visiting 127.0.0.1:3000 and see the browser output Hello.
node ./build/bundile.js
Copy the code
The code above will be compiled using WebPack before we run it, so it supports ES Modules and does not force CommonJS.
src/components/Home/index.js
import React from 'react';
const Home = () = > {
return <div>home</div>
}
export default Home;
Copy the code
In/SRC /server/index.js, we can use the Home component. We need to install the React-dom, and use renderToString to convert the Home component into a label string.
import express from 'express';
import Home from '.. /Components/Home';
import React from 'react';
import { renderToString } from 'react-dom/server';
const app = express();
const content = renderToString(<Home />);
app.get(The '*'.function(req, res) {
res.send(`
<html>
<body>${content}</body>
</html>
`);
})
var server = app.listen(3000);
Copy the code
Run the node./build/bundile.js serviceCopy the code
The page now displays the code for our React component.
React server rendering is server-side rendering based on the virtual DOM, and server rendering makes the first screen rendering of a page much faster. However, server-side rendering also has drawbacks. Client-side rendering of React code is executed in the browser, which consumes performance on the user’s browser, whereas server-side rendering consumes performance on the server, because the React code runs on the server. It’s a huge drain on server performance, because React code is computationally expensive.
If your project is completely unnecessary to use SEO optimization and your project access speed has been very fast, it is recommended not to use SSR technology, because its cost is still relatively large.
In order to solve this problem, we need to do webpack automatic packaging and Node restart. We add the build command to package.json and use –watch to listen for file changes for automatic packaging.
{..."scripts": {
"build": "webpack --config webpack.server.js --watch"}... }Copy the code
Repackaging is not enough, we also need to restart the Node server, here we need to use the Nodemon module, here we use the global installation of Nodemon, add a start command in the package.json file to start our Node server. Node. /build/bundile.js/node. /build/bundile.js/node. /build/bundile.js/node. /build/bundile.js
{..."scripts": {
"start": "nodemon --watch build --exec node \"./build/bundile.js\""."build": "webpack --config webpack.server.js --watch"}... }Copy the code
When we start the server, we need to run the following commands in both Windows, since no more commands are allowed after build.
npm run build
npm run start
Copy the code
At this point the page will update automatically after we modify the code.
However, the above process is still a bit troublesome, we need two Windows to execute commands, we want one window to complete the execution of the two commands, we need to use a third-party module npm-run-all, which can be installed globally. Then modify it in package.json.
When we package and debug in the development environment, we create a dev command, which executes NPM -run-all, –parallel means parallel execution, and executes all the commands starting with dev:. I want to start the server and listen for file changes while running NPM run dev.
{..."scripts": {
"dev": "npm-run-all --parallel dev:**"."dev:start": "nodemon --watch build --exec node \"./build/bundile.js\""."dev:build": "webpack --config webpack.server.js --watch"}... }Copy the code
What is isomorphism
For example, in the following code, we bind a click event to a div and expect it to pop up when clicked. However, after running it, we will find that the event is not bound, because the server cannot bind the event.
src/components/Home/index.js
import React from 'react';
const Home = () = > {
return <div onClick={()= > { alert('click'); }}>home</div>
}
export default Home;
Copy the code
Normally we render the page and run the same code in the browser like a traditional React project so that the click event is present.
This gives rise to the concept of isomorphism, which I understand to be a set of React code that executes once on the server and again on the client.
Isomorphism can solve the problem of invalid click events. First, the server side performs once to display the page normally, and the client side performs once again to bind the event.
We can load an index.js file at page rendering time and use app.use to create a static file access path, so that the accessed index.js file will be requested in /public/index.js.
app.use(express.static('public'));
app.get('/'.function(req, res) {
res.send(`
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
})
Copy the code
public/index.js
console.log('public');
Copy the code
In this case, we can execute the React code once in the browser. We will create/SRC /client/index.js. Insert the code executed by the client. Here our isomorphic code uses hydrate instead of render.
import React from 'react';
import ReactDOM from 'react-dom';
import Home from '.. /Components/Home';
ReactDOM.hydrate(<Home />.document.getElementById('root'));
Copy the code
Then we also need to create a webpack.client.js file in the root directory. The import file is./ SRC /client/index.js, and the export file is public/index.js
const Path = require('path');
module.exports = {
mode: 'development'.entry: './src/client/index.js'.output: {
filename: 'index.js'.path: Path.resolve(__dirname, 'public')},module: {
rules: [{test: /.js? $/,
loader: 'babel-loader'.exclude: /node_modules/,
options: {
presets: ['react'.'stage-0'['env', {
targets: {
browsers: ['last 2 versions']}}]]}}Copy the code
Json file to add a package client directory command
{..."scripts": {
"dev": "npm-run-all --parallel dev:**"."dev:start": "nodemon --watch build --exec node \"./build/bundile.js\""."dev:build": "webpack --config webpack.server.js --watch"."dev:build": "webpack --config webpack.client.js --watch",}... }Copy the code
This will compile the client running file when we start up. The next time you visit the page, you can bind the event.
The webpack.server.js and webpack.client.js files have a lot of overlap. We can merge the contents using the webpack-merge plugin.
webpack.base.js
module.exports = {
module: {
rules: [{test: /.js? $/,
loader: 'babel-loader'.exclude: /node_modules/,
options: {
presets: ['react'.'stage-0'['env', {
targets: {
browsers: ['last 2 versions']}}]]}}Copy the code
webpack.server.js
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // The server running webPack needs to run NodeExternals, which is used to keep node modules such as Express from being packaged into JS.
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const serverConfig = {
target: 'node'.mode: 'development'.entry: './src/server/index.js'.output: {
filename: 'bundle.js'.path: Path.resolve(__dirname, 'build')},externals: [NodeExternals()],
}
module.exports = merge(config, serverConfig);
Copy the code
webpack.client.js
const Path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const clientConfig = {
mode: 'development'.entry: './src/client/index.js'.output: {
filename: 'index.js'.path: Path.resolve(__dirname, 'public')}};module.exports = merge(config, clientConfig);
Copy the code
SRC /server is where the server runs the code, SRC /client is where the browser runs the JS.
Routing in server-side rendering
First, the browser sends a request to the server. The server returns an empty HTML, and the browser requests js. After loading the JAVASCRIPT, the React code takes over the page execution process.
When we did the refactoring we needed to make way for the code to execute once on the browser and once on the server. The browser executed exactly the same process as before but on the server, we used the StaticRouter component instead of browserRouter for the browser.
npm install react-router-dom --save
Copy the code
Create SRC/routes.js to configure a route.
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './components/Home';
export default (
<div>
<Route path="/" exact component={Home}></Route>
</div>
);
Copy the code
When we do isomorphism we have to get out of the way and run it on the server and run it on the client.
SRC /client/index.js, use BrowserRouter to wrap Routes defined earlier, delete Home, because Home has been introduced in Routes.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '.. /Routes';
const App = () = > {
return (
<BrowserRouter>
{Routes}
</BrowserRouter>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
SRC /server/index.js, again using StaticRouter to render Routes. Home is already introduced in Routes so we don’t need to introduce it here. Context is what the StaticRouter is doing for data transfer, so let’s write an empty object.
StaticRouter doesn’t know what the request path is, because it’s running on the server, so it’s not as good as BrowserRouter. It needs to get the weight of the request and pass it to it. Here we need to put content in the request. Assign the value of location to req.path.
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.get(The '*'.function(req, res) {
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
<Routes />
</StaticRouter>
));
res.send(`
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`);
})
var server = app.listen(3000);
Copy the code
We add a page here to realize the multi-page route jump. We return a LOGIN component when the user accesses the login path.
src/Routes.js
import React from 'react';
import { Route } from 'react-router-dom';
import Home from './components/Home';
import Login from './components/Login';
export default (
<div>
<Route path="/" exact component={Home}></Route>
<Route path="/login" exact component={Login}></Route>
</div>
);
Copy the code
src/components/Login/index.js
import React from 'react';
const Login = () = > {
return <div>Login</div>
}
export default Login;
Copy the code
At this point the page opens and the Login route is loaded. Let’s clean up the code, create a new utils file and pull out the common methods.
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '.. /Routes';
export const render = (req) = > {
const content = renderToString((
<StaticRouter location={req.path} context={{}}>
<Routes />
</StaticRouter>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
SRC /server/index.js and use the render function.
import express from 'express';
import { render } from './utils';
const app = express();
app.use(express.static('public'));
app.get(The '*'.function(req, res) {
res.send(render(req));
})
var server = app.listen(3000);
Copy the code
With the code sorted out, we then use the Link tag to concatenate the entire routing workflow.
We create a common components SRC/components/headers/index, js
import React from 'react';
const Header = () = > {
return <div>header</div>
}
export default Header;
Copy the code
We will have a SRC/components/Home/index, js components the introduction of the Header.
import React from 'react';
import Header from '.. /Header';
const Home = () = > {
return <div>
<Header>
Home
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
export default Home;
Copy the code
src/components/Login/index.js
import React from 'react';
import Header from '.. /Header';
const Login = () = > {
return <div><Header />Login</div>
}
export default Login;
Copy the code
The page executes fine, then we introduce Link in the Header and use it to jump to Home and Login.
src/components/Header/index.js
import React from 'react';
import { Link } from 'react-router-dom';
const Header = () = > {
return <div>
<Link to="/">Home</Link>
<br />
<Link to="/login">Login</Link>
</div>
}
export default Header;
Copy the code
When we are doing page isomorphism, the server side rendering only releases when we enter the page for the first time, the jump using Link behind is the browser side jump, will not load the page resource file.
Therefore, not every page will be rendered on the server side, but only the first page will be rendered on the server side. The rest of the pages will still use the React routing mechanism, which we should pay attention to.
The middle layer
Middle tier is a term we hear a lot when we do server-side rendering. When we do server-side rendering, the browser requests the server, also known as Node-server, and the server returns the page data to the browser. However, when we do large projects, the acquisition of page content will involve database query or data calculation. Generally, when doing server-side rendering, the architecture layer will put database query or complex calculation in Java, c++ and other servers, rather than in node.
This is because Java or C++ has higher computational performance than Node.
One advantage of this architecture is that Java services only need to focus on getting the data and computing the data, while Node servers focus on generating the content of the page and are responsible for generating the page structure from the data retrieved from the Java server. So Node-Server is just an intermediate layer, responsible for assembling pages.
We know react computing is a performance drain, and when the traffic becomes too heavy for Nodes to handle, we can increase the number of Node servers to improve the load bottleneck. This architecture is very convenient online.
However, its disadvantages are also relatively obvious, increase the front-end complexity, when we are concerned about page rendering at the same time to maintain the server, concerned about the project architecture.
Isomorphic projects are introduced into Redux
If I want to use Redux in my project AND I need to use Redux on both the server and client side, we need to install Redux first. React-redux allows you to use redux in React. Redux-thunk is a middleware for Redux.
npm install redux react-redux redux-thunk --save
Copy the code
Next we open SRC /client/index.js. To get started with Redux, first introduce createStore to create a store. Those of you who are not familiar with Redux can refer to my previous article on Redux Design Patterns.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
const reducer = (state = { name: 'yd'}, action) = > {
return state;
}
const store = createStore(reducer);
const App = () = > {
return (
<Provider store={store}>
<BrowserRouter>
{Routes}
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
So our story will create over, we can again the SRC/components/Home/index, use redux in js.
import React from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
const Home = (props) = > {
return <div>
<Header>
<div>{props.name}</div>
<div>Home</div>
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
const mapStatetoProps = state => ({
name: state.name
});
export default connect(mapStatetoProps, null)(Home);
Copy the code
We also need to open SRC /server/utils.js to write store.
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import { createStore } from 'redux';
import { Provider } from 'react-redux';
export const render = (req) = > {
const reducer = (state = { name: 'yd'}, action) = > {
return state;
}
const store = createStore(reducer);
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<Routes />
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
At this point the page will be displayed. Redux has also been introduced to our pages.
We want to use some middleware with Redux, which we can demonstrate in SRC /server/utils.js.
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
export const render = (req) = > {
const reducer = (state = { name: 'yd'}, action) = > {
return state;
}
const store = createStore(reducer, applyMiddleware(thunk));
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<Routes />
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
SRC /client/index.js should also be added.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunk from 'redux-thunk';
const reducer = (state = { name: 'yd'}, action) = > {
return state;
}
const store = createStore(reducer, applyMiddleware(thunk));
const App = () = > {
return (
<Provider store={store}>
<BrowserRouter>
{Routes}
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
So here we see that both client and server use stores, so we can separate them out and not write them in every place.
Note here that in the Render method every user accesses a store, but on the server we only define the store once, not every time we call Render, so we share the store, which is not right, every user should have their own store. So we’re going to export a method to create a Store here, and create a Store where we use it.
src/store/index.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
const reducer = (state = { name: 'yd'}, action) = > {
return state;
}
const getStore = () = > {
return createStore(reducer, applyMiddleware(thunk));
}
export default getStore;
Copy the code
SRC /client/index.js should also be added.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import getStore from '.. /store'; / / use the store
import { Provider } from 'react-redux';
const App = () = > {
return (
<Provider store={getStore()}>
<BrowserRouter>
{Routes}
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import Routes from '.. /Routes';
import getStore from '.. /store'; / / use the store
import { Provider } from 'react-redux';
export const render = (req) = > {
const content = renderToString((
<Provider store={getStore()}>
<StaticRouter location={req.path} context={{}}>
<Routes />
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
Here, let’s formalize the structure of the Redux project code. We want the page to display a list when we visit the root directory.
First, we modify the Home page SRC/components/Home/store/reducer, js initialization to create some data and deal with the actions of data changes.
import { CHANGE_LIST } from './constants';
const defaultState = {
newsList: []}export default (state = defaultState, action) => {
switch (action.type) {
case CHANGE_LIST:
return {
...state,
newsList: action.list
};
default:
returnstate; }}Copy the code
Next, we came to the global store, which should make a combination of all reducer, we will modify. We introduced reducer in home
src/store/index.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer} from '.. /components/Home/store';
const reducer = combineReducers({
home: homeReducer
});
const getStore = () = > {
return createStore(reducer, applyMiddleware(thunk));
}
export default getStore;
Copy the code
src/components/Home/store/index.js
import reducer from './reducer';
export { reducer };
Copy the code
SRC/components/Home/index, js, we want to found in this file a request to display list, here we are transformed into a class type components.
With the dispatch capability, we send an asynchronous request in getHomeList. Here the dispatch needs to use the Action
import React, { Component } from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<Header>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
componentDidMount() {
this.props.getHomeList();
}
}
const mapStatetoProps = state => ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch => ({
getHomeList() {
dispatch(getHomeList());
}
})
export default connect(mapStatetoProps, mapDispatchToProps)(Home);
Copy the code
SRC/components/Home/store/actions. Js here we define a function, this function returns an object as an action may also return a function to do an asynchronous operation, this is the story – thunk of capacity.
Here we do asynchronous requests, here we use Axios, don’t forget to install.
import axios from 'axios';
import { CHANGE_LIST } from './constants';
const changeList = (list) = > {
type: CHANGE_LIST,
list
}
export const getHomeList = () = > {
return (dispatch) = > {
return axios.get('http://127.0.0.1:3000/getlist').then(res= > {
constlist = res.data.data; dispatch(changeList(list)); }}})Copy the code
SRC/components/Home/store/the js stored constants.
export const CHANGE_LIST = 'HOME/CHANGE_LIST';
Copy the code
So we have the structure of the Redux created, but we find that our project works fine but the structure of the page rendering does not have the structure of the list. And that’s because componentDidMount doesn’t execute when we run it on the server, so the list is empty, so the contents of the list aren’t being generated, and the list that we’re seeing is the list that the client is executing when the client is running, so the list is rendered by the client.
What we need to do on the server side is also call componentDidMount to get the data. Render the page structure.
SRC/components/Home/index, js, let’s go Home components to add a static method in the Home. The loadData, this function is responsible for the routing needs to be on the server before rendering data loading in advance.
import React, { Component } from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<Header>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>} componentDidMount() { this.props.getHomeList(); }} home. loadData = (store) => {// Execute action to expand store. return store.dispatch(getHomeList()); } const mapStatetoProps = state => ({ list: state.home.newsList }); const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList()); } }) export default connect(mapStatetoProps, mapDispatchToProps)(Home);Copy the code
ComponentDidMount is not executed on the server,
SRC /server/utils.js store is empty, we can make store get real data when rendering. Here we need to know which components are loaded by the current route. So we also need to modify the routing configuration to determine the loaded data according to the route origin.
LoadData is the method executed before the component is loaded. We’ll write it as home. loadData. The Login component doesn’t need to load any data, so we don’t need to define it.
src/Routes.js
import React from 'react';
import Home from './components/Home';
import Login from './components/Login';
export default[{path: '/'.component: Home,
exact: true.key: 'home'.loadData: Home.loadData
},
{
path: '/login'.component: Login,
key: 'login'.exact: true}]Copy the code
Because our routing mechanism has changed, we need to change where we use router.js. The Router is no longer a component but an array, so we need to change that.
SRC /client/index.js Notice that all routes need to be wrapped in div, otherwise an error will be reported. Because react-route-dom requires routes to appear in groups.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import routes from '.. /Routes';
import getStore from '.. /store'; / / use the store
import { Provider } from 'react-redux';
const App = () = > {
return (
<Provider store={getStore()}>
<BrowserRouter>
<div>
{
routes.map(route => (
<Route {. route} / >))}</div>
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
SRC /server/utils.js In addition to modifying routes, we also need to determine the current access path, and then put the corresponding data to be loaded in store in advance. We need to use the matchRoute method. It is provided by the react-router-config plug-in. MatchRoute matches second-level routes. MatchPath provided by react-router-dom matches only first-level routes.
Here our Render method needs to receive an extra RES for the send call back to the browser. Because the requesting function is asynchronous, it needs to return after the callback ends.
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import routes from '.. /Routes';
import getStore from '.. /store'; / / use the store
import { Provider } from 'react-redux';
import { matchRoute } from 'react-router-config';
export const render = (req, res) = > {
const store = getStore();
// You can take store and populate it with store.
// Add data to store based on route path, need to use matchRoute, return value is an array of matched routes for each level.
const matchedRoutes = matchRoute(routes, req,path);
// Make the loadData method of all components in matchRoutes run once
// item.route.loadData returns a promise. We wait for the promise to complete before we proceed, so use promise.all to request the response and return the data to the browser.
const promises = [];
matchedRoutes.forEach(item= > {
if(item.route.loadData) { promises.push(item.route.loadData(store)); }});Promise.all(promises).then(() = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{
routes.map(route => (
<Route {. route} / >))}</div>
</StaticRouter>
</Provider>
));
res.send(`
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`); })}Copy the code
SRC /server/index.js also needs to be modified. Pass in both req and RES, and return the response in the Render method.
import express from 'express';
import { render } from './utils';
const app = express();
app.use(express.static('public'));
app.get(The '*'.function(req, res) {
render(req, res)
})
var server = app.listen(3000);
Copy the code
Note that the browser automatically requests a Favicon file, causing code to repeat itself. We can fix this problem by adding this image to the public folder.
This allows us to visit the browser and see that the page structure has been rendered, and that it has been rendered by the server rather than the browser.
So let’s sort this code out.
SRC /server/index.js moves store creation here when a user request is received, keeping the Render function clean.
import express from 'express';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import getStore from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if(item.route.loadData) { promises.push(item.route.loadData(store)); }});Promise.all(promises).then(() = >{ res.send(render(store, routes, req)); })})var server = app.listen(3000);
Copy the code
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
export const render = (store, routes, req) = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{
routes.map(route => (
<Route {. route} / >))}</div>
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
Here’s a summary of the server-side rendering process.
When a user requests our web page, we first create an empty store, and then match the request path and routing items to determine what add-ons corresponding to the user’s current access path are. Therefore, the components to be displayed are placed in matchedRoutes. The matchedRoutes loop checks if the component has loadData. If it does, it needs to load some data, so it executes loadData as follows, placing the request in the Promise array, After the Promose of all components is executed, it indicates that the component dependency data to be displayed in this path is ready. Then, combining the displayed data with routing and request data, an HTML content is generated and returned to the user. This HTML contains all the information the user needs.
However, we have a problem here, when we load the page will flash a little, because this is when js execution will clear the page and then display the page. Because the client starts with an empty store, it doesn’t get data until the request is finished.
Here we need to use a concept called dewatering and waterflooding. We first go to SRC /server/utils.js, and when rendering the page we can add a script tag at the bottom of the page, in which we can write our rendered data.
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
export const render = (store, routes, req) = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{
routes.map(route => (
<Route {. route} / >))}</div>
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: The ${JSON.stringfiy(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
Open SRC /store/index.js and add a new method getClientStore.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer} from '.. /components/Home/store';
const reducer = combineReducers({
home: homeReducer
});
export const getStore = () = > {
return createStore(reducer, applyMiddleware(thunk));
}
export const getClientStore = () = > {
const defaultState = window.context.state;
// defaultState as the default value
return createStore(reducer, defaultState, applyMiddleware(thunk));
}
Copy the code
SRC /server/index.js Modifies the obtaining mode of store
import express from 'express';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import { getStore } from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if(item.route.loadData) { promises.push(item.route.loadData(store)); }});Promise.all(promises).then(() = >{ res.send(render(store, routes, req)); })})var server = app.listen(3000);
Copy the code
So SRC /client/index.js and I’m going to change getStore to getClientStore, client store and we’re going to use the store that the server gave me.
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import routes from '.. /Routes';
import { getClientStore } from '.. /store'; / / use the store
import { Provider } from 'react-redux';
const store = getClientStore();
const App = () = > {
return (
<Provider store={store}>
<BrowserRouter>
<div>
{
routes.map(route => (
<Route {. route} / >))}</div>
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
But can we get rid of the method that got the data in componentDidMount? No, because the component that was loaded when we routed the jump still needs to perform the change. As mentioned earlier, server rendering only loads the content of the first page, and not all the content of the later route loads are displayed.
We can determine if the data exists, and if it doesn’t, ask for it, and if it does, don’t ask.
src/components/Home/index.js
import React, { Component } from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<Header>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>} componentDidMount() { if (! this.props.list.length) { this.props.getHomeList(); }}} home. loadData = (store) => {// Execute action to expand store. return store.dispatch(getHomeList()); } const mapStatetoProps = state => ({ list: state.home.newsList }); const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList()); } }) export default connect(mapStatetoProps, mapDispatchToProps)(Home);Copy the code