React Server Side Render (SSR) React Server Side Render (SSR)

Here is the project’s Github address: github.com/sanyuan0704…

Welcome everyone to point star, mention issue, progress together!

Part1: Implement a basic React component SSR

This section briefly implements a React component SSR.

I. SSR vs CSR

What is server-side rendering?

Without further ado, start an Express server.

var express = require('express')
var app = express()

app.get('/', (req, res) => {
 res.send(
 `   hello   

hello

world

`
) }) app.listen(3001, () = > {console.log('listen:3001')})Copy the code

After starting localhost:3001, you can see hello World displayed on the page. And open the page source code:

This is server-side rendering. In fact, it’s easy to understand that the server returns a bunch of HTML strings that the browser displays.

The opposite of server Side Render is Client Side Render. So what is client rendering? Now create a new React project, scaffolding the project, and run it. Here you can see the React scaffolding automatically generating the first page.

Therefore, the biggest difference between CSR and SSR lies in that JS is responsible for page rendering of the former, while the latter allows the server to directly return HTML for the browser to render directly.

Why use server-side rendering?

  1. The first screen load time is slow due to the JS file pull and React code execution during page display.
  2. SEO(Search Engine Optimazition, Search Engine optimization) is completely helpless, because Search Engine crawlers only recognize HTML structure content, not JS code content.

The emergence of SSR is to solve the drawbacks of traditional CSR.

React server rendering

The initial Express service returns just a plain HTML string, but we’re talking about server-side rendering of React. What does that do? Start with a simple React component:

// containers/Home.js
import React from 'react';
const Home = (a)= > {
  return (
    <div>
      <div>This is sanyuan</div>
    </div>)}export default Home
Copy the code

The task now is to convert it into HTML code and return it to the browser. As we all know, tags in JSX are actually based on the virtual DOM, and eventually have to be converted to the real DOM in some way. Virtual DOM is also JS object, it can be seen that the entire server side rendering process is completed through the compilation of virtual DOM, so the huge expression power of virtual DOM is also visible.

The React-DOM library provides a way to compile the virtual DOM. The practice is as follows:

// server/index.js
import express from 'express';
import { renderToString } from 'react-dom/server';
import Home from './containers/Home';

const app = express();
const content = renderToString(<Home />);
app.get('/', function (req, res) {
   res.send(
   `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
      </body>
    </html>`); }) app.listen(3001, () => { console.log('listen:3001') })Copy the code

Start the Express service and open the corresponding port on the browser. The page shows’ This is Sanyuan ‘. At this point, we have a preliminary implementation of the React component that is server-side rendering. Of course, this is only a very simple SSR, in fact, it is powerless for complex projects, and will be improved step by step to create a fully functional React SSR framework.

Part2: Isomorphism

I. Introduction of isomorphism

In fact, the previous SSR is not complete, usually in the development process will inevitably have some event binding, such as adding a button:

// containers/Home.js
import React from 'react';
const Home = (a)= > {
  return (
    <div>
      <div>This is sanyuan</div>
      <button onClick={()= > {alert('666')}}>click</button>
    </div>)}export default Home
Copy the code

Try again and you’ll be surprised to find that the event binding doesn’t work! So why is that? The reason is simple: renderToString under React-DOM/Server does not handle events, so there is no event binding for the content returned to the browser.

So how do you solve this problem?

That’s where isomorphism comes in. Isomorphism, in layman’s terms, is a set of React code that runs once on the server and again in the browser. The server side renders the page structure and the browser side renders the event binding.

So how do you do event binding on the browser side?

The only way to do this is to let the browser pull the JS file and let the JS code take control. The code returned from the server looks like this:

So how do we produce this index.js?

In this case, we’re going to use the React-dom. The specific approach is actually very simple:

//client/index. js
import React from 'react';
import ReactDom from 'react-dom';
import Home from '.. /containers/Home';

ReactDom.hydrate(<Home />, document.getElementById('root'))
Copy the code

Then compile and package it into index.js using Webpack:

//webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode: 'development'.entry: './src/client/index.js'.output: {
    filename: 'index.js'.path: path.resolve(__dirname, 'public')}},module.exports = merge(config, clientConfig);

