The topic

A few years ago, jquery was a must-have skill for a front-end engineer. At the time, many companies were using Java with a template engine like Velocity or Freemarker, leaving the page rendering to the server and the front end to write the interaction and business logic in jquery. But with the rise of frameworks like React and Vue, this server-side rendering mode combined with template engines was gradually abandoned in favor of client-side rendering. The immediate benefits of this are reduced server stress and a better user experience, especially during page switching, where the client rendering experience is much better than the server rendering. But over time, seo on client rendering was found to be very poor, and the first screen rendering time was too long. And those are the advantages of server-side rendering, a return to the days of template engines? History doesn’t go backwards, so people started trying to render React or Vue components on the server side, and eventually nuxtJS, NextJS and other server rendering frameworks came into being. The main purpose of this article is not to introduce these server rendering frameworks, but to introduce their ideas, and to build a server rendering project without using them. Here we use React as an example.

Making address:

  • Example code for this article: github.com/ruichengpin…
  • Full server render project: github.com/ruichengpin…

Easy React server rendering

renderToString

React-dom render () {render (); render (); render ();

import {render} from 'react-dom';
import App from './app';
render(<App/>,document.getElementById("root"));
Copy the code

Render is only a part of the React dom. RenderToString is a function of the React dom. RenderToString is a function of the React dom.

Render a React element to its initial HTML. React will return an HTML string. You can use this method to generate HTML on the server and send the markup down on the initial request for faster page loads and to allow search engines to crawl your pages for SEO purposes.

React renders the React element as its initial Html and returns an Html string. Generate HTML on the server. Generate HTML on the server. Let’s see if we can do that.

const express = require('express');
const app = express();
const React = require('react');
const {renderToString} = require('react-dom/server');
const App = class extends React.PureComponent{
  render() {return React.createElement("h1",null,"Hello World");; }}; app.get('/'.function(req,res){
  const content = renderToString(React.createElement(App));
  res.send(content);
});
app.listen(3000);
Copy the code

To do the logic, first define an App component that returns a simple “Hello World”. Then call createElement to generate a React element. RenderToString is called to generate an HTML string based on the React element and is returned to the browser. The expected effect is that the page will say “Hello World”, so let’s verify that.

The Denver nuggets

webpack

From the above example, we found several problems:

  • Cannot JSX syntax
  • You can only use the commonJS modular specification, not esModule

Therefore, we need to do a Webpack operation on our server code. The server webpack configuration needs to pay attention to these points:

  • Be sure to include the target:”node” configuration item
  • Make sure you have the webpack-node-externals library

So your WebPack configuration must look something like this:

const nodeExternals = require('webpack-node-externals'); . module.exports = { ... target:'node'Externals: [nodeExternals()],// Externals: [nodeExternals()],// externals: [node_modules]... };Copy the code

homogeneous

Let’s tweak the previous example slightly to look like this:

const express = require('express');
const app = express();
const React = require('react');
const {renderToString} = require('react-dom/server');
const App = class extends React.PureComponent{
  handleClick=(e)=>{
    alert(e.target.innerHTML);
  }
  render() {return <h1 onClick={this.handleClick}>Hello World!</h1>;
  }
};
app.get('/'.function(req,res){
  const content = renderToString(<App/>);
  console.log(content);
  res.send(content);
});
app.listen(3000);
Copy the code

We attach a click event to the h1 tag, and the event response is simple enough to pop the contents of the H1 tag. “Hello World! , we perform a run. If you do, you’ll notice that no matter how much you click, “Hello World!” will not pop up. . Why is that? RenderToString simply returns an HTML string, and the js interaction logic for the element is not returned to the browser, so clicking on the H1 tag does not respond. How to solve this problem? Before we talk about solutions, let’s talk about isomorphism. What is “isomorphism” is simply “different forms of the same structure”. React isomorphism React isomorphism

The same React code is executed on the server and again on the client.

After executing the same React code on the server, we can generate the corresponding HTML. After the client executes it once, it can respond to the user’s actions. This makes a complete page. So we need additional entry files to package the JS interaction code that the client needs.

import React from 'react';
import {render} from 'react-dom';
import App from './app';
render(<App/>,document.getElementById("root"));
Copy the code

This is just like writing the client rendering code, wrapping it with webpack, and adding references to the packed JS in the rendered HTML string.

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import App from  './src/app';
const app = express();

app.use(express.static("dist"))

app.get('/'.function(req,res){ const content = renderToString(<App/>); res.send(` <! doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div>
                <script src="/client/index.js"></script>
            </body> 
        </html>
    `);
});
app.listen(3000);
Copy the code

Here “/client/index.js” is the js file we packaged for the client to execute, and then we look at the effect, now the page can respond to our operation.

But the console throws a warning like this:

Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.

Remind us to use reactdom.render () instead of reactdom.render () for server render, and warn us that we won’t be able to use reactdom.render () to mix server rendered tags with react17.

The difference between reactdom.hydrate () and reactdom.render () is:

Reactdom.render () clears out all children of the mounted DOM node and regenerates them. Reactdom.hydrate () reuses the children of the mounted DOM node and relates them to the React virtualDom.

The difference between the two shows that reactdom.render () overturns everything the server does, whereas reactdom.hydrate () does something deeper on top of what the server does. Clearly reactdom.hydrate () is better than reactdom.render () at this point. Reactdom.render () at this point just makes us look stupid and do a lot of useless work. So let’s adjust the client entry file.

import React from 'react';
import {hydrate} from 'react-dom';
import App from './app';
hydrate(<App/>,document.getElementById("root"));
Copy the code

The flow chart

Join the routing

Now that we’ve covered the principles of server-side rendering in a large part of the previous section, and figured out the general process of server-side rendering, we’re going to talk about how to route it. On the server side, Html is generated, and different access paths correspond to different components. Therefore, the server side needs a routing layer to help us find the React component corresponding to the access path. Second, the client does a hydrate operation, which also needs to match the react component based on the access path. To sum up, both the server and the client need to be represented by routing. There is no need to elaborate on the client route, but let’s look at how the server adds the route.

StaticRouter

In client-side rendering projects, the React-Router provides BrowserRouter and HashRouter for us to choose from. The common feature of these two routers is that they need to read the URL of the address bar, but there is no address bar in the server environment. If there is no window.location object, BrowserRouter and HashRouter cannot be used in the server environment. The react-Router provides another router called StaticRouter. The react-Router website describes it as a StaticRouter.

This can be useful inServer-side rendering and rendering scenarios When the user isn't actually drinking around, so the location never actually changes.Copy the code

This is designed to be useful in server-side rendering scenarios. Next we’ll combine StaticRouter to implement this. Now we have two pages Login and User, which look like this:

Login:

import React from 'react';
export default class Login extends React.PureComponent{
  render() {return</div>}}Copy the code

User:

import React from 'react';
export default class User extends React.PureComponent{
  render() {returnUser </div>}}Copy the code

Server code:

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {StaticRouter,Route} from 'react-router';
import Login from '@/pages/login';
import User from '@/pages/user';
const app = express();
app.use(express.static("dist"))
app.get(The '*'.function(req,res){
    const content = renderToString(<div>
    <StaticRouter location={req.url}>
      <Route exact path="/user" component={User}></Route>
      <Route exact path="/login"component={Login}></Route> </StaticRouter> </div>); res.send(` <! doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div>
                <script src="/client/index.js"></script>
            </body>
        </html>
    `);
});
app.listen(3000);
Copy the code

Final effect:

/ user:

/ login:

The routes at the front and back ends are isomorphic

Through the above small experiment, we have mastered how to add routes on the server side, the next thing we need to deal with is the isomorphism of the front and back end routes. Since the front and back ends need to add routes, if both ends write a route separately, it will take time and effort to say nothing, and maintenance is very inconvenient. Therefore, we hope that the same set of routes can run at both ends. So let’s implement that.

Train of thought

Here first say the idea, first of all, we certainly will not write a route in the server side, and then write a route in the client side, so time-consuming and laborious not to say, and not easy to maintain. Most people like me would like to write less code, and the first rule of writing less code is to get rid of common parts. Then we can look for generic part, look carefully is not hard to find whether the service side routing or client routing, the relationship between the path and the component is constant, a path corresponds to a component, b path corresponding to the b component, so here we hope that the relationship between the path and the component can use the abstract language to describe clearly, also is what we call routing configuration. Finally, we provide a converter that can be converted into a server or client route according to our needs.

code

routeConf.js

import Login from '@/pages/login';
import User from '@/pages/user';
import NotFound from '@/pages/notFound';

export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'}, {type:'route',
  path:'/user',
  exact:true,
  component:User
},{
  type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:The '*',
  component:NotFound
}]
Copy the code

