Server rendering is often referred to as SSR. I believe many students have heard of this concept. Here, I would like to briefly talk about it for the sake of the concept alignment in front of the article.

  • Client-side rendering

In the field of front-end development in the past ten years, we have gradually moved to customer Rendering, namely CSR(Client Side Rendering). For students who are new to front-end Rendering, they may start with Client Rendering. Especially today with React, Vue, Angular. The user enters the URL address in the browser, the browser loads the HTML, JS, and CSS of the page, and draws the page layout through ajax data request, and finally displays the page. You can see that the first rendering of the overall layout is done on the client side. If the browser disables js execution of the page, you’ll notice that the HTML will be an empty body structure with no content.

Client-side rendering can be said to be the product of the separation of the front and back end, as well as the best practice of Ajax development, but it also has two problems. One is that SEO is not friendly, search engines will not execute JS when crawling pages, they will analyze the website through the HTML content they get. Obviously get empty bodyHTML without any content, there is no way to give site positioning ranking. The second problem is white screen time. The site initially presents a blank page to the user, and when the data is loaded, the page is redrawn. This loading process takes time, and depends on the speed of the interface and page rendering speed. If the interface is too slow, the screen goes blank for a long time.

  • Server side rendering

SSR(Server Side Rendering), which means that the Server renders the first screen of a page. Data acquisition and HTML construction are all placed in the Server, so that the client browser can directly obtain a complete structure of HTML. SEO can capture more information, the browser will also render faster, compare the data back from the web request directly render can avoid blank wait time.

Server-side rendering may be a new concept, but the principle behind it is actually quite old. This was the way it was developed in the early days of the Web, when the division of labor between the front and back ends was unclear. There must be code like this in some of today’s older systems.

The main server-side rendering frameworks are nuxt.js for Vue and next.js for React. This is not to introduce the use of these two frameworks, but to build a complete SET of SSR frameworks from scratch to get familiar with its underlying principles. Gives you a better idea of what server-side rendering is.

Since I am familiar with React, I will use React to demonstrate it.

Demonstration environment setup

Initialize a project and install react, Express, webpack, webpack- CLI, Webpack-node-externals install babel-loader babel-core babel-preset-react babel-stage-0 babel-preset-env.

NPM React, Express, webpack, webpack- CLI webpack-node-externals install babel-loader babel-core babel-preset-react babel-preset-stage-0 babel-preset-env --saveCopy the code
    1. Configuration webpack

Configure the webpack.server.js file after the installation is complete.

The Webpack here is running on the server side (node) and needs to add a key/value pair with target node.

As we all know, 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. Therefore, the package should tell webpack whether to pack server-side code or browser-side code, using the webpack-node-externals module.

/ SRC /server/index.js. The output file name 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
    1. Writing entry files

Then write a simple service based on the Express module. ./src/server/index.js

We created a Express service with port 3000 and introduced the React component Home, which was introduced to allow it to compile, but not render.

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
    1. Write the first component

Writing. The SRC/components/Home/index. The js components

import React from 'react';

const Home = () = > {
    return <div>home</div>
}

export default Home;
Copy the code
    1. Compile operation

As you can see from webpack.server.js, this js contains a React component, a express server with port 3000. When running webpack package, this js file has this function.

webpack --config webpack.server.js
Copy the code

After the package, a bundle.js file appears in the directory. This JS file is the code that the package generates and is finally ready to run. You can run this file using Node, which starts a server with port 3000. Access 127.0.0.1:3000 to access this service, and when you do, you will see the browser output Hello. This is the content of express server SEND.

node ./build/bundle.js
Copy the code
    1. Render the React component

/ SRC /server/index.js/Home/SRC /server/index.js/Home/SRC /server/index.js/SRC /server/index.js

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

Repack and run

# run the node. /build/bundle.js serviceCopy the code

The React component’s code is displayed.

    1. Configuring Startup Commands

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.

The above code needs to re-execute webpack packaging and restart the server after each change, which is too cumbersome to debug. To solve this problem, you need to do webpack automatic packaging and Node restart. Add the build command to package.json and automate packaging by listening for file changes with –watch.

{..."scripts": {
        "build": "webpack --config webpack.server.js --watch"}... }Copy the code

