Github address github.com/hzfvictory/… , convenient for everyone to understand more easily, otherwise behind a lot of places will be confused.

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

Client-side rendering versus server-side rendering

CSR:

Page rendering is done by JSCopy the code

Browser sends the request – > server returns HTML – > browser sends the bundle.js request – > server returns bundle.js – > Browser executes the react code in bundle.js to complete the rendering

SSR:

The server side just returns the HTML and lets the browser render directlyCopy the code

The browser sends the request – > the server runs the React code to generate the page – > the server returns the page

Disadvantages of traditional CSR:

  1. The rendering process involves at least two HTTP request cycles (HTML + JS) due to the JS file pull and React code execution during the page display process, so it takes some time and the first screen load time is slow.

  2. SEO(Search Engine Optimazition, Search Engine optimization) is completely helpless, because Search Engine crawlers only recognize HTML structure content, not JS code content.

Disadvantages of SSR:

The emergence of SSR is to solve the drawbacks of traditional CSRCopy the code

Using THE React SSR technology, we made the React code run first on the server side, so that the HTML downloaded by the user already contains all the page display content. In this way, the page display process only needs to go through one HTTP request cycle. TTFP (Time To First Page) Time is more than doubled

However, using SSR technology will make the simple React project very complicated

  1. The renderer used in SSR naturally takes more CPU and memory resources than a server that simply needs to serve static files

  2. Some common browser apis, such as Window, Docment, and Alert, may not work properly on server-generated pages, and the environment in which they are running needs to be judged if they are used

  3. Development and debugging can be a bit tricky because of the complexity of managing the life cycle of some of the SPA’s components because browsers and servers are involved

  4. Some factors may cause the server-side rendering to be inconsistent with the browser-side results, the project becomes less maintainable, and it becomes difficult to trace code problems

Therefore, when using SSR to solve problems, it will also bring a lot of side effects, and sometimes these side effects are much more harmful than the advantages brought by SSR technology. SSR is generally recommended, unless your project is heavily dependent on search engine traffic or has special requirements for first screen time, but prerender is recommended for SEO only.

The essence of SSR implementation

It is a SPA project based on React. It is not a pure back-end direct rendering method like ThinkPHP, JSP, nodeJs+ EJS. So most of this is just for the first screen SSR, because the browser route jump method is using the H5 history API window.history.pushState(), which allows us to change the URL or not refresh the page. So it will not go to the server side [can get the required data through preloading].

SSR can be realized essentially because of the existence of virtual DOM

In the React framework, we introduced a concept called the virtual DOM. When React does page operations, it does not actually operate on the DOM directly. Instead, you manipulate the virtual DOM, that is, normal JavaScript objects, which makes SSR possible. On the server, I can manipulate JavaScript objects, determine if the environment is a server environment, and we map the virtual DOM to string output; On the client side, I can also manipulate JavaScript objects, determine that the environment is the client environment, and directly map the virtual DOM to the real DOM to complete the page mount.

Project selection

  • Next. Js /nuxt.js is cheap, and you can just write pages without worrying too much about server routing
  • Renderer implements server-side prerendering of SPA projects
  • Implement server-side rendering of SPA project using Google Rendertron
  • With the attitude of learning, I learned the basic principle and chose to build it by myself (after a break in the middle for a period of time, I picked it up again now). I read an article about someone building SSR with React + Redux + Express before, so based on my familiarity with and special preference for DVA and KOA, Dva-core + KOA was directly selected to build state management.

Koa implements the base version of SSR

Don’t use koa – the router

const Koa = require('koa');
const app = new Koa();

app.use((ctx) = > {
  if (ctx.path === '/') {
    ctx.body =
      ` < HTML > < head > < title > grain and SSR < / title > < / head > < body > < h1 > hello < / h1 > < h2 > world < / h2 > < / body > < / HTML > `; }})const server = app.listen('9999'.() = > {
  const {port} = server.address();
  console.log(`http://localhost:${port}`)})Copy the code

Use koa – the router

const Koa = require('koa');
const app = new Koa();
const route = require("koa-router") ()// Constructors can also be used here