//webpack.base.js
module.exports = {
  module: {
    rules: [{
      test: /\.js$/.loader: 'babel-loader'.exclude: /node_modules/.options: {
        presets: ['@babel/preset-react'['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']}}]]}}// Script part of package.json
  "scripts": {
    "dev": "npm-run-all --parallel dev:**"."dev:start": "nodemon --watch build --exec node \"./build/bundle.js\""."dev:build:server": "webpack --config webpack.server.js --watch"."dev:build:client": "webpack --config webpack.client.js --watch"
  },
Copy the code

Here you need to enable the static file service of Express:

const app = express();
app.use(express.static('public'));
Copy the code

Now the front-end script can get the javascript code that controls the browser.

Bind event complete!

Now for a preliminary summary of the flow of isomorphic code execution:

2. Routing problem in isomorphism

Now write a routing configuration file:

// Routes.js
import React from 'react';
import {Route} from 'react-router-dom'
import Home from './containers/Home';
import Login from './containers/Login'

export default (
  <div>
    <Route path='/' exact component={Home}></Route>
    <Route path='/login' exact component={Login}></Route>
  </div>
)
Copy the code

In the client-side control code, which is the client/index.js code written above, the corresponding change should be made:

import React from 'react';
import ReactDom from 'react-dom';
import { BrowserRouter } from 'react-router-dom'
import Routes from '.. /Routes'

const App = (a)= > {
  return (
    <BrowserRouter>
      {Routes}
    </BrowserRouter>
  )
}
ReactDom.hydrate(<App />, document.getElementById('root'))
Copy the code

The console will say something wrong,

// server/index.js
import express from 'express';
import {render} from './utils';

const app = express();
app.use(express.static('public'));
// Note that * is used to match
app.get(The '*'.function (req, res) {
   res.send(render(req));
});
 
app.listen(3001, () = > {console.log('listen:3001')});Copy the code
// server/utils.js
import Routes from '.. /Routes'
import { renderToString } from 'react-dom/server';
// It is important to use StaticRouter
import { StaticRouter } from 'react-router-dom'; 
import React from 'react'