It is not enough just to repackage. You also need to restart the Node server. Here, you need to use the Nodemon module to install nodemon globally and add a start command in the package.json file to start the Node server. Node. /build/bundle.js/node. /build/bundle.js/node. /build/bundle.js

{..."scripts": {
        "start": "nodemon --watch build --exec node \"./build/bundle.js\""."build": "webpack --config webpack.server.js --watch"}... }Copy the code

To start the server, you need to run the following commands in both Windows, since you are not allowed to type any more commands after build.

npm run build
npm run start
Copy the code

Modify the code and the page will update automatically.

However, there is still some trouble in the above process, which requires two Windows to execute commands. If you want one window to execute two commands, you need to use a third-party module npm-run-all, which can be installed globally. Then modify it in package.json.

Packaging and debugging should be done in the development environment, where you need to create a dev command that runs NPM -run-all, –parallel, in parallel, to execute 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/bundle.js\""."dev:build": "webpack --config webpack.server.js --watch"}... }Copy the code

The project structure

    1. SSR isomorphism

Isomorphism refers to code reuse. That is, to achieve maximum code reuse on the client and server side, isomorphism can be simply understood as letting the server and client execute a set of code, rather than writing two sets of code for both ends.

For example, the following code binds a click event to a div and expects the click prompt to pop up when clicked. But when you run it, you see that the event is not bound, because the HTML is rendered on the server, and the server can’t bind events because binding events is dom behavior.

src/components/Home/index.js

import React from 'react';

const Home = () = > {
    return <div onClick={()= > { alert('click'); }}>home</div>
}

export default Home;
Copy the code

The usual approach is to render the page first, then run the same code in the browser as the traditional React project, so that the click event is generated.

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.

You 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 code.

console.log('public');
Copy the code

In this case, execute the React code once in the browser and create/SRC /client/index.js. Insert the code executed by the client. 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
    1. Client Configuration

You 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/bundle.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 at startup. The next time you visit the page, you can bind the event.

    1. Webpack finishing

The webpack.server.js and webpack.client.js files have a lot of overlap. Use the webpack-merge plugin to merge the contents.

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.

    1. Directory description
Package. json # project management file webpack.base.js # webpack Common part webpack.server.js # server webpack configuration Webpack.client. js # client Webpack configuration # Routes/index.js # Routes/index.js # Routes/index.js # build/ Public / # Front-end static folder, used to store the javascript code executed by the browserCopy the code

The server side renders the route

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.

The refactoring is done in a way that allows the code to execute both the browser and the server. The browser does exactly the same thing but the server does something different. Instead of browserRouter, we use the StaticRouter component.

    1. Configuring the Routing Module
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
    1. Configure the client code

SRC /client/index.js uses BrowserRouter to wrap the Routes defined earlier.

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
    1. Configure the server code

SRC /server/index.js, again using StaticRouter to render Routes. 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. 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
    1. Add a page component to test routing

Add a page to realize route redirecting from multiple pages. Returns 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

Clean up the code and create a new utils file to 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, using 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

Use the Link tag to concatenate the entire routing workflow. 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

SRC/components/Home/index. Js component the introduction of the Header component.

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

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 doing page isomorphism, the server side rendering will only be carried out when entering the page for the first time, and the subsequent jump using Link is the browser side jump, and will not load the resource file of the page.

Therefore, not every page is rendered on the server side, but only the first page is rendered on the server side. The other pages still use the React routing mechanism.

What is the middle layer?

Middle tier is a term you hear a lot when doing server-side rendering. When server-side rendering is done, the browser requests the server, known as Node-server, and the server returns the page data to the browser. However, when it comes to 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.

React computing takes a lot of performance, and nodes may be overwhelmed by traffic. In this case, you can increase the number of node servers to improve the load bottleneck. This architecture is very convenient online.

However, his disadvantages are also more obvious, increased front-end complexity, in the care of page rendering at the same time to maintain the server, concerned about the project architecture.

The introduction of story

If I want to use Redux in a project, I need to use Redux on both the server and client sides, and I need to install Redux first. React-redux allows developers to use Redux in React. Redux-thunk is a middleware for Redux.

npm install redux react-redux redux-thunk --save
Copy the code
    1. Client Configuration

SRC /client/index.js, which introduces 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

