React16 + Redux + router4 + koa + webpack server side rendering (load on demand, hot update)

Time:2021-4-19

Project structure chart

React16 + Redux + router4 + koa + webpack server side rendering (load on demand, hot update)
The main construction ideas of this project are as follows:

  1. The development environment uses webpack dev server as the back-end server to realize the hot update without refreshing the page, including the hot update of component and reducer changes.
  2. In the production environment, KOA is used as the back-end server to share the creatapp code with the front-end. After packaging, the creatapp method is obtained by reading the file. Then, the code is separated on demand through react loadable, and the initial data is requested before rendering, which is put into the home page.

GitHub address: https://github.com/wd2010/React-universal-ssr

Code structure

The front end uses react + Redux + router4, in which Redux thunk is used to process asynchronous actions. The front-end and back-end share configurestore and create app, as well as the front-end routing configuration routesconfig required by the back-end, so they are exposed in one file.

export default {
  configureStore,
  createApp,
  routesConfig
}
among configureStore.js For:
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from 'history/createMemoryHistory';
import {  routerReducer, routerMiddleware } from 'react-router-redux'
import rootReducer from '../store/reducers/index.js';

Const routerreducers = routermiddleware (createhistory()); // route
const composeEnhancers = process.env.NODE_ENV=='development'?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers];

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

I put router into reducer

Const routerreducers = routermiddleware (createhistory()); // route
const middleware=[thunkMiddleware,routerReducers];

In this way, the router information can be read directly in the reducer, and it does not need to be transferred from the component layer to layer.

createApp.js
import React from 'react';
import {Provider} from 'react-redux';
import Routers from './router/index';
import Loadable from 'react-loadable';

const createApp=({store,history,modules})=>{
  if(process.env.NODE_ENV==='production'){
    return (
      <Loadable.Capture report={moduleName => modules.push(moduleName)}>
        <Provider store={store}>
          <Routers history={history} />
        </Provider>
      </Loadable.Capture>
    )
  }else{
    return (
      <Provider store={store}>
        <Routers history={history} />
      </Provider>
    )
  }
}

export default createApp;

The history used by the front end is as follows:

import createHistory from 'history/createBrowserHistory';
let history=createHistory();

The history used by the back end is:

import createHistory from 'history/createMemoryHistory';
let history=createHistory();

Development version hot load update

if(process.env.NODE_ENV==='development'){
  if(module.hot){
    module.hot.accept('./store/reducers/index.js',()=>{
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      /*import('./store/reducers/index.js').then(({default:module})=>{
        store.replaceReducer(module)
      })*/
    })
    module.hot.accept('./app/index.js',()=>{
      let {createApp}=require('./app/index.js');
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      let application=createApp({store,history});
      hydrate(application,document.getElementById('root'));
      /*import('./app/index.js').then(({default:module})=>{
        let {createApp}=module;
        import('./store/reducers/index.js').then(({default:module})=>{
          store.replaceReducer(module)
          let application=createApp({store,history});
          render(application,document.getElementById('root'));
        })
      })*/
    })
  }
}

It includes hot update of components and hot update of reducer. When importing changed files, you can use require or import.

Front end DOM node generation

const renderApp=()=>{
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
}

window.main = () => {
  Loadable.preloadReady().then(() => {
    renderApp()
  });
};

Among them Loadable.preloadReady () is a “react loadable” writing method, which is also used in server rendering.

Router4 dynamic on demand loading

This project uses react loadable to load on demand.

const Loading=(props)=>
  <div>Loading...</div>

const LoadableHome = Loadable({
  loader: () =>import(/* webpackChunkName: 'Home' */'../../containers/Home'),
  loading: Loading,
});
const LoadableUser = Loadable({
  loader: () =>import(/* webpackChunkName: 'User' */'../../containers/User'),
  loading: Loading,
});

const routesConfig=[{
  path: '/',
  exact: true,
  component: LoadableHome,
  thunk: homeThunk,
}, {
  path: '/user',
  component: LoadableUser,
  thunk: ()=>{},
}];

It can be used not only in routing, but also in component dynamic import (). A component can load components on demand dynamically.thunk: homeThunkThe first method is to request the initial data of the home page before entering the home page, and then render it to the front-end. The other method is to request the initial data when the server enters the user page and jumps from the user page to the home page. In this case, it is requested when the front-end component componentdidmount, Therefore, in order to use this method, requests are sent to the jump route, whether it is from the front link or from the server.

export const homeThunk=store=>store.dispatch(getHomeInfo())
//Simulate dynamic request data
export const getHomeInfo=()=>async(dispatch,getState)=>{
  let {name,age}=getState().homeInfo;
  if(name || age)return
  await new Promise(resolve=>{
    let homeInfo={name:'wd2010',age:'25'}
    console.log ('--- request gethomeinfo')
    setTimeout(()=>resolve(homeInfo),1000)
  }).then(homeInfo=>{
    dispatch({type:GET_HOME_INFO,data:homeInfo})
  })
}

And the server side is through thereact-router-configOfmatchRoutesTo match the current URL and routesconfig

let branch=matchRoutes(routesConfig,ctx.req.url)
let promises = branch.map(({route,match})=>{
    return route.thunk?(route.thunk(store)):Promise.resolve(null)
  });
await Promise.all(promises)

Koa render rendertostring

Through the front-end exposed createapp, configurestore and routesconfig, the front-end HTML page needs the rootstring string to be rendered through the rendertostring method. Combined with on-demand loading:

let store=configureStore();
let history=createHistory({initialEntries:[ctx.req.url]});
let rootString= renderToString(createApp({store,history,modules}));

When listening to the port in the koa server entry file, use react loadable:

Loadable.preloadAll().then(() => {
  app.listen(port)
})

In this way, KOA back-end rendering can be loaded dynamically on demand.

React16 + Redux + router4 + koa + webpack server side rendering (load on demand, hot update)

However, the dynamic generated HTML does not exist User.js Of:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>yyy</title>
  <link href="/css/style.7dae77f648cd2652a570.css" rel="stylesheet"></head>
  <body>
    <div id="root"></div>
    <script type="text/javascript"></script>
    <script type="text/javascript"></script>
    <script type="text/javascript"></script>
  </body>
  <script>window.main()</script>
</html>

In each refresh, localhost already contains all the contents of the first screen, which solves the problems of first screen white screen and SEO Search.

epilogue

After finishing this exercise, I was thinking that when the code is compiled and the server requests the data needed for the first screen before rendering, there will be a short white screen. In fact, the problem of white screen has not been solved at this time, so can I request all the data needed for the home page when compiling the code? I also think that the compilation process at this time takes a lot of time, and requests the data that could have jumped in the front-end route. All the first screen white screen problems seem to be solved, but there are better solutions.

React16 + Redux + router4 + koa + webpack server side rendering (load on demand, hot update)

Because I’m also the first time to make react server rendering, many places are made by referring to the practice of the great gods, and there are many places I don’t know, please give me more advice, complete code inhttps://github.com/wd2010/React-universal-ssr