The router generator

import React from 'react';
import { createBrowserHistory } from "history";
import {Route,Router,StaticRouter,Redirect,Switch} from 'react-router';
import routeConf from  './routeConf';

const routes = routeConf.map((conf,index)=>{
  const {type. otherConf} = conf;if(type= = ='redirect') {return<Redirect key={index} {... otherConf}/>; }else if(type= = ='route') {return <Route  key={index} {...otherConf}></Route>;
  }
});

export const createRouter = (type)=>(params)=>{
  if(type= = ='client'){
    const history = createBrowserHistory();
    return <Router history= {history}>
      <Switch>
        {routes}
      </Switch>
    </Router>
  }else if(type= = ='server'){
    const {location} = params;
    return<StaticRouter {... params}> <Switch> {routes} </Switch> </StaticRouter> } }Copy the code

The client

createRouter('client') ()Copy the code

The service side

const context = {};
createRouter('server')({location:req.url,context}) //req.url comes from the Node serviceCopy the code

Here is only the implementation of single-layer routing, in the actual project we will use more nested routines, but the principle of the two is the same, nested routines by the words, friends can privately implement oh!

Redirection problem

Problem description

After the isomorphism, we found a small problem, although when the URL is “/”, the route is redirected to “/user”, but when we open the console, we will find that the returned content is inconsistent with the browser display content. Therefore, we can conclude that this redirection should be done for us by the client route, but this is problematic, we should want to have two requests, one request response status code 302, telling the browser to redirect to “/user”, and the other browser requests the resource under “/user”. So on the server side we need to make a change.

