Github address of this project react-KOA2-SSR

React16.x + React-Router4.x + koA2.x

preface

Some time ago, I did a simple ancient text website in my spare time, but the project used React SPA rendering, which was not conducive to SEO, so I had the need for server rendering. Later, I want to write a demo to summarize the whole process and deepen my understanding of it. During this period, due to work, the process is intermittent. Anyway, that was the project. Regarding the pros and cons of server rendering, vue server rendering official documentation is the most clear. Say it most clearly. For most scenes, the most important two points are to improve the loading speed of the first screen and facilitate SEO. To quickly build a development environment, create a base project directly using create-React-app and koA2.x. The whole project is developed based on this. At present, only the most basic requirements have been completed. There are still many bugs and places that can be optimized.

Server rendering the most basic theoretical knowledge comb

The front and back ends were quickly generated using scaffolding from create-React-app and KOA2, respectively, and then the two projects were merged together. This saves us some of the tedious configuration of WebPack, and the server uses Babel compilation. By default, you already know webPack and KoA2.x and Babel. Let’s get right to the important part. In my view, there are only three main points to build a React SSR environment: the first is the rendering API provided by the React server, the second is the isomorphism of the front and back end routes, and the third is the isomorphism of initializing asynchronous data. So this simple demo will focus on these three things.

  • React server render conditions
  • The react-Router4. x and KOA2. x routes are isomorphic
  • Redux initial data isomorphism

React server render conditions

See Chapter 7 of The React Tech Stack for more details. In a nutshell, React can do server-side rendering because ReactDOM provides an API for server-side rendering

  • RenderToString converts a React element into an HTML string with reactid.
  • RenderToStaticMarkup converts to an HTML string without reactid, which saves a lot of reactid if it is static text. With these two methods in place, React can actually be thought of as a template engine. Parse JSX syntax into plain HTML strings.

We can call both apis to pass in the ReactComponent and return the corresponding HTML string to the client. After the browser receives the HTML, it does not re-render the DOM tree, but does events binding and so on. This improves the performance of the first screen load.

The routes on the react- Router4. x and the server are isomorphic.

React – Router4. x Has major changes compared with the previous version. The entire route becomes componentized. You can focus on the detailed examples and documentation provided here as a standard reference for the basic ideas.

The difference between server rendering and client rendering is that the route is stateless, so we need to wrap the APP with a stateless Router component and match the specific route array and its related attributes through the URL requested by the server. So we use BrowserRouter on the client side and stateless StaticRouter on the server side.

  • BrowserRouter uses HTML5’s history API (pushState, replaceState, and PopState events) to keep the UI and URL in sync.
  • StaticRouter is a router component that does not change addresses. The reference code is shown below:
// Server routing configuration import {createServer} from'http'
import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { StaticRouter } from 'react-router'
import App from './App'

createServer((req, res) => {
  const context = {}

  const html = ReactDOMServer.renderToString(
    <StaticRouter
      location={req.url}
      context={context}
    >
      <App/>
    </StaticRouter>
  )

  if (context.url) {
    res.writeHead(301, {
      Location: context.url
    })
    res.end()
  } else{ res.write(` <! doctype html> <div id="app">${html}</div>
    `)
    res.end()
  }
}).listen(3000)
And then the client:import ReactDOM from 'react-dom'// Client routing configuration import {BrowserRouter} from'react-router-dom'
import App from './App'

ReactDOM.render((
  <BrowserRouter>
    <App/>
  </BrowserRouter>
), document.getElementById('app'))

Copy the code

We pass in the KOA route URL, which will automatically match the React component based on the URL, so we can refresh the page and the server will return the same route component as the client. At this point we have achieved page refresh consistency between the server and the client.

Redux server isomorphism

Under the first official document made simple introduction to introduce cn.redux.js.org/docs/recipe…

The processing steps are as follows:

  • 1. We obtained the corresponding asynchronous method according to the corresponding server request API to obtain the asynchronous data.
  • Generate an initialized store using asynchronous dataconst store = createStore(counterApp, preloadedState).
  • 3 Then callconst finalState = store.getState()Method to get the initialization state of the store.
  • 4 Pass the initial initState as a parameter to the client
  • 5 During client initialization, check whether there is data under Window. INITIAL_STATE. If there is data, re-generate a client store as the initial data. As shown in the code below.

The service side

 <html>
      <head>
        <title>Redux Universal Example</title>
      </head>
      <body>
        <div id="root">${html}</div>
        <script>
          window.__INITIAL_STATE__ = ${JSON.stringify(finalState)}
        </script>
        <script src="/static/bundle.js"></script>
      </body>
    </html>    
Copy the code

The client