route.get("/".(ctx) = > {
  ctx.body =
    ` < HTML > < head > < title > grain and SSR < / title > < / head > < body > < h1 > hello < / h1 > < h2 > world < / h2 > < / body > < / HTML > `
})

app.use(route.routes());
app.use(route.allowedMethods()); Ctx. status Completes the response header

const server = app.listen('9999'.() = > {
  const {port} = server.address();
  console.log(`http://localhost:${port}`)})Copy the code

In addition, the source code of the web page has the DOM information, which is very SEO friendly. We react and Vue introduced JS through webpack, and all the function page display is completed by JS.

Implement server-side rendering of the React component

At this point, you can’t start the service directly with Node. With no Babel, React doesn’t convert to createElement, and node doesn’t import directly.

Write a random React component

// src/pages/home
import React from 'react';
const Home = () = > {
  return (
    <div>
      <div>Home components</div>
    </div>)}export default Home
Copy the code

Then we render the current component using the server and look at the following configuration:

Webpack base

// config/webpack.base.js
const path = require('path')

module.exports = {
  module: {
    rules: [{
      test: /\.js|jsx$/,
      loader: 'babel-loader'.exclude: /node_modules/,
      options: {
        presets: ['@babel/preset-react'['@babel/preset-env', {
          targets: {
            browsers: ['last 2 versions']}}]]}}Copy the code

Server-side Webpack configuration

If the server runs code that relies on Node core modules or third-party modules, there is no need to package some of the client’s module code into the final code. Because the environment already has these dependencies installed, they can be referenced directly. In this case, we need to configure: target: node in webpack and use the webpack-node-externals plugin to solve the problem of third-party dependency packaging.

// config/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'./ / the compilation can let the node identification code https://webpack.docschina.org/concepts/targets/
  mode: 'development'.// Pay special attention to the mode
  entry: './src/server/index.js'.// The corresponding server code
  // https://webpack.docschina.org/configuration/externals/
  externals: [nodeExternals()], // To ignore all modules in the node_modules folder
  output: {
    filename: 'bundle.js'.path: path.resolve(__dirname, '.. /bundle')}}module.exports = merge(config, serverConfig)
Copy the code

Approximate difference between target: ‘node’ and target: ‘web’

// target: 'node'
exports.ids = [0];
exports.modules = {};
// target: 'web' 
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0]] and {})Copy the code
// server/index.js
import Koa from 'koa';
import Router from "koa-router"
import React from "react"; // Must be introduced
import {renderToString} from 'react-dom/server'; // React-dom provides a method

import Home from ".. /src/pages/home"

const app = new Koa();
const route = new Router()

const content = renderToString(<Home/>);