Modified code

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {createRouter} from '@/router';

const app = express();
app.use(express.static("dist"))
app.get(The '*'.function(req,res){
  const context = {};
  const content = renderToString(<div>
    {createRouter('server')({ location:req.url, context })} </div>); /** * ------ focus start */ / when Redirect is used, context.url will contain the Redirect addressif(context.url){
    //302
    res.redirect(context.url);
  }else{ //200 res.send(` <! doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div>
                <script src="/client/index.js"></script> </body> </html> `); } /** * ------ key end */}); app.listen(3000);Copy the code

Here we just add a layer of judgment to check if context.url exists and redirect to context.url if it does, otherwise render normally. As for the reason of this judgment, this is the official document provided by the react-router to determine whether there is a redirection method, interested friends can have a look at the document and source code. The document address is as follows:

Reacttraining.com/react-route…

404 problem

Although I configured the 404 page in routeconf.js, there is a problem. Let’s take a look at this diagram.

The service side

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {createRouter} from '@/router';

const app = express();
app.use(express.static("dist"))
app.get(The '*'.function(req,res){
  const context = {};
  const content = renderToString(<div>
    {createRouter('server')({ location:req.url, context })} </div>); // When Redirect is used, context.url will contain the Redirect addressif(context.url){
    //302
    res.redirect(context.url);
  }else{
    if(context.NOT_FOUND) res.status(404); 404 res.send(' <! doctype html> <html> <title>ssr</title> <body> <div id="root">${content}</div>
                <script src="/client/index.js"></script> </body> </html> `); }}); app.listen(3000);Copy the code

The main thing is to add this line of code that determines whether or not to set the status code to 404 based on whether context.not_found is true.

 if(context.NOT_FOUND) res.status(404); // Determine whether to set the status code to 404Copy the code