In SRC/components/Home/index. Js components used in the story.

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
    1. Server Configuration

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

Using some middleware with Redux can be demonstrated 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

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

Both client and server use stores, so you can separate them out and not write them all.

Note that in the Render method a store is used for every user visit, but on the server the store is defined only once, and not created every time Render is called, so the store is shared, which is not correct, and every user should have their own store. Here we export a method to create a Store in the used location.

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

Define the structure of the Redux project code. The page displays a list when you want to access the root directory.

First change homepage 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

Then in the global store, a reducer should be combined.

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, hope found in this file a request to display a list of here into a class type components.

With the dispatch capability, 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

Redux-thunk defines a function that can either return an object as an action or return a function to do an asynchronous operation, which is the power of redux-thunk.

src/components/Home/store/actions.js

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 the structure of redux is created, but we find that the project works fine but the structure of the page rendering does not have the structure of the list. This is because componentDidMount doesn’t execute when the server is running, so the list is empty, so the contents of the list are not being generated, and the list that the user sees is being rendered by the client when the client is running.

On the server side you also need to call componentDidMount to get the data. Render the page structure.

SRC/components/Home/index, js, a static method is added in the Home components 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, so let store fetch the real data when rendering. You need to know which components are being loaded by the current route. Therefore, we 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, written as home. loadData. The Login component doesn’t need to load any data, so you 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

Since the routing mechanism has changed, the use of router.js will be modified. The Router is no longer a component but an array and needs to be modified.

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, it is necessary to determine the current access path, and then place the corresponding data to be loaded in store in advance. MatchRoute method is needed. 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.

The Render method here 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 and waits until the promise completes, 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 repetitive code execution. You can fix this problem by adding this image to the public folder.

This way a visitor to the browser can see that the page structure has been rendered, and that it has been rendered by the server rather than the browser.

Let’s clean up this code.

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

Summarize the server-side rendering process.

When a user requests a web page, the server first creates an empty store, and then matches the request path with the routing item to determine what add-ons the user is currently accessing. Therefore, the component to be displayed is placed in the 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.

There is a problem, however, that when the page is loaded the page will flash a little bit, because this is when the 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. First, find SRC /server/utils.js. When rendering the page, we can add a script tag at the bottom of the page and write the rendered data in this tag.

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

Then open SRC /client/index.js and replace getStore with getClientStore. The client store will use the store given to me by the server.

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 does that mean that the method for getting data in componentDidMount can be removed? No, because the component loaded when the route jumps still needs to perform the change. As mentioned earlier, server rendering only loads the content of the first page, not all the content of the later route load is displayed.

You can determine whether the data exists, if it does not exist request, does not request.

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

Intermediate capacity

As mentioned earlier, When the browser and server communicate, Node serves as the middle layer for rendering the page, and the data is fetched from the real data server.

Let’s examine whether the previous code implements the concept of the middle tier. SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js: SRC /public/index.js

All you need to do is make Node-server a proxy server, which is a proxy function, assisted by an Express-HTTP-proxy package.

npm install express-http-proxy --save
Copy the code

SRC /server/index.js Modifies the obtaining mode of store

import express from 'express';
import proxy from 'express-http-proxy';
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.use('/api', proxy('xx.xx.xx.xx', {
    proxyReqPathResolver: (req) = > { // Which path to forward to
        return req.url;
    }
}))

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/components/Home/store/actions, js, delete the requested domain name.

import axios from 'axios';
import { CHANGE_LIST } from './constants';

const changeList = (list) = > {
    type: CHANGE_LIST,
    list
}

export const getHomeList = (server) = > {
    let url = ' ';
    if (server) { // The server environment uses the real address
        url = 'xx.xx.xx.xx/api/getlist'
    } else { // The browser environment uses relative addresses to do forwarding
        url = '/api/getlist'
    }
    return (dispatch) = > {
        return axios.get(url).then(res= > {
            constlist = res.data.data; dispatch(changeList(list)); }}})Copy the code

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(false)); } const mapStatetoProps = state => ({ list: state.home.newsList }); const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList(true)); } }) export default connect(mapStatetoProps, mapDispatchToProps)(Home);Copy the code

withExtraArgument

The above code is cumbersome to determine the request path by passing a Boolean value, so use withExtraArgument to clean it up.

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