route.get("/".(ctx) = > {
  ctx.body =
    '< HTML > <head> <title> SSR </title> </head> <body> <div id="root">${content}</div>
      </body>
    </html>
    `
})

app.use(route.routes());
app.use(route.allowedMethods());

const server = app.listen('9999'.() = > {
  const {port} = server.address();
  console.log(`http://localhost:${port}`)})Copy the code

RenderToString is used above. As we know, react-DOM provides four server-side rendering functions, as follows:

  1. RenderToString: Converts the React Component to an HTML string. The generated HTML DOM will have additional attributes: Each DOM will have a data-react-id attribute, and the first DOM will have a data-reactroot attribute.
  2. RenderToStaticMarkup: Converts the React Component to an HTML string, but the DOM that generates the HTML does not have additional attributes, saving the size of the HTML string.
  3. RenderToNodeStream: Output HTML as a stream instead of generating the entire HTML like renderToString before sending it to the client. RenderToString responds faster to clients and improves page rendering speed.
  4. RenderToStaticNodeStream: Like renderToNodeStream, this output stream does not contain data-reactroot attributes in HTML.

For server-side rendering

  • renderToStringThe nodes rendered by the react method will have the data-react-id attribute. After the front-end React loading is complete, the front-end React will recognize the content rendered by the server and will not re-render the DOM node. The front-end React will take over the page and executecomponentDidMoutBinding browser events and so on is not done on the server and is not possible.
  • renderToStaticMarkupRender out is not stripdata-react-idAfter react loads, the server will erase the page rendered by the server and re-render it. In other words,The react front end doesn’t know what the server renderedThe render method overwrites the content in #react-target using innerHTML

Add startup configuration in package

// package.json
"scripts": {
    "dev": "npm-run-all --parallel dev:build:server dev:start"."dev:build:server": "webpack --config config/webpack.server.js --watch"."dev:start": "nodemon ./bundle/bundle.js"
}
Copy the code

The React component is rendered on the server side. If you add some methods or call interfaces to the component Home, you’ll notice that none of them are executed. So we still need to make further improvements.

homogeneous

To solve the above problems, we need to isomorphism, the isomorphism, popular, is a set of the React code running on the server again, running again, to the browser renders the service side rendering completed page structure, the browser rendering complete event binding interface is taken (reloading js or CSS client will compare coordination phase, If it is the same, it is not rendered.

The client packages JS for the route

Inject the packed JS into HTML, so that the browser will request again, you can complete the event binding and other behavior operations.

We need the React-DOM hydrate

// client/index.js
import React, {Component} from "react"
import ReactDom from 'react-dom';
import {BrowserRouter as Router, Switch} from 'react-router-dom';
import {renderRoutes} from 'react-router-config';
import Loadable from 'react-loadable'; // Here is my route split, you can not use

import routes from '.. /router';

class App extends Component {
  render() {
    return (
      <Router>
        <Switch>
          {renderRoutes(routes.routes)}
        </Switch>
      </Router>
    )
  }
}

Loadable.preloadReady().then(() = > {
  ReactDom.hydrate(
    <App/>.document.getElementById('root'));
})
Copy the code

Hydrate () describes the process by which ReactDOM reuses content rendered by the ReactDOMServer with as much structure as possible and complements client-specific content such as event bindings

There are no unique attributes on the Render () tag, but SSR’S HTML structure needs to be reused as much as possible, so there is hydrate(), but both are currently available, render() version 17 no longer supports SSR

Zhihu explains ReactDom. Hydrate

Then configure the client webpack to compile and package it into JS, which is imported into the server HTML.

Client Webpack configuration

The client and server packaged output directory

// config/outputPath 
module.exports = {
  OUTPUTCLIENT: 'static'.OUTPUTSERVER: 'bundle'
}
Copy the code
// config/webpack.client.js
const path = require('path')
const merge = require('webpack-merge')
const config = require('./webpack.base')
const {OUTPUTCLIENT} = require("./outputPath")
const outputPath = `.. /${OUTPUTCLIENT}`

const clientConfig = {
  mode: 'development'.entry: path.resolve(__dirname, '.. /client/index.js'),
  output: {
    filename: 'index.[chunkhash:8].js'.// I'm using hash here to prevent caching
    path: path.resolve(__dirname, outputPath),
    publicPath: '/'
  },
  module: {
    rules: [{test: /\.css? $/,
        use: ['style-loader', {// The style-loader is recommended. A small amount of CSS is rendered directly by the client
          loader: 'css-loader'.options: {
            modules: true.// This must be consistent with the server, otherwise there is a style in the head, the client does not have the corresponding class}}}, {test: /\.(png|jpeg|jpg|gif|svg)? $/,
        loader: 'url-loader'.options: {
          limit: 8000.outputPath: outputPath, // Input path
          publicPath: '/'}}]}}module.exports = merge(config, clientConfig)
Copy the code

Add dev:build:client: webpack –config webpack.client.js –watch to package.json.

Routing logic on the server

The server-side routing code is a bit more complex, requiring you to pass location (the current request path) to the StaticRouter component so that it can figure out which component is currently required based on the path. (PS: StaticRouter is a route component specifically provided by the React-Router for server-side rendering.)

// server/index.js
import Koa from 'koa';
import React from "react";
import Router from "koa-router"
import {renderToString} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import Loadable from 'react-loadable';
import routes from '@/router';
import {renderRoutes, matchRoutes} from "react-router-config";

import {renderHTML} from "./tem"

const app = new Koa();
const route = new Router()

route.get(["/:route?"./\/([\w|\d]+)\/.*/].(ctx) = > {
  const content = renderToString(
    // That's the point
    <StaticRouter location={ctx.path}>
        {renderRoutes(routes.routes)}
    </StaticRouter>
  );
  ctx.body = renderHTML(content, {})
})
// Note the order in which the middleware is placed
app.use(require('koa-static')(process.cwd() + '/static'));
app.use(route.routes());
app.use(route.allowedMethods());

Loadable.preloadAll().then(() = > {
  const server = app.listen('9999'.() = > {
    const {port} = server.address();
    console.log(`\x1B[33m\x1B[4mhttp://localhost:${port}\x1B[0m`)})});Copy the code
// server/tem.js
const glob = require('glob');
let project = glob.sync(process.cwd() + '/static/index.*.js');


let path = project[0].split('/')

export const renderHTML = (content, store) = > ` <! DOCTYPE html> <html lang="zh"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, User-scalable =no" /> <meta name="theme-color" content="#000000"> <title> SSR </title> </head> <body> <div ID ="root">${content}</div>
      <script src=/${path[path.length - 1]}></script> // This '/' must be added, the pit for a long time </body> </ HTML > '
Copy the code

CSS style problem handling

Normal server rendering only returns the HTML string, and the style is added only after the browser has loaded the CSS. This process will cause the page to flash, so add the CSS reference directly in the server.

We can no longer use style-loader because the Webpack Loader loads the style module into the HTML header at compile time. However, in the server rendering environment, there is no window object, and the style-loader will report an error. Generally, isomorphic-style-loader is used instead, and isomorphic-style-loader can also solve the problem of page style flashing, and its principle is not difficult to understand: Isomorphic-style-loader uses the Context API to obtain the style information of all React components when rendering the page components. When the server outputs THE HTML string, the style is also inserted into the HTML string, and the result is transmitted to the client.

Since we have cssmodules enabled, there will be no style conflicts when importing directly into the head. Isomorphic-style-loader has provided us with some higher-order functions withsSyles that import CSS

“And hooks which are easy to use.

Take a look at the code configuration

// config/webpack.client.js
{
  test: [/\.css|less$/].use: [
      'style-loader'.// It can also be the same as on the server side, except that each time CSS is used, additional procedures are required
      {
        loader: 'css-loader'.options: {
          modules: true,}},'less-loader']},Copy the code
// config/webpack.server.js
{
  test: [/\.css|less$/].use: [
      'isomorphic-style-loader',
      {
        loader: 'css-loader'.options: {
          modules: true,}},'less-loader'  // It must be configured otherwise it will be regarded as CSS. It may not be visually obvious because the client is configured with less]}Copy the code

Server Home page

// server/index.js 
/ /...
const css = new Set(a)// This must be inside the routing function. If it is outside, it will add up the CSS before it appears
const insertCss = (. styles) = > styles.forEach(style= > css.add(style._getCss()));

const content = renderToString(
  <StaticRouter location={ctx.path}>
        <StyleContext.Provider value={{insertCss}}> 
           {renderRoutes(routes.routes)}
      </StyleContext.Provider>
   </StaticRouter>
)
 
ctx.body = renderHTML(content, {}, css)
/ /...
Copy the code

The client also needs to be configured

// client/index.js
import React from "react"
import ReactDom from 'react-dom';
import {BrowserRouter as Router} from 'react-router-dom';
import {renderRoutes} from 'react-router-config';
import Loadable from 'react-loadable';
import StyleContext from 'isomorphic-style-loader/StyleContext'


import routes from '.. /router';

const insertCss = (. styles) = > {
  const removeCss = styles.map(style= > style._insertCss && style._insertCss());
  return () = > removeCss.forEach(dispose= > dispose && dispose())
}

const App = () = > {
  return (
    <Router>
        {renderRoutes(routes.routes)}
    </Router>)}; Loadable.preloadReady().then(() = > {
  ReactDom.hydrate(
    <StyleContext.Provider value={{insertCss}}>
      <App/>
    </StyleContext.Provider>.document.getElementById('root'));
})
Copy the code

In this way, both the server and the client can directly use some API of isomorphic-style-loader, but some pages are not important. Or non-essential CSS can be rendered directly by the client because the client uses the style-loader and does not need to introduce higher-order functions or useStyles.

Specific use within the page

// Functional components
import useStyles from 'isomorphic-style-loader/useStyles'
import styles from "./index.css"

const Index = (props) = > {
  useStyles(styles)
}  
Copy the code
// Class component
import withStyles from 'isomorphic-style-loader/withStyles'
import styles from "./index.css"
@withStyles(styles) // Additional configuration is required in webpack.base.js

class Index extends React.Component {}
Copy the code
// Use client rendering
import styles from "./index.css"

const Index = () = > {
  // Client rendering can also be used here using the useStyles section
  return (
    <div>
      <h1 className={styles['title-center']} >message</h1>
      <h1 className={'title-center'} >message</h1>
    </div>)}Copy the code

Then open up the source code of the web page and you can see that the CSS we need is already in the head.

Acquisition of asynchronous data in SSR + use of Dva

The use of the Dva

Dva has been used in the previous project. Here, DVA-core is directly used instead of Redux, which will not be configured. Please check the document by yourself.

Create Store: This section has a pit to avoid, as you know, in client rendering there is always only one Store in the user’s browser, so you can write this in code

const dvaApp = createApp({
  initialState: {},
  models: models,
});
const store = dvaApp.getStore();
export default store;
Copy the code

On the server side, however, this is a problem, because on the server side the Store is used by all users, and if you build a Store like this, where the Store becomes a singleton and all users share the Store, it’s obviously a problem. So in server-side rendering, Store creation should look like the following, returning a function that is re-executed for each user as they visit, providing a separate Store for each user

const dvaApp = createApp({
  initialState: {},
  models: models,
});

export const getStore =  () = > {
  return dvaApp.getStore();
}
Copy the code

Do not panic, if you do this, redux data will still be synchronized to all customers, because your model is an object, it is a static import, in this case you should write the model as a function, so that the background can get the latest data every time

const menuTree = () = > {
  return {
    namespace: 'menuTree'.state: {
      routes: []},effects: {*reset(payload, {call, put, select, update}) {
         / /...}},reducers: {
      save(state, {payload}) {
        return{... state, ... payload}; ,}}}};export default menuTree
Copy the code

ForEach (model => app.model(model); Models.foreach (model => app.model(model())); With respect to OK.

Data acquisition

The solution of data acquisition is to configure the route route-router-config, combine with matchRoutes, find the request interface required by relevant components on the page and execute the request, which requires the developer to explicitly inform the server of the request content through the route configuration information.

Modifying client routes

// router/index.js
{
  path: '/login'.exact: true.component: Login,
  loadData: Login.loadData, // Here is how to request data
  title: 'Login page'
}
Copy the code
// Used by the client component
class Index extends Component {}

Index.loadData = async (store) => {
  store.dispatch({
    type: "menuTree/reset"});console.log('Let me see if this loads.');
}
export default Index
Copy the code

Server code

// server/index.js

// The method to get the request
const promises = [];

matchedRoutes.forEach(item= > {
  if (item.route.loadData) {
    const promise = new Promise((resolve, reject) = > {
      // Then must be used in the component async or promiseitem.route.loadData(store).then(resolve).catch(reject) }) promises.push(promise); }});// One thing to note here is that your method may be asynchronous and ctx.body will not execute, so make this middleware asynchronous

// To ensure that the loadData method of the component is executed
await Promise.all(promises).then(() = > {
  const css = new Set(a);// Prevent the hook function from executing twice
  const insertCss = (. styles) = > styles.forEach(style= > css.add(style._getCss()));
  const helmet = Helmet.renderStatic();
  const content = renderToString(
    <Provider store={store}>
        <StaticRouter location={ctx.path}>
          <StyleContext.Provider value={{insertCss}}>
            {renderRoutes(routes.routes)}
          </StyleContext.Provider>
        </StaticRouter>
      </Provider>
     )
     ctx.body = renderHTML(content, store, css, helmet)
})
Copy the code

Water injection and dehydration

It involves pre-fetching data, which is the real meaning of server-side rendering.

The above code works fine, but the client and server stores are out of sync.

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 when rendering on the server side, the server first asks the interface to get the data, and handles the ready data state (in the case of Redux, store updates). To reduce client requests, we need to preserve this state. The common practice is to return the data json.stringify when the HTML string is returned by the server. This process is called waterflooding. On the client side, there is no need to request data, you can directly use the data sent from the server side, this process is called dehydration.

<script>
   window.context = {
   // Here is water injection
   state: ${serialize(store.getState())}  // serialize is to prevent XSS attacks
}
</script>
Copy the code
import {create} from 'dva-core';

function createApp(opt) {
  / /...
  return app;
}

// Server redux
const dvaApp = createApp({
  initialState: {},
  models: models,
});
export const getStore = () = > {
  return dvaApp.getStore();
}

// Client redux
export const getClientStore = () = > {
  // We need to get data from the server first
  const initialState = window.context ? window.context.state : {};
  const dvaClientApp = createApp({
    initialState,
    models: models,
  });

  return dvaClientApp.getStore();
}
Copy the code

Configure the agent

There is no domain on the server side, so there will be no cross-domain problem. However, there is also a cross-domain problem on the client fetching interface, so you need to configure the proxy, the code is as follows:

import httpProxy from 'http-proxy-middleware';
import k2c from "koa2-connect"

// Forwarding proxy
app.use(async (ctx, next) => {
  if (ctx.url.startsWith('/api')) { // Matches the request URL with the API field
    ctx.respond = false // Bypass the koA built-in object Response and write the original res object instead of the response handled by KOA
    await k2c(httpProxy({
        target: 'https://api.xxxxx.xxx'.changeOrigin: true.secure: false.pathRewrite: {
          '^/api': ' '
        }
      }
    ))(ctx, next);
  }
  await next()
})
Copy the code

You can also install koA’s proxy module, KoA2-proxy-Middleware, as follows:

const proxy = require('koa2-proxy-middleware');
const options = {
  targets: {
    '/user': {
      // this is option of http-proxy-middleware
      target: 'http://localhost:3001'.// target host
      changeOrigin: true.// needed for virtual hosted sites
    },
    '/user/:id': {
      target: 'http://localhost:3001'.changeOrigin: true,},'/api/*': {
      target: 'http://localhost:3001'.changeOrigin: true.pathRewrite: {
        '/passager/xx': '/mPassenger/ee'.// rewrite path
      }
    },
  }
}
app.use(proxy(options));
Copy the code

If you’re interested, check out KoA2-proxy-Middleware

The introduction of the react – helmet

Do more complete SEO

The App component is embedded in the document.getelementById (‘root’) node and generally does not contain the head tag, but single-page applications may also need to dynamically modify the head tag information, such as the title content, when switching routes. In other words, applying a switch page on a single page will not be rendered by the server, but we still need to change the title content of the document.

If you changed the title on the client, you could have used document.title directly, but now we need to do SEO well, and then we need to change the meta title in the server head and so on. Here we need to use react-Helmet.

The code is very simple

// Client implementation
import React, {Component, Fragment} from "react"
import {Helmet} from "react-helmet";

class Index extends Component {
  render() {
    return (
      <Fragment>
        <Helmet>
          <title>This is the login page</title>
          <meta name="description" content="Here's the Heckou and React-SSR survey."/>
        </Helmet>
      </Fragment>)}}Copy the code
// Server implementation
import Koa from 'koa';
import React from "react";
import Router from "koa-router"
import {renderToString} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom';
import {Helmet} from 'react-helmet'; // introduce here
/ /...
const app = new Koa();
const route = new Router()

route.get(["/:route?"./\/([\w|\d]+)\/.*/].(ctx) = > {
  / /...
  const helmet = Helmet.renderStatic(); // Get the current head information

  const content = renderToString(
    <StaticRouter location={ctx.path}>
      <StyleContext.Provider value={{insertCss}}>
        {renderRoutes(routes.routes)}
      </StyleContext.Provider>
    </StaticRouter>
  )
  
  
  ctx.body = ` <! DOCTYPE html> <html lang="zh-Hans-CN"> <head> <meta charset="utf-8">${helmet.title.toString()}
          ${helmet.meta.toString()}
          <link rel="shortcut icon" href="/favicon.ico">
          <style>${[...css].join(' ')}</style>
        </head>
        <body>
          <div id="root">${content}</div>
          <script src=/index.js></script>
        </body>
    </html>
  `
})
/ /... .
Copy the code

Request Token processing

When the client logs in, it puts the login token in the cookie of the browser and saves a copy in Redux. The cookie can be directly obtained from the requested page on the server. Therefore, when the user refreshes the page, the user can obtain the token through the page request, and then store a copy in redux, so that the client can obtain the token directly in Redux. The loadDate function can obtain the token through the second parameter.

404 pages

With the react — the router config matchRoutes method, when the capture is empty array, show that no current routing, jump to 404 pages, this point is there is a note that said if there is any level 2 or level 2 or more routing, this method can capture the first routing method, Therefore, check whether the current obtained route is a level-1 route and the current data cannot be empty.

// server/index.js  
/ / 404
let hasRoute = matchedRoutes.length === 1&&!!!!! matchedRoutes[0].route.routes
if(hasRoute || ! matchedRoutes.length) { ctx.response.redirect('/ 404');
  return;
}
// Adding '/' redirection is the same trick
Copy the code

Security issues

Security is critical, especially when it comes to server-side rendering, and developers need to be careful. Here’s a point: We mentioned the water injection and dehydration process earlier, where the code is:

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

Very vulnerable to XSS attacks, json.stringify may cause script injection, which is handled using the serialize-javascript library, which is one of the most overlooked details in homogeneous applications.

Another way to circumvent this XSS risk is to pass the data to a hidden TextaREA value on the page, and the Value of the TextaREA is not afraid of XSS risk.

To optimize the

  1. Client JS unpack, compress the code
  2. The js packaged by the client has a hash suffix
  3. Use copy-webpack-plugin to package the required files directly into the corresponding folder.
  4. Middleware forwarding proxy cross domain, etc
  5. Static resources use CDN
  6. The server uses caching
  7. Switch to client rendering when the strain on the server becomes too much
  8. NodeJs /ReactJs version upgrade

Summary of problems encountered

  1. The path to the static resource in the second level menu, with the path to the first level menu
  2. When importing CSS from server, CSS does hash processing and cannot load CSS correctly (CSsmodules).
  3. The server imports CSS at componentWillMount, not at componentDidMount, which is already on the client.
  4. Unlike express, koA routing cannot use ***** directly.
  5. CTX. Body = “” for sequential, and asynchronous middleware
  6. [Fixed] When react-Helmet is used, the server does not display Settings like title (imported in outermost layer)
  7. Note the difference and connection between the Redux client and server when waterflooding
  8. The history used by the client route, the path jump does not access the KOA route
  9. SSR deployment code volume is particularly large, add concurrent, common separate out, add CDN
  10. Problems with PM2 environment variables (reload history to be clear)
  11. Antd style is also compiled after cssModules is opened
  12. Add an array of constants that must be used for server rendering, not too many to affect performance.
  13. Jump from another page, why open the page source code has rendered HTML, should not only the first screen render? [Opening the console is like re-rendering]
  14. If the server obtains data, how does the client determine that it has obtained the data and does not call the interface again
  15. If you have CSS on the server, do you need CSS on the client?
  16. How to transmit and obtain SSR parameters?

The above problems, have been solved, the article may not be specific, specific to the source code.

Reference documentation

Build React SSR engineering architecture from zero to one

Zhihu rendertron

Github address for this article

Like the mark of 👍