routeConf.js

import Login from '@/pages/login';
import User from '@/pages/user';
import NotFound from '@/pages/notFound';


export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'}, {type:'route',
  path:'/user',
  exact:true,
  component:User,
  loadData:User.loadData
},{
  type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:The '*'Render :({staticContext})=>{if (staticContext) staticContext.NOT_FOUND = true;
    return <NotFound/>
  }
}]
Copy the code

I chose not to modify the NotFound component directly on its constructor lifecycle here, considering that the 404 component may be used later on for projects rendered by other clients, keeping the component as generic as possible.

Component :NotFound // Render :({staticContext})=>{if (staticContext) staticContext.NOT_FOUND = true;
    return <NotFound/>
}
Copy the code

Join the story

For those of you who are new to server-side rendering, it may be hard to understand why there is a separate section on how to integrate Redux into a server-side rendering project. Redux is a javaScript state container. The server does not need to render to generate HTML. In other words, it is client-side stuff. Why do we have to talk about it separately?

Here I’ll explain to the nuggets article details page, for example, suppose the nuggets is a rendering of a service project, then each article page source should be to include the full contents of the article, it also means that the interface is the request on the server to request before rendering HTML, rather than on the client side to take over the page and then to request, So once the server gets the requested data, it has to find a place to use it for subsequent HTML rendering. Let’s take a look at how the client gets the request data and store it. Generally there are two ways, one is the component state and the other is redux. Let’s go back to rendering on the server side and look at these two methods one by one. First, we put them in the state of the component, which is obviously not acceptable. RenderToString is not available yet. The second parameter to redux’s createStore is used to pass in the initialization state. We can use this method to implement data injection, as shown in the following flowchart.

The basic framework

Let’s start with the basic directory structure:

A redux folder under the user page contains these three files:

  • Actions. Js (collection redux-thunk encapsulates dispatch in functions to solve action type memory problems)
  • Actiontypes.js (Actions needed by reducer)
  • Reducer. Js (regular Reducer file)

Some people might like the structure of putting redux-related items into a folder and grouping them into several categories. But I’m a big fan of putting Redux stuff next to the page because it’s easier to find and maintain. It can be done according to personal preference, but I’m just offering one of my ways.

actions.js

import {CHANGE_USERS} from './actionTypes';
export const changeUsers = (users)=>(dispatch,getState)=>{
  dispatch({
    type:CHANGE_USERS,
    payload:users
  });
}
Copy the code

actionTypes.js

export const CHANGE_USERS = 'CHANGE_USERS';

Copy the code

reducer.js

import {CHANGE_USERS} from  './actionTypes';
const initialState = {
  users:[]
}
export default (state = initialState, action)=>{
  const {type,payload} = action;
  switch (type) {
    case CHANGE_USERS:
      return {
        ...state,
        users:payload
      }
    default:
      returnstate; }}Copy the code

/store/index.js This file is a reducer to do a reducer integration, exposed to create store methods.

import { createStore, applyMiddleware,combineReducers } from 'redux';
import thunk from 'redux-thunk';
import user from '@/pages/user/redux/reducer';

const rootReducer = combineReducers({
  user
});
export default () => {
  return createStore(rootReducer,applyMiddleware(thunk))
};
Copy the code

As for why not expose store directly, the reason is simple. The main reason is that since this is a singleton pattern, if the server makes a change to the store’s data, the change will always be retained. Simply put, user A’s access will affect user B’s access, so it is not advisable to expose the Store directly.

Data acquisition and injection

Routing modification

routeConf.js

export default [{
  type:'redirect',
  exact:true,
  from:'/',
  to:'/user'}, {type:'route',
  path:'/user',
  exact:true, Component :User, loadData: user. loadData // server to get data function},{type:'route',
  path:'/login',
  exact:true,
  component:Login
},{
  type:'route',
  path:The '*',
  component:NotFound
}]
Copy the code