. Const preloadedState = window.__initial_state__ // Use the initial state to create Redux store const store = createStore(counterApp, preloadedState) render( <Provider store={store}> <App /> </Provider>, document.getElementById('root'))Copy the code

This is basically a standard redux isomorphic process, in fact, more official is to provide us with a standardized idea, we can follow this to do more optimization. First of all, we don’t need to have a set of asynchronous loading methods for the server and the client directly through the API, which is very redundant. The react-router package provides react-router-config, which is used to configure static routes. The provided matchRoutes API returns an array of routes based on the incoming URL. You can use this method to directly access the React component on the server. If you want to get asynchronous methods directly from a route, I’ve seen a lot of similar isomorphism schemes,

  • There are two main methods: one is to add a thunk method to the route directly, through which to directly get the asynchronous data initialization, I think the advantage is relatively straightforward, directly in the routing layer to solve the problem.
  • The second is to use the static method of class. We can route to the static method under the component’s class. In this way, we can declare both the server and client initializers inside the container component.

This project uses the second option. Let’s take a look at the code:

/ / module.exports. Render = async(CTX,next) =>{const {store,history} = getCreateStore(ctx);
    const branch = matchRoutes(router, ctx.req.url);
    const promises = branch.map(({route}) => {
        const fetch = route.component.fetch;
        return fetch instanceof Function ? fetch(store) : Promise.resolve(null)
    });
    await Promise.all(promises).catch((err)=>{
        console.log(err);
    }); 

    const html = ReactDOMServer.renderToString(
                <Provider store={store}>
                            <StaticRouter
                            location={ctx.url}
                            context={{}}>
                                <App/>
                            </StaticRouter>
                </Provider>
        )
        let initState=store.getState();
        const body =  layout(html,initState);
   ctx.body =body;
}

Copy the code

The corresponding container component provides a static FETCH method

class Home extends Component {
  ...
  static fetch(store){
        return store.dispatch(fetchBookList({page:1,size:20}))
  }

Copy the code

So this is our actions

@param {*} param */export const fetchBookList = (params) => {
    return async (dispatch, getState) => {
        await axios.get(api.url.booklist, {
            params: params
        }).then((res) => {
            dispatch(booklist(res.data.result));
        }).catch((err) => {

        })
    }
}
Copy the code

First we get all the routes under the current route through matchRoutes, and then we iterate over them to get a Promise array for an asynchronous method, which in this case means an asynchronous method in actions. Since we also initialize store on the server side we can call actions directly on the server side, Here we need to pass a store to the static method of the container component so that we can call actions via store.dispatch(fetchBookList({page:1,size:20})). The above method yields an array of promises. We use promise.all to execute all asynchronously. At this point, the store is actually running the same as the client. We write all the initial data to the store in the asynchronous process. So we can get the initialization data with Store.getState (). The client initialization is the same as in the official Redux example. Determines whether initialization state is passed in directly, and if so, as initialization data. How do we avoid duplication when we initialize asynchrony on the server side and when we initialize asynchrony on the client side? So we’re going to get the initial data in the store, see if it exists, and if it doesn’t we’re going to load it.

At this point we can implement asynchronous data server processing of page refresh, without page refresh front-end processing, a basic isomorphic scheme body comes out, and the rest is some optimization items and some project customization things.

Server page distribution

Import {matchPath} from ‘react-router-dom’; import {matchPath} from ‘react-router-dom’; We use the matchPath API in the React-router-dom package to match whether the current requested route is the same as our client’s route configuration. If not, we default to requesting a static resource or something else. If the current route does not match we simply execute next() to go to the next middleware. Because our project is really still a front and back end separation project with the addition of server-side rendering. If the server needs to handle additional requests, we can add additional routes through the server, mapping to match the corresponding render page and API.

other

Writing this demo, I read a lot of github project and related articles. These materials are very inspiring for this project

Vue. Js server side rendering guide

react-server

beidou

react-ssr-optimization

React-universal-ssr

fairy

D2 – Build a highly reliable and high-performance React isomorphic solution

Egg + React server rendering development guide

Server render with Universal React App

React isomorphism provides optimization summary

React Mobile Web extreme optimization

github.com/joeyguo …

conclusion

We know that the advantage of server-side rendering is that it can optimize the first screen very quickly, supports SEO, and has a more data processing method compared with traditional SPA. The downside is that server-side rendering is equivalent to porting parts of the client’s process to the server, which increases the load on the server. Therefore, to make a good SSR scheme, caching is essential. At the same time, the engineering aspect also has a lot to be optimized. Here is just a taste, and did not do the relevant processing, estimated that there is time to do some optimization welcome your attention.

The github address of this project is github.com/yangfan0095…

The above で す