export const render = (req) = > {
  // Build a route on the server side
  const content = renderToString(
    <StaticRouter location={req.path} >
      {Routes}
    </StaticRouter>
  );
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `
}
Copy the code

There is no problem with the route jump now. Note that this is only a jump to the first level route. RenderRoutes for multi-level routes will be handled with renderRoutes in the react-router-config series.

Part3: Introduction of Redux in homogeneous projects

This section focuses on how Redux is introduced into isomorphic projects and the issues to be aware of.

To recap how Redux works:

Create a global store

Now let’s create the Store. Under the store folder (total Store) in the project root directory:

import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '.. /containers/Home/store';
Merge the Reducer of the store in the project component
const reducer = combineReducers({
  home: homeReducer
})
// Create a store and introduce middleware thunk to manage asynchronous operations
const store = createStore(reducer, applyMiddleware(thunk));

// Export the created store
export default store
Copy the code

2. Construction of actions and Reducer in components

The structure of the project files in the Home folder is as follows:

//constants.js
export const CHANGE_LIST = 'HOME/CHANGE_LIST';
Copy the code
//actions.js
import axios from 'axios';
import { CHANGE_LIST } from "./constants";

/ / common action
const changeList = list= > ({
  type: CHANGE_LIST,
  list
});
// Asynchronous action(with thunk middleware)
export const getHomeList = (a)= > {
  return (dispatch) = > {
    return axios.get('xxx')
      .then((res) = > {
        const list = res.data.data;
        console.log(list)
        dispatch(changeList(list))
      });
  };
}
Copy the code
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name: 'sanyuan'.list: []}export default (state = defaultState, action) => {
  switch(action.type) {
    default:
      returnstate; }}Copy the code
//index.js
import  reducer  from "./reducer";
// This is done to export the reducer to the global stores to merge
// We need to import Home/store instead of Home/store/reducer.js into index.js in the global store
// The scaffold automatically recognizes the index file in the folder
export {reducer}
Copy the code

3. Connect component to global store

Here is an example of writing a Home component.

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

class Home extends Component {
  render() {
    const { list } = this.props
    return list.map(item => <div key={item.id}>{item.title}</div>)
  }
}

const mapStateToProps = state => ({
  list: state.home.newsList,
})

const mapDispatchToProps = dispatch => ({
  getHomeList() { dispatch(getHomeList()); }}) // Connect to storeexport default connect(mapStateToProps, mapDispatchToProps)(Home);
Copy the code

The operation of store connection is divided into two parts in the homogeneous project, one is the connection with the client store, the other is the connection with the server store. Stores are passed through providers in react-Redux.

Client:

//src/client/index.js
import React from 'react';
import ReactDom from 'react-dom';
import {BrowserRouter, Route} from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '.. /store'
import routes from '.. /routes.js'

const App = (a)= > {
  return (
    <Provider store={store}>
      <BrowserRouter>
        {routes}
      </BrowserRouter>
    </Provider>
  )
}

ReactDom.hydrate(<App />, document.getElementById('root'))
Copy the code

Server:

// the contents of SRC /server/index.js remain unchanged
SRC /server/utils.js
import Routes from '.. /Routes'
import { renderToString } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom'; 
import { Provider } from 'react-redux';
import React from 'react'

export const render = (req) = > {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} >
        {Routes}
      </StaticRouter>
    </Provider>
  );
  return `
    <html>
      <head>
        <title>ssr</title>
      </head>
      <body>
        <div id="root">${content}</div>
        <script src="/index.js"></script>
      </body>
    </html>
  `
}
Copy the code

Four, the potential pit

In fact, the above store creation method is problematic, what is the reason?

The store above is a singleton. When this singleton is exported, all users use the same store, which is not desirable. So how do we solve this problem this way?

In the global store/index.js file, change it as follows:

// Export some changes
export default() = > {return createStore(reducer, applyMiddleware(thunk))
}
Copy the code

In this way, when the client and server js files are introduced, a function is actually introduced. Executing this function will get a new store, so that each user can be guaranteed to use a new store when accessing.

Part4: Server-side rendering scheme for asynchronous data (data flooding and dewatering)

First, the introduction of problems

In a typical React client development, we typically fetch asynchronous data in the component’s componentDidMount lifecycle function. However, there is a problem with server-side rendering.

Now I make an Ajax request in the componentDidMount hook function:

import { getHomeList } from './store/actions'
  / /...
  componentDidMount() {
    this.props.getList();
  }
  / /...
  const mapDispatchToProps = dispatch= >({ getList() { dispatch(getHomeList()); }})Copy the code
//actions.js
import { CHANGE_LIST } from "./constants";
import axios from 'axios'

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

export const getHomeList = (a)= > {
  return dispatch= > {
    // Add a local backend service
    return axiosInstance.get('localhost:4000/api/news.json')
      .then((res) = > {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
//reducer.js
import { CHANGE_LIST } from "./constants";

const defaultState = {
  name: 'sanyuan'.list: []}export default (state = defaultState, action) => {
  switch(action.type) {
    case CHANGE_LIST:
      constnewState = { ... state,list: action.list
      }
      return newState
    default:
      returnstate; }}Copy the code

Ok, now start the service.

When the browser sends a request, the server receives the request, the store for both the server and the client is empty, and then the client executes functions in the Life cycle of componentDidMount to fetch the data and render it to the page. However, the server side never executes componentDidMount, so no data is retrieved, and the server side store is always empty. In other words, operations on asynchronous data are always just client-side rendering.

The job now is to get the server to perform the data retrieval operation again to achieve a true server rendering.

2. Modify routes

Before completing this solution, we need to modify the original route, namely routes.js

import Home from './containers/Home';
import Login from './containers/Login';

export default[{path: "/".component: Home,
  exact: true.loadData: Home.loadData,// The server gets asynchronous data
  key: 'home'
},
{
  path: '/login'.component: Login,
  exact: true.key: 'login'}}];Copy the code

The JSX code written on both the client and server sides changes accordingly

/ / the client
// All the following routes variables refer to arrays exported by routes.js
<Provider store={store}>
  <BrowserRouter>
      <div>
        {
            routers.map(route => {
                <Route {. route} / >})}</div>
  </BrowserRouter>
</Provider>
Copy the code
/ / the server
<Provider store={store}>
  <StaticRouter>
      <div>
        {
            routers.map(route => {
                <Route {. route} / >})}</div>
  </StaticRouter>
</Provider>
Copy the code

A loadData parameter is configured, which represents the server’s function to get the data. This function for the corresponding component is called each time a rendered component retrieves asynchronous data. Therefore, before writing the specific code for this function, it is necessary to figure out how to match different loadData functions for different routes.

Add the following logic to server/utils.js

  import { matchRoutes } from 'react-router-config';
  // Call matchRoutes to match the current route.
  const matchedRoutes = matchRoutes(routes, req.path)
  // Array of Promise objects
  const promises = [];
  matchedRoutes.forEach(item= > {
    // If the component corresponding to this route has a loadData method
    if (item.route.loadData) {
      // Execute once and pass store in
      // Note that a loadData call returns a Promise object
      promises.push(item.route.loadData(store))
    }
  })
  Promise.all(promises).then((a)= > {
      // All the required data is stored in the store
      // Perform the render process (res.send)})Copy the code

It is now safe to write our loadData function, which is quite easy after the groundwork has been laid.

import { getHomeList } from './store/actions'

Home.loadData = (store) = > {
    return store.dispatch(getHomeList())
}
Copy the code
//actions.js
export const getHomeList = (a)= > {
  return dispatch= > {
    return axios.get('xxxx')
      .then((res) = > {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
Copy the code

With this in mind, asynchronous data retrieval is complete in server-side rendering.

Water injection and dehydration of data

In fact, there are still some details of doing this. For example, when I annotate the asynchronous request function in the lifecycle hook, now there is no data in the page, but when I open the page source, I find:

It’s actually pretty easy to understand. When the server gets the store and gets the data, the JS code of the client executes again, and an empty store is created during the execution of the client code. The data of the two stores cannot be synchronized.

So how do you synchronize the data between the two stores?

First, after the server gets the fetch, add a script tag to the returned HTML code:

<script>
  window.context = {
    state: ${JSON.stringify(store.getState())}
  }
</script>
Copy the code

This is called “water flooding” of data, which injects server store data into the Window global environment. The next step is to “dewater” the window. In other words, the window is bound to the client store. This can be done at the source of the client store, in the global store/index.js.

//store/index.js
import {createStore, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer } from '.. /containers/Home/store';

const reducer = combineReducers({
  home: homeReducer
})
// The server store creates the function
export const getStore = (a)= > {
  return createStore(reducer, applyMiddleware(thunk));
}
// The client store creates the function
export const getClientStore = (a)= > {
  const defaultState = window.context ? window.context.state : {};
  return createStore(reducer, defaultState, applyMiddleware(thunk));
}
Copy the code

At this point, the data dehydration and water injection operation is complete. However, there are still some flaws. When the server gets the data, the client doesn’t need to send Ajax requests, and the client’s React code still has such wasteful code. What to do?

Again in the Home component, make the following changes:

componentDidMount() {
  // Determine whether the current data has been fetched from the server
  // Keep in mind that if the component is rendered the first time, the request will not be repeated
  // If the component is not rendered the first time the page is rendered, the asynchronous request code must be executed
  // Both cases are possible for the same component
  if (!this.props.list.length) {
    this.props.getHomeList()
  }
}
Copy the code

Down the way, asynchronous data server rendering is still more complex, but the difficulty is not great, need to be patient geographical clear thinking.

At this point, a relatively complete SSR framework is almost built, but there are some content to supplement, and will continue to update. Come on!

Part5: Node for middle layer and request code optimization

Why introduce the Node middle tier?

In fact, any technology is closely related to its application scenario. Here we repeatedly talk about SSR, in fact, we do not have to do it, SSR to solve the biggest pain point is SEO, but it has brought more expensive costs. Not only because of the service side rendering needs to be more complex processing logic, but also because of homogeneous process need the server and the client code is executed again, although it to the client and no matter, but for the service side is great pressure, because of the large number of visitors, for each visit will be in addition to execute on the server and compile the code again to compute, It takes a lot of performance out of the server side and increases the cost. If the traffic is large enough, the pressure that a single server could bear when SSR was not used may increase to 10 now. The pain point is SEO, but if the actual SEO requirements are not high, then the use of SSR is unnecessary.

So again, why introduce Node as a middle tier? Where is it in the middle? And what scenarios are solved?

In development mode without the front and back end separation of the middle tier, the front end typically requests the back end interface directly. However, in real scenarios, the data format provided by the back-end is not what the front-end wants, but the interface format cannot be changed for performance reasons or other reasons. In this case, some additional data processing operations need to be performed in the front-end. There is nothing wrong with the front-end handling of data, but when the amount of data becomes huge, it will cause huge performance loss on the client side, and even affect the user experience. At this point, the concept of node middle tier comes into being.

It finally solves the problem of front – and back-end collaboration.

The general workflow of the middle layer is like this: each time the front-end sends a request, it requests the interface of the Node layer, and then the node forwards the corresponding front-end request. After obtaining the data, the Node layer performs the corresponding data calculation and other processing operations, and then returns it to the front end. This lets the Node layer take over the data for the front end.

Second, the introduction of middle layer in SSR framework

In the SSR framework built before, the server and client requests use the same set of request back-end interface code, but this is not scientific.

For clients, it’s best to go through the Node middle tier. For this SSR project, the server node starts is a middle tier role, so the server can directly request the real back-end interface to perform the data request.

//actions.js
// The server parameter indicates whether the current request is on the Node server
const getUrl = (server) = > {
    return server ? 'XXXX (back-end interface address)' : '/ API/sanyuan. Json (node interface)';
}
// The server parameter is passed from the Home component,
// Call this action in componentDidMount with false,
// Pass true when called in loadData, so there is no component code attached here
export const getHomeList = (server) = > {
  return dispatch= > {
    return axios.get(getUrl(server))
      .then((res) = > {
        const list = res.data.data;
        dispatch(changeList(list))
      })
  }
}
Copy the code

Server /index.js should forward the request to the front end, which is directly used as a proxy, or node can send an HTTP request to the back end alone.

// Add the following code
import proxy from 'express-http-proxy';
// It intercepts the/API part of the front-end request address and changes it to another address
app.use('/api', proxy('http://xxxxxx(server address)', {
  proxyReqPathResolver: function(req) {
    return '/api'+req.url; }}));Copy the code

Third, request code optimization

There is room for optimization in the requested code, as the server parameters above are not actually passed.

Now we use axios instance and Thunk withExtraArgument to do some wrapping.

/ / the new server/request. Js
import axios from 'axios'

const instance = axios.create({
  baseURL: 'http://xxxxxx(server address)'
})

export default instance


/ / the new client/request. Js
import axios from 'axios'

const instance = axios.create({
  // Is the node service of the current path
  baseURL: '/'
})

export default instance
Copy the code

Then make a tweak to the global store code:

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

const reducer = combineReducers({
  home: homeReducer
})

export const getStore = (a)= > {
  // Use thunk middleware with serverAxios
  return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = (a)= > {
  const defaultState = window.context ? window.context.state : {};
   // Use thunk middleware with clientAxios
  return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}

Copy the code

Now the action requesting data in the Home component does not need to be passed. The request code in actions.js is as follows:

export const getHomeList = (a)= > {
  // The default third argument in the return function is the axios instance passed in by withExtraArgument
  return (dispatch, getState, axiosInstance) = > {
    return axiosInstance.get('/api/sanyuan.json')
      .then((res) = > {
        const list = res.data.data;
        console.log(res)
        dispatch(changeList(list))
      })
  }
}
Copy the code

At this point, code optimization is done, and this kind of code encapsulation technique can be applied to other projects, which is actually quite elegant.

Part6: renderRoutes

Now change the content of routes.js to the following:

import Home from './containers/Home';
import Login from './containers/Login';
import App from './App'

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

Now the requirement is to make the page common Header component, the App component is written as follows:

import React from 'react';
import Header from './components/Header';

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

For multi-level routing rendering, both the server and client need to perform it once. Therefore, all JSX code written should implement:

//routes refers to the array returned in routes.js // server: <Provider store={store}> <StaticRouter location={req.path} > <div> {renderRoutes(routes)} </div> </StaticRouter> </Provider> //  <Provider store={getClientStore()}> <BrowserRouter> <div> {renderRoutes(routes)} </div> </BrowserRouter> </Provider>Copy the code

The renderRoutes method is used here. In fact, its job is very simple. It is to render the routing components of one layer (App component in this case) according to the URL, and then pass the routing of the next layer to the current App component through the props.

The App component can use props. Route. routes to get the next layer of routes to render:

import React from 'react';
import Header from './components/Header';
// Add the renderRoutes method
import { renderRoutes } from 'react-router-config';

const  App = (props) = > {
  console.log(props.route)
  return (
    <div>
      <Header></Header>
      <! Get the Login and Home component route -->
      {renderRoutes(props.route.routes)}
    </div>)}export default App;
Copy the code

At this point, the rendering of multi-level routes is complete.

Part7: CSS server-side rendering (Context hook variables)

1. Introduce CSS in client projects

Again, take the Home component

//Home/style.css
body {
  background: gray;
}

Copy the code

Now introduce in the Home component code:

import styles from './style.css';
Copy the code

It is important to know that this method of introducing CSS code does not work under normal circumstances, so you need to configure it in WebPack. Start by installing the appropriate plug-in.

npm install style-loader css-loader --D
Copy the code
//webpack.client.js
const path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const clientConfig = {
  mode: 'development'.entry: './src/client/index.js'.module: {
    rules: [{
      test: /\.css? $/.use: ['style-loader', {
        loader: 'css-loader'.options: {
          modules: true}}}}]],output: {
    filename: 'index.js'.path: path.resolve(__dirname, 'public')}},module.exports = merge(config, clientConfig);
Copy the code
// the webpack.base.js code, to recap, is configured with the ES syntax
module.exports = {
  module: {
    rules: [{
      test: /\.js$/.loader: 'babel-loader'.exclude: /node_modules/.options: {
        presets: ['@babel/preset-react'['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']}}]]}}Copy the code

Ok, now CSS is working on the client side.

2. Introduction of server-side CSS

First, install a webPack plug-in,

npm install -D isomorphic-style-loader
Copy the code

Then do the corresponding CSS configuration in webpack.server.js:

//webpack.server.js
const path = require('path');
const nodeExternals = require('webpack-node-externals');
const merge = require('webpack-merge');
const config = require('./webpack.base');

const serverConfig = {
  target: 'node'.mode: 'development'.entry: './src/server/index.js'.externals: [nodeExternals()],
  module: {
    rules: [{
      test: /\.css? $/.use: ['isomorphic-style-loader', {
        loader: 'css-loader'.options: {
          modules: true}}}}]],output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, 'build')}}module.exports = merge(config, serverConfig);
Copy the code

What did it do?

Take a look at this line of code:

import styles from './style.css';
Copy the code

When importing the CSS file, the isomorphic-style-loader helps us hang three functions in styles. Output styles to see:

So where do we put the CSS code once we get it? The react-router-dom StaticRouter has already prepared a hook context for us. The following

<StaticRouter location={req.path} context={context}> <div> {renderRoutes(routes)} </div> </StaticRouter>Copy the code

This means that components in the Routes configuration object can get the context during server rendering, and this context is equivalent to the props. StaticContext in the component. Also, the props. StaticContext will only exist during server rendering, not client rendering. This allows us to distinguish between the two rendering environments using this variable.

Now we need to initialize the context variable before the render function on the server side executes:

let context = { css: []}Copy the code

We just need to write the appropriate logic in the component’s componentWillMount lifecycle:

componentWillMount() {
  // Determine whether to render the server environment
  if (this.props.staticContext) {
    this.props.staticContext.css.push(styles._getCss())
  }
}
Copy the code

After executing the renderToString on the server side, the CSS of the context is now an array with contents. Let’s get the CSS code in it:

// Splice code
const cssStr = context.css.length ? context.css.join('\n') : ' ';
Copy the code

Now mount to page:

<style>${cssStr}</style>Copy the code

Optimize code with higher-order components

You’ve probably seen that for every component that has a style, exactly the same logic needs to be executed in the componentWillMount lifecycle. Can we encapsulate that logic so that it doesn’t have to be repeated?

It can be done. With higher-order components:

// create withstyle. js file import React, {Component} from'react'; // The function returns the component. // The first argument that needs to be passed is the component that needs to be decorated. // The second argument is the styles objectexport default (DecoratedComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {// Determine if it is a server rendering processif (this.props.staticContext) {
        this.props.staticContext.css.push(styles._getCss())
      }
    }
    render() {
      return<DecoratedComponent {... this.props} /> } } }Copy the code

Then let the exported function wrap our Home component.

import WithStyle from '.. /.. /withStyle';
/ /...
const exportHome = connect(mapStateToProps, mapDispatchToProps)(withStyle(Home, styles));
export default exportHome;
Copy the code

Isn’t that a lot simpler? For more and more components in the future, this approach is perfectly acceptable.

Part8: Make sure that a react-Helmet is more popular

In this section, let’s talk a little bit about SEO.

First, SEO skills to share

SEO(Search Engine Optimization) refers to the use of Search Engine rules to improve the natural ranking of a website in relevant Search engines. The current search engine crawler generally adopts the mode of full-text analysis, which covers the content of three main parts of a website: text, multimedia (mainly pictures) and external links, through which the type and theme of a website can be judged. Therefore, when doing SEO optimization, you can expand around these three angles.

For the text, try not to copy the existing article, to write technical blog for example, pieced together to copy the article ranking is generally not high, if you need to cite other people’s article to remember the source, but it is best to be original, so the ranking effect will be better. Multimedia includes video, pictures and other file forms. At present, authoritative search engine crawlers such as Google have basically no problem in analyzing pictures. Therefore, high-quality pictures are also a plus. In addition, external links, that is, the point of a tag in the website, should also be some links related to the current website, which are easier for crawler analysis.

Of course, the face of the site, that is, the title and description is also crucial. Such as:

Conversion rate

A: React-Helmet was introduced

In the React project, a single-page application is developed. The page always has only one title and description. How to display different website titles and descriptions according to different components?

It can be done.

npm install react-helmet --save
Copy the code

Component code :(again, the Home component)

import { Helmet } from 'react-helmet';

/ /...
render() { 
    return (
      <Fragment>
        <! The items in the Helmet tag will now be placed in the head section of the client.
        <Helmet>
          <title>This is sanyuan's technology blog, sharing front-end knowledge</title>
          <meta name="description" content="This is Sanyuan's technology blog, sharing front-end knowledge."/>
        </Helmet>
        <div className="test">
          {
            this.getList()
          }
        </div>
      </Fragment>); / /...Copy the code

This is only done on the client side; it still needs to be done on the server side.

It’s actually quite simple:

//server/utils.js
import { renderToString } from 'react-dom/server';
import {  StaticRouter } from 'react-router-dom'; 
import React from 'react';
import { Provider } from "react-redux";
import { renderRoutes } from 'react-router-config';
import { Helmet } from 'react-helmet';

export const render = (store, routes, req, context) = > {
  const content = renderToString(
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          {renderRoutes(routes)}
        </div>
      </StaticRouter>
    </Provider>
  );
  // Take the Helmet object and introduce it in the HTML string
  const helmet = Helmet.renderStatic();

  const cssStr = context.css.length ? context.css.join('\n') : ' ';

  return  `
    <html>
      <head>
        <style>${cssStr}</style>
        ${helmet.title.toString()}
        ${helmet.meta.toString()}
      </head>
      <body>
        <div id="root">${content}</div>
        <script>
          window.context = {
            state: The ${JSON.stringify(store.getState())}
          }
        </script>
        <script src="/index.js"></script>
      </body>
    </html>
  `
};
Copy the code

Now look at the results:

React server rendering is very complex and requires a high level of front-end capabilities. However, it will be helpful to learn how to React server rendering works. I believe you have the ability to build your own SSR wheel after reading this series and have a deeper understanding of this aspect of technology.

References:

React Server Rendering Principle Analysis and Practice