Routeconf.js is a loadData method that is used by the server to obtain data. The implementation of user-loadData method will be discussed later. Routeconf.js is a loadData method that is used by the server to obtain data. See here have a friend may have such doubt, will be paid by component User components, get the User components can not get the loadData method? Why do you need a separate loadData? Take User as an example. First of all, you may not use component or render. Second, the corresponding component of component may not be User. It is possible to wrap the User in a library like react-loadable to form an asynchronous load component so that the user.loadData method cannot be retrieved through the component. Given the uncertainty of the Component, it’s safer to have a separate loadData.

The page transformation

In route modification, we mentioned that we need to add a loadData method, so let’s implement it.

import React from 'react';
import {Link} from 'react-router-dom';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import axios from 'axios';
import * as actions from './redux/actions';
@connect(
  state=>state.user,
  dispatch=>bindActionCreators(actions,dispatch) ) class User extends React.PureComponent{ static loadData=(store)=>{ // Axios itself is wrapped around a Promise, so axios.get() returns a Promise objectreturn  axios.get('http://localhost:3000/api/users').then((response)=>{
      const {data} = response;
      const {changeUsers} = bindActionCreators(actions,store.dispatch);
      changeUsers(data);
    });
  }
  render(){
    const {users} = this.props;
    returnThe < div > < table > < thead > < tr > < th > name < / th > < th > height < / th > < th > birthday < / th > < / tr > < thead > < tbody > {users. The map ((user) = > {const {name,birthday,height} = user;return <tr key={name}>
                    <td>{name}</td>
                    <td>{birthday}</td>
                    <td>{height}</td>
                  </tr>
                })
             }
           </tbody>
        </table>
    </div>
  }
}

export default User;
Copy the code

The Render section is simple, simply showing a table with three columns: name, height, and date of birth. The data in the table is from the users of props. After accessing the data through the interface, change the value of the users of props using the changeUsers method. Let’s just say that for the sake of understanding). This is the main logic of the entire page, so let’s focus on loadData.

LoadData must have an argument that accepts store and must return a Promise object

There must be a parameter to accept store, which is easier to understand. According to the flowchart drawn above, we need to modify the value of state in store. Without store, it is impossible to modify the value of state. The Promise object is returned mainly because javascript is asynchronous, but we need to wait for the data request to complete before rendering the React component to generate HTML. Promise is much better than callback. There are a lot of articles on the web that compare promises and callback, but they are not discussed here. Apart from the fact that promises handle asynchrony better than callback, the main point, and the fatal flaw of callback, is that in nested cases we need to call loadData for multiple components, as in the following example.

import React from 'react';
import {Switch,Route} from 'react-router';
import Header from '@/component/header';
import Footer from '@/component/footer';
import User from '@/pages/User';
import Login from '@/pages/Login'; Class App extends React.PureComponent{static loadData = ()=>{// Request data}render(){
        const {menus,data} = this.props;
        return <div>
            <Header menus={menus}/>
                <Switch>
                   <Route path="/user" exact component={User}/>
                   <Route path="/login" exact component={Login}/>
                </Switch>
            <Footer data={data}/>
        </div>
    }
}
Copy the code

When the path is /user, we call not only user.loadData, but also app.loadData. With promises, we can easily solve the completion response problem of multiple asynchronous tasks with promise.all, whereas with callback, it becomes very complicated.

There must be a store.dispatch step

This is actually a bit easier to understand, changing the value of store state can only be done by calling store.dispatch. But in this case you can call store.dispatch directly, or I can use redux-thunk middleware with a third party. In this case, I’m using Redux-Thunk, which encapsulates the store.dispatch operation in changeUsers.

import {CHANGE_USERS} from './actionTypes';
export const changeUsers = (users)=>(dispatch,getState)=>{
  dispatch({
    type:CHANGE_USERS,
    payload:users
  });
}
Copy the code

Server Transformation