src/components/Home/store/actions.js

import { CHANGE_LIST } from './constants';

const changeList = (list) = > {
    type: CHANGE_LIST,
    list
}

export const getHomeList = (server) = > {
    return (dispatch, getState, axiosInstance) = > {
        return axiosInstance.get(url).then(res= > {
            constlist = res.data.data; dispatch(changeList(list)); }}})Copy the code

src/store/index.js

import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer} from '.. /components/Home/store';
import clientAxios from '.. /client/request';
import serverAxios from '.. /server/request';

const reducer = combineReducers({
    home: homeReducer
});

export const getStore = () = > {
    return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}

export const getClientStore = () = > {
    const defaultState = window.context.state;
    // defaultState as the default value
    return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
Copy the code

src/client/request.js

import axios from 'axios';

const instance = axios.create({
    baseURL: '/'
})
Copy the code

src/server/request.js

import axios from 'axios';

const instance = axios.create({
    baseURL: 'xx.xx.xx.xx'
})
Copy the code

Rendering the routing

src/App.js

import React from 'react';
import Header from './component/Header';
import { renderRoutes } from 'react-router-config';

const App = (props) = > {
    return (<div>
        <Header />
        {renderRoutes(props.route.routes)}
    </div>)}export default App;
Copy the code

You want the App component to be displayed no matter how the user accesses it.

src/Routes.js

import React from 'react';
import App from './App';
import Home from './components/Home';
import Login from './components/Login';

export default [{
    path: '/'.component: App,
    routes: [{path: '/'.component: Home,
            exact: true.key: 'home'.loadData: Home.loadData
        },
        {
            path: '/login'.component: Login,
            key: 'login'.exact: true}}]]Copy the code

A secondary route is constructed to match App components when users access directories, and App and login components when users access /login paths.

src/server/utils.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';

export const render = (store, routes, req) = > {
    const content = renderToString((
        <Provider store={store}>
            <StaticRouter location={req.path} context={{}}>
                <div>
                {renderRoutes(routes)}
                </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

src/components/Home/index.js

import React, { Component } from 'react';
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>
            <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

src/components/Login/index.js

import React from 'react';

const Login = () = > {
    return <div>Login</div>
}

export default Login;
Copy the code

src/client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
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>
                    {renderRoutes(routes)}
                </div>
            </BrowserRouter>
        </Provider>
    )
}

ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code

Request Failure Handling

If the request in the action fails, a catch is fired instead of a THEN, which can cause the site to get stuck and not respond. Because the promise collection in server/index.js will fail, it will never return success.

Promise.all(promises).then(() = > {
    res.send(render(store, routes, req)); 
})
Copy the code

So you can put a catch in there.

Promise.all(promises).then(() = > {
    res.send(render(store, routes, req)); 
}).catch(() = > {
    res.end('sorry');
})
Copy the code

This allows the page to be displayed, but the problem is that we don’t know what went wrong, or when multiple components are rendered, we expect the ones that interface properly to return normally.

You can wrap a new Promise around loadData and call resolve whether loadData succeeds or fails to ensure that all requests are completed. Promise.all can be executed normally.

src/server/index.js

import express from 'express';
import proxy from 'express-http-proxy';
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.use('/api', proxy('xx.xx.xx.xx', {
    proxyReqPathResolver: (req) = > { // Which path to forward to
        return req.url;
    }
}))

app.get(The '*'.function(req, res) {
    const store = getStore();
    const matchedRoutes = matchRoute(routes, req,path);
    const promises = [];
    matchedRoutes.forEach(item= > {
        if (item.route.loadData) {
            const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = >{ res.send(render(store, routes, req)); })})var server = app.listen(3000);
Copy the code

How do YOU support CSS styling

First you need WebPack to compile your CSS files.

The webpack.server.js server uses isomorphic-style-loader to replace the style-loader of the client.

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: {
        rules: [{test: /\.css? $/,
                use: ['isomorphic-style-loader', {
                    loader: 'css-loader'.options: {
                        importLoaders: 1.modules: true.localIdentName: '[name]_[local]_[hase:base64:5]'}}]}}module.exports = merge(config, serverConfig);
Copy the code

The webpack.client.js client is loaded using style-loader.

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: {
        rules: [{test: /\.css? $/,
                use: ['style-loader', {
                    loader: 'css-loader'.options: {
                        importLoaders: 1.modules: true.localIdentName: '[name]_[local]_[hase:base64:5]'}}]}};module.exports = merge(config, clientConfig);
Copy the code

src/components/Home/style.css

body {
    background: green;
}
.test {
    background: red;
}
Copy the code

src/components/Home/index.js


import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';

class Home extends Component {
    componentWillMount() { // Handle styles
        if (this.props.staticContext) { // Server running exists, client running does not exist. So don't do it on the client. Store the style in context.
            this.props.staticContext.css = styles._getCss(); }}getList() {
        const { list } = this.props;
        return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
        return <div className={styles.test}>
            <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

SRC /server/index.js handles the style in the render method.

import express from 'express';
import proxy from 'express-http-proxy';
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.use('/api', proxy('xx.xx.xx.xx', {
    proxyReqPathResolver: (req) = > { // Which path to forward to
        return req.url;
    }
}))

app.get(The '*'.function(req, res) {
    const store = getStore();
    const matchedRoutes = matchRoute(routes, req,path);
    const promises = [];
    matchedRoutes.forEach(item= > {
        if (item.route.loadData) {
            const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = > {
        consthtml = render(store, routes, req, context) res.send(html); })})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 { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';

export const render = (store, routes, req, context) = > {
    const content = renderToString((
        <Provider store={store}>
            <StaticRouter location={req.path} context={{}}>
                <div>
                {renderRoutes(routes)}
                </div>
            </StaticRouter>
        </Provider>
    ));

    const cssStr = context.css ? context.css : ' ';
    return `
        <html>
            <head>
                <style>${cssStr}</style>
            </head>
            <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

How the styles of multiple components are integrated. You can use an array to store CSS styles.

src/server/utils.js

import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';

export const render = (store, routes, req, context) = > {
    const content = renderToString((
        <Provider store={store}>
            <StaticRouter location={req.path} context={{}}>
                <div>
                {renderRoutes(routes)}
                </div>
            </StaticRouter>
        </Provider>
    ));

    const cssStr = context.css.length ? context.css.join('\n') : ' ';
    return `
        <html>
            <head>
                <style>${cssStr}</style>
            </head>
            <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

src/components/Home/index.js


import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';

class Home extends Component {
    componentWillMount() { // Handle styles
        if (this.props.staticContext) { // Server running exists, client running does not exist. So don't do it on the client. Store the style in context.
            this.props.staticContext.css.push(styles._getCss()); }}getList() {
        const { list } = this.props;
        return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
        return <div className={styles.test}>
            <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

SRC /server/index.js handles the style in the render method.

import express from 'express';
import proxy from 'express-http-proxy';
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.use('/api', proxy('xx.xx.xx.xx', {
    proxyReqPathResolver: (req) = > { // Which path to forward to
        return req.url;
    }
}))

app.get(The '*'.function(req, res) {
    const store = getStore();
    const matchedRoutes = matchRoute(routes, req,path);
    const promises = [];
    matchedRoutes.forEach(item= > {
        if (item.route.loadData) {
            const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = > {
        const context = { css: []};consthtml = render(store, routes, req, context) res.send(html); })})var server = app.listen(3000);
Copy the code

There’s actually a problem with the code above. A loadData method is mounted on the Home component, but the Home file exports not the Home component, but the connect wrapped component, so it exports another component. Fortunately, connect analyzes the properties of the original component and mounts them to the current output, so you can still call the loadData method when you use the Home component later. This is not a good idea, however, and it is best to declare it directly to avoid confusing code usage.

Mount loadData to ExportHome.

src/components/Home/index.js

.// Home.loadData = (store) => {
// // Run action to expand the store.
// return store.dispatch(getHomeList());
// }

const mapStatetoProps = state= > ({
    list: state.home.newsList
});

const mapDispatchToProps = dispatch= > ({
    getHomeList(){ dispatch(getHomeList()); }})const ExportHome = connect(mapStatetoProps, mapDispatchToProps)(Home);

ExportHome.loadData = (store) = > {
    // Execute action to expand store.
    return store.dispatch(getHomeList());
}

export default ExportHome
Copy the code

Simplify code with higher-order components

The above styles are too cumbersome to write. You need to use the componentWillMount life cycle first, and then inject its styles into the Context. So each component needs a piece of code like this. This is not a reasonable design. You can tidy it up. Use higher-order components.

SRC/withstyle.js creates the higher-order component function. This function returns a component. In fact, this function is a function that generates higher-order components, and the returned components are called higher-order components, and their job is to render the pre-push style.

This function accepts the style file styles because the component doesn’t know where the styles are. You also receive the DecoratedComponent that was originally to be rendered, render it in the higher-order component, and pass in the parameters.

import React, { Component } from 'react';

export default (DecoratedComponent, styles) => {
    return class NewComponent extends Component {
        componentWillMount() {
            if (this.props.staticContext) {
                this.props.staticContext.css.push(styles._getCss()); }}render() {
            return <DecoratedComponent {. this.props} / >}}}Copy the code

Now that the higher-order components are written, we can transform the Home component.

SRC/components/Home/index, js, here can delete their own componentWillMount, introducing the withStyle function, and then at the bottom of the export use wrapped in withStyle Home components, when we pass styles style is ok. withStyle(Home, styles);


import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';
import withStyle from '.. /.. /withStyle';

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 className={styles.test}>
            <div>Home</div>
            {this.getList()}
            <button onClick={()= >{ alert('click1'); } > button</button>
        </div>
    }

    componentDidMount() {
        if (!this.props.list.length) {
            this.props.getHomeList(); }}}const mapStatetoProps = state= > ({
    list: state.home.newsList
});

const mapDispatchToProps = dispatch= > ({
    getHomeList(){ dispatch(getHomeList()); }})const ExportHome = connect(mapStatetoProps, mapDispatchToProps)(withStyle(Home, styles));

ExportHome.loadData = (store) = > {
    // Execute action to expand store.
    return store.dispatch(getHomeList());
}

export default ExportHome
Copy the code

Add SEO optimization

SEO optimization is also called search engine optimization.

Title and description are rarely useful for SEARCH engine optimization, they are just descriptions of the site. Baidu’s search classifies websites by matching them with all their text content. Therefore, in many cases, the websites searched are consistent with the required content, but the titile website searched does not contain the keyword searched.

A website is composed of text, multimedia, link three parts.

In today’s Internet, content needs to be original, original works will get more traffic, SEO will analyze the originality of content. So text can add originality.

Link to the content of the site and the content of the current site to be relevant, the stronger the relevance of SEO weight is higher.

Multimedia also needs to be original.

The React – Helmet components

React-helmet lets you customize your page’s title and meta

import React, { Component, Fragment } from 'react';
import { Helmet } from 'react-helmet';

class Home extends Component {
    render() {
        return <Fragment>
            <Helmet>
                <title>This is the title of the Helmet definition</title>
                <meta name="description" content="This is the description of what Helmet is." />
            </Helmet>
            <div>Home</div>
            {this.getList()}
            <button onClick={()= >{ alert('click1'); } > button</button>
        </Fragment>}}Copy the code

The above code is just client-side rendering, the server short rendering is a little different, but it’s also easy to modify utils.js

src/server/utils.js

.import { Helmet } from 'react-helmet';

export const render = (store, routes, req, context) = >{...const helmet = Helmet.renderStatic();
    return `
        <html>
            <head>
                ${helmet.title.toString()}
                ${helmet.meta.toString()}
                <style>${cssStr}</style>
            </head>
            <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

It is not recommended that you use the above code directly in the production environment. It is just an analysis of the principle. If the project needs you to choose a mature server rendering framework, after all, it is cheaper to learn and use.

Finally, server rendering is not recommended

The server rendering cost is high, so it is not recommended to render the server if it is not necessary. If it is only for SEO optimization, you can change the idea. Set up a pre-render server.

You can put a layer of Nginx on top of your website and forward the request to a pre-render server if it’s a spider, or to a real server if it’s a user.

The prerender module provides the ability to run a mini-browser when accessing it, load the web content and respond to the request so that the request gets the structure of the page.

const prerender = require('prerender');
const server = prerender({
    port: 8000
});

server.start();
Copy the code

The page element exists in the url content that is accessed.

localhost:8000/render? url=http://localhost:3000/Copy the code