import express from 'express';
import React from 'react';
import {renderToString} from 'react-dom/server';
import {Provider} from 'react-redux';
import {createRouter,routeConfs} from '@/router';
import { matchPath } from "react-router-dom";
import getStore from '@/store';
const app = express();
app.use(express.static("dist"))
app.get('/api/users'.function(req,res){
  res.send([{
    "name":"Yoshizawa Mingbu"."birthday":"1984-03-03"."height":"161"}, {"name":"The bridge has not been long."."birthday":"1987-12-24"."height":"158"}, {"name":"Xiangchengyou"."birthday":"1988-08-04"."height":"158"}, {"name":"Emaria"."birthday":"1996-02-22"."height":"165"
  }]);
});
app.get(The '*'.function(req,res){
  const context = {};
  const store =  getStore();
  const promises = [];
  routeConfs.forEach((route)=> {
    const match = matchPath(req.path, route);
    if(match&&route.loadData){
      promises.push(route.loadData(store));
    };
  });
  Promise.all(promises).then(()=>{
    const content = renderToString(<Provider store={store}>
      {createRouter('server')({
          location:req.url,
          context
      })}
    </Provider>);
    if(context.url){
      res.redirect(context.url);
    }else{
      res.send(`
            <html>
                <head>
                    <title>ssr</title>
                    <script>
                        window.INITIAL_STATE = ${JSON.stringify(store.getState())}
                    </script>
                </head>
                <body>
                    <div id="root">${content}</div>
                    <script src="/client/index.js"></script> </body> </html> `); }}); }); app.listen(3000);Copy the code

Ps: An interface “/ API/Users” is added here for subsequent demonstration, which is not within the scope of transformation

Let’s talk about what needs to be changed:

Find all loadData methods that need to be called based on the path, then pass the store call to get the Promise object and add it to promises array.

Here we use the matchPath method provided by the react-router-dom. Since this is a single-level route, the matchPath method is sufficient. For multi-level routing, the react-router-config package can be used. I won’t go into details about how to use this package. In addition, some partners’ routing configuration rules may be different from the official ones. For example, my own routing configuration in the company project is different from the official ones. In this case, I need to write a matching function by myself, but it is also simple, a recursive application.

Add promise. all, and put actions like rendering the React component to generate HTML into its THEN.

As mentioned earlier, in multi-level routing, there are cases where multiple loadData needs to be called. Promise.all is a very good solution to the problem of multiple asynchronous task completion responses.

Add a script to the HTML to mount the new state onto the window object.

According to the flowchart, when the client creates a store, it must pass in an initial state to achieve the effect of data injection. In this case, the global object window is also mounted to facilitate the client to obtain the state.

Client Transformation

const getStore =  (initialState) => {
  return createStore(rootReducer,initialState,applyMiddleware(thunk))
};
const store =  getStore(window.INITIAL_STATE);
Copy the code

The transformation of the client side is relatively simple, mainly on two points:

  • GetStore supports passing in an initial state.
  • Call getStore and pass window.INITIAL_STATE to get a store to inject data into.

Final rendering

Node layer adds interface proxy

When we talk about “join Redux” module, we use a “/ API/Users” interface. This interface is written on the current service, but in the real project, we will be more of an interface to call other services. In addition to calling the interface on the server side, the client side also needs to call the interface, and the client side calls the interface will face the cross-domain problem. Therefore, adding interface proxy in node layer can not only realize the invocation of multiple services, but also solve the cross-domain problem, killing two birds with one stone. I use http-proxy-Middleware package, it can be used as express middleware, the specific use can be viewed in the official documentation, I will not repeat here, I will give you an example for your reference.

The preparatory work

Before using HTTP-proxy-middleware, “/ API/Users” is currently on top of the current service. For demonstration purposes, I built a simple Mock service based on jSON-server with port 8000. Distinguish it from the 3000 ports currently in service. Finally we can visit “http://localhost:8000/api/users” to get what we want, results are as follows.

Start the configuration

It’s as simple as adding a line of code to our server.

import proxy from 'http-proxy-middleware';
app.use('/api',proxy({target: 'http://localhost:8000', changeOrigin: true }))
Copy the code

You can do this for multiple services.

import proxy from 'http-proxy-middleware';
const findProxyTarget = (path)=>{
  console.log(path.split('/'));
  switch(path.split('/') [1]) {case 'a':
      return 'http://localhost:8000';
    case 'b':
      return 'http://localhost:8001';
    default:
      return "http://localhost:8002"
  }
}
app.use('/api'.function(req,res,next){
  proxy({
    target:findProxyTarget(req.path),
    pathRewrite:{
      "^/api/a":"/api"."^/api/b":"/api"
    },
    changeOrigin: true })(req,res,next);
})
Copy the code
  • /api/a/users => http://localhost:8000/api/users
  • /api/b/users => http://localhost:8001/api/users
  • /api/users => http://localhost:8002/api/users

Different services can be distinguished by different prefixes. The extra prefix is used to identify the target. Remember to remove the extra prefix with pathRewrite, otherwise there will be proxy errors.

Working with CSS styles

To solve the error

  module:{
    rules:[{
      test:/\.css$/,
      use: [
        'style-loader',
        {
            loader: 'css-loader',
            options: {
              modules: true}}}}]]Copy the code

The webPack configuration is used to handle CSS styles, but the webPack configuration on the server will have the following problem.

  module:{
    rules:[{
      test:/\.css$/,
      use: [
        'isomorphic-style-loader',
        {
            loader: 'css-loader',
            options: {
              modules: true}}}}]]Copy the code

A potential problem

Problem description

The solution

Isomorphic-style-loader provides this method in the official documentation of isomorphic-style-loader. You can check it in the official documentation of isomorphic-style-loader. So let me just say a few things about this.

webpack

Webpack configuration needs to be aware of two things:

  1. Webpack.client.conf. js (client) and webpack.server.conf.js (server) style-loader must be replaced with isomorphic-style-loader.
  2. CSS -loader must enable CSS Modules, and its options. Modules =true.

component

import withStyles from 'isomorphic-style-loader/withStyles';
import style from './style.css';
export default withStyles(style)(User);
Copy the code

Operation steps:

  1. Introduce the withStyles method.
  2. Import CSS files.
  3. Wrap the component you want to export with withStyles.

The service side

import StyleContext from 'isomorphic-style-loader/StyleContext';

app.get(The '*'.function(req,res){ const css = new Set() const insertCss = (... styles) => styles.forEach(style => css.add(style._getCss())) const content = renderToString( <StyleContext.Provider value={{ insertCss }}> <Provider store={store}> {createRouter('server')({ location:req.url, context })} </Provider> </StyleContext.Provider> ); res.send(` <! doctype html> <html> <head> <title>ssr</title> <style>${[...css].join('')}</style>
                <script>
                    window.INITIAL_STATE = ${JSON.stringify(store.getState())}
                </script>
          </head>
          <body>
                <div id="root">${content}</div>
                <script src="/client/index.js"></script>
          </body>
        </html>
     `);        
})
Copy the code

Operation steps:

  1. Introduce the StyleContext.
  2. Create a new Set object CSS (select Set to ensure uniqueness)
  3. Define an insertCss method. The internal logic is simple. Call the _getCss method of each style object to get the CSS content and add it to the Set object CSS defined earlier.
  4. Add styleconText. Provider, whose value is an object containing an insertCss property that corresponds to the insertCss method defined earlier.
  5. Add a style tag to the rendered template with the content [… CSS].join(” “).

The client

import React from 'react';
import {hydrate} from 'react-dom';
import StyleContext from 'isomorphic-style-loader/StyleContext'
import App from './app'; const insertCss = (... styles) => { const removeCss = styles.map(style => style._insertCss())return () => removeCss.forEach(dispose => dispose())
}
hydrate(
  <StyleContext.Provider value={{ insertCss }}>
    <App />
  </StyleContext.Provider>,
  document.getElementById("root"));Copy the code

Operation steps:

  1. The introduction of StyleContext
  2. Define an insertCss method. The internal logic is to get the return value of _insertCss from each style object passed in, and finally return a function that gets the return value of the previous one and executes it again.
  3. Add styleconText. Provider, whose value is an object containing an insertCss property that corresponds to the insertCss method defined earlier

End result:

react-helmet

When it comes to SEO optimization, one thing you can definitely answer is to add the title tag and two meta tags (keywords, description) to the head tag. In a single-page application, title and meta are fixed, but in a multi-page application, title and meta may be different from page to page, so server rendering projects need to support title and meta changes. The React ecosystem already has a library to help us implement this feature, react-Helmet. Here we talk about its basic use, more use methods you can check its official documentation.

component

import {Helmet} from "react-helmet";
class User extends React.PureComponent{
    render(){
        const {users} = this.props;
        return<div> <Helmet> <title> User page </title> <meta name="keywords" content="user" />
            <meta name="description" content={users.map(user=>user.name).join(', ')} />
          </Helmet>
        </div>
    }
}
Copy the code

Operation steps:

  1. Add the React element Helmet to the Render method.
  2. Add title and meta data to Helmet.

The service side

import {Helmet} from "react-helmet";
app.get(The '*'.function(req,res){
    const content = renderToString(
        <StyleContext.Provider value={{ insertCss }}>
          <Provider store={store}>
              {createRouter('server')({ location:req.url, context })} </Provider> </StyleContext.Provider> ); const helmet = Helmet.renderStatic(); res.send(` <! doctype html> <html> <head>${helmet.title.toString()} 
            ${helmet.meta.toString()}
            <style>${[...css].join('')}</style>
            <script>
              window.INITIAL_STATE = ${JSON.stringify(store.getState())}
            </script>
          </head>
          <body>
            <div id="root">${content}</div>
            <script src="/client/index.js"></script>
          </body>
        </html>
  `);
})
Copy the code

Operation steps:

  1. Execute Helmet. RenderStatic () to get title, meta, etc.
  2. Bind the data to the HTML template.

Note :Helmet. RenderStatic must be called after renderToString otherwise the data will not be available.

conclusion

After reading this article, you need to know the following:

  • The basic flow of server-side rendering.
  • React isomorphism concept.
  • How do I add a route?
  • How do I resolve redirection and 404 issues?
  • How do I add redux?
  • How to complete dehydration and water injection based on REdux data?
  • How to configure proxy at node layer?
  • How to implement CSS style in web source code?
  • The use of the react – helmet

On the server side rendering, there are many voices online that this thing is very chicken ribs. Server-side rendering has two major advantages:

  • Good SEO
  • Short white screen time

The first advantage is that a single-page application can address its SEO problems with prerender technology by adding a prerender-SPa-plugin to the WebPack configuration file. The second is the first screen rendering time. Since the server rendering needs to load data in advance, the white screen time here needs to be combined with the data waiting time. If the interface with a long waiting time is encountered, the experience is definitely not as good as the client rendering. In addition, there are many measures for client rendering to reduce the white screen time, such as asynchronous components, JS splitting, CDN and so on. The “short white screen time” advantage is gone. Personally, I think there are application scenarios for server-side rendering, such as active page projects. This activity page project is very care about bad time, the shorter the bad time be able to retain users to browse, active page data requests are very few, that is to say the server rendering data waiting time basically is negligible, the characteristics of the second active page project is number is more, when the number to a certain degree, Less obvious is the effect of client-side renderings that reduce the amount of time a white screen spends. To summarize what kind of project is suitable for server-side rendering, this project should have the following two conditions:

  • More concerned about the white screen time
  • The waiting time of the interface is short. Procedure
  • There are a lot of pages and a lot of room for growth

Personally, I think SEO can not be used as the primary reason to choose server rendering. First, the server rendering project is much more complex than the client rendering project. Secondly, the client has technology to solve SEO problems. You may not believe it, but there is no Meituan on the first page of baidu search for “take-out” keywords, and there is neither Ele. me nor Meituan in the first three ads.

Next. Js is a server rendering framework that is very well developed, easy to use, well documented and even available in Chinese. Good news for those who don’t like English documents. It is not recommended that you build a server rendering project manually. Server rendering is relatively complex and may not be able to cover all aspects. This article only covers some of the core points, and many of the details are not covered. Second, handing over projects to others is also time-consuming. Of course, if you want to learn more about server-side rendering, it’s best to build one yourself.