preface

In fact, SSR technology is not a novel technology, said the simple point, is the server directly return HTML string to the browser, the browser directly parse HTML string to generate DOM structure, not only can reduce the number of first screen rendering requests, but also for the search engine spider crawl is very effective.

This time, we will introduce how to use react natively to implement SSR. Of course, there is a mature solution at present, which is to use next.js. The basic principle of this framework is the same.

  • What is the overall flow of the request?
  • How does the server return assembled asynchronous and synchronous data to the client?
  • htmlHow are methods that bind labels in strings bound?
  • SSRHow to load components on demand in scenarios?

If you are clear on the above questions, you can skip the following and try to build your own SSR framework. Of course, if in doubt, you can read on. If you want to view the source directly, here is the portal

What is the overall flow of the request?

Without further ado, first attach a flow chart, which is basically as follows:

It should be explained that the above diagram follows the first rendering process. Apart from the first rendering process, whether you use Node as a forwarding layer or call Java /phpapi directly depends entirely on your actual scenario, and this example is mainly built according to this process

The process is roughly divided into the following three parts:

The first process

The Node service obtains data from Java. The Node determines the initialization data of the route to be loaded based on the page route, and the non-route does not do any processing. The time spent in this phase mainly depends on the communication time between the Node service and Java service.

Second process

Node service will get the data and the basic structure of HTML assembled back to the client for parsing, and complete the loading process of HTML string chain JS resources, this stage mainly do two things:

  • The assembled data mainly includes:Ajax returns data,HTML infrastructure,The CSS initializes data,Meta, title and other information
  • htmlString tags on the method binding and external chain JS code execution

Third process

So in homogeneous code, componentDidMount life cycle triggers an Ajax request to get server-side data, so if it’s a routed page, you can make a decision that if there’s data locally, you don’t want to send a request, and if there’s data locally, you want to send a request.

Of course, there may be data synchronization issues involved here, and you can also design an API to tell the page whether to pull data again.

Project directory structure description

The main libraries involved are:

See the package.json file in the source code for more

  • react
  • react-loadable
  • react-router-config
  • redux-saga
  • axios
  • express
├ ─ ─ the build / / packaging │ ├ ─ ─ webpack. Base. Config. Js │ ├ ─ ─ webpack. Client. Config. Js │ └ ─ ─ webpack. The server config. Js ├ ─ ─ ├─ ├─ SRC // ├─ app.js // ├─ SRC // ├─ app.js // ├─ SRC // ├─ app.js // ├─ SRC // ├─ ├─ SRC // ├─ app.js // ├─ SRC // ├─ ├─ SRC // ├─ app.js // ├ ─ ─ assets / / need to introduce the project static resource ├ ─ ─ the components / / common components folder ├ ─ ─ components - hoc / / high order component folder ├ ─ ─ entry - client / / client entry folder │ └ ─ ─ Index. Js ├ ─ ─ entry - server / / server entry folder │ ├ ─ ─ index. The js │ └ ─ ─ renderContent. Js ├ ─ ─ public / / public function method ├ ─ ─ the router / / routing configuration folder ├ ─ ─ the static / / will be packaged directly generated to entry - client folder, not directly introduce projects ├ ─ ─ store / / share data store folder └ ─ ─ different page views / / folderCopy the code

Begin to build

The project uses saga middleware to manage Stroe, so the loadData and import store of defining components will be written differently, which will be described later.

Now that the process is clear and the general directory structure is clear, we can start to build our own SSR framework. When you start rendering on the server side, you have to understand the problem of code isomorphism, which is essentially a set of code. Since the server side is running on the client side, of course there will be different packaging logic, corresponding to the entry file on the client side and the entry file on the server side.

Client entry file

Start with entry-client/index.js, which is basically as follows:

import React, { Fragment } from 'react'
import ReactDom from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { renderRoutes } from 'react-router-config'
import Loadable from 'react-loadable'
import { configureClientStore } from '.. /store/index'
import routes from '.. /router'
import rootSaga from '.. /store/rootSagas'

const store = configureClientStore()
store.runSaga(rootSaga)

const App = (a)= > {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <Fragment>{renderRoutes(routes)}</Fragment>
      </BrowserRouter>
    </Provider>
  )
}
Loadable.preloadReady().then((a)= > {
  ReactDom.hydrate(<App />, document.getElementById('root'))
})
Copy the code

Here, the react-router-config renderRoutes method is used to render the route object. Because the route is defined in object form, the fragment code is basically as follows:

// src/router/index.js
export default[{path: '/'.component: App,
    key: 'app'.routes: [{path: '/'.component: Home,
        exact: true.loadData: Home.loadData,
        key: 'home'
      },
      {
        component: NotFound,
        key: 'notFound'}}]]Copy the code

The store imported here is not generated directly through createSagaMiddleware, but is a function configureClientStore, which carries a runSaga method, CreateSagaMiddleware ().run is executed again, and why this is introduced is further explained in the server entry file.

You will notice that the final dom binding to the page is reactdom.hydrate instead of the reactdom.render method, you will not have a problem with render, just performance loss. React source code also shows this. Hydrate and render method will be called legacyRenderSubtreeIntoContainer method, just different on the fourth parameter, hydrate and render were true and false, true, on behalf of the client need to reuse rendering DOM structure, For details, see the React code analysis.

This is why we started with the concept of isomorphism, since both the code server and client are rendered, there is a performance cost, and using reactdom.hydrate will at least reuse the client DOM structure and reduce the performance cost.

The Loadable on demand configuration is explained separately.

So far, the client entry file is basically clear, other configurations are more general, I will not do a detailed introduction.

Server entry file

The server side is a bit more complicated than the client side, because it involves retrieving asynchronous data and related synchronous data, and assembling data back to the client, roughly completing the process. The complete code is as follows:

import express from 'express'
import proxy from 'express-http-proxy'
import { matchRoutes } from 'react-router-config'
import { all } from 'redux-saga/effects'
import Loadable from 'react-loadable'
import { renderContent } from './renderContent'
import { configureServerStore } from '.. /store/'
import routes from '.. /router'
import C from '.. /public/conf'

const app = express()

app.use(express.static('build-client'))

app.use(
  '/api',
  proxy(`${C.MOCK_HOST}`, {
    proxyReqPathResolver: req= > {
      return `/api/` + req.url
    }
  })
)

app.get(The '*', (req, res) => {
  const store = configureServerStore()

  const matchedRoutes = matchRoutes(routes, req.path)
  const matchedRoutesSagas = []
  matchedRoutes.forEach(item= > {
    if (item.route.loadData) {
      matchedRoutesSagas.push(item.route.loadData({ serverLoad: true, req }))
    }
  })

  store
    .runSaga(function* saga() {
      yield all(matchedRoutesSagas)
    })
    .toPromise()
    .then((a)= > {
      const context = {
        css: []}const html = renderContent(req, store, routes, context)

      // 301 Redirection Settings
      if (context.action === 'REPLACE') {
        res.redirect(301, context.url)
      } else if (context.notFound) {
        / / 404 set
        res.status(404)
        res.send(html)
      } else {
        res.send(html)
      }
    })
})
Loadable.preloadAll().then((a)= > {
  app.listen(8000, () = > {console.log('8000 start')})})Copy the code

Get (‘*’) is used to match the client route, and then matchRoutes(routes, req.path) is used to match all the routes and subroutes under the current route.

matchedRoutes.forEach(item= > {
  if (item.route.loadData) {
    matchedRoutesSagas.push(item.route.loadData({ serverLoad: true, req }))
  }
})
Copy the code

Where is loadData defined later? The principle is to customize the wrapped component to be used by the server to get data asynchronously

By iterating through the matchedRoutes, find the loadData methods defined on the route, and use the matchedRoutesSagas to collect these methods. In fact, it is easy to understand here, which is to collect the data methods that should be loaded on all the routes involved in the first rendering, and then call them uniformly. Finally return.

In matchedRoutesSagas, loadData uses the store.runsaga () method, passing in the Generator function, so that the Generator function defined in saga can be called. That is, the matchedRoutesSagas collection, and store.runsaga () returns a task that has a toPromise method,

Res.send (); res.send(); res.send();

RenderContent: string concatenation: string concatenation: string concatenation: string concatenation: string concatenation: string concatenation: string concatenation: string concatenation

import React, { Fragment } from 'react'
import { renderToString } from 'react-dom/server'
import { StaticRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { renderRoutes } from 'react-router-config'
import { Helmet } from 'react-helmet'
import minify from 'html-minifier'
import Loadable from 'react-loadable'
import { getBundles } from 'react-loadable/webpack'
import stats from '.. /.. /build-client/react-loadable.json'

export const renderContent = (req, store, routes, context) = > {
  let modules = []
  constcontent = renderToString( <Provider store={store}> <StaticRouter location={req.path} context={context}> <Loadable.Capture report={moduleName => modules.push(moduleName)}> <Fragment>{renderRoutes(routes)}</Fragment> </Loadable.Capture> </StaticRouter> </Provider> ) let bundles = getBundles(stats, modules) const helmet = Helmet.renderStatic() const cssStr = context.css.length ? context.css.join('\n') : '' const minifyStr = minify.minify( ` <! DOCTYPE html> <html lang="en"> <head> ${helmet.title.toString()} ${helmet.meta.toString()} <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no"> <link rel="icon" href="/static/favicon.ico" type="image/x-icon"> <link href="/static/css/reset.css" rel="stylesheet" /> <script src="/static/js/rem.js"></script> <style>${cssStr}</style> </head> <body> <div id="root">${content}</div> <script> window.context = { state: ${JSON.stringify(store.getState())} } </script> ${bundles .map(bundle => { return `<script src="/${bundle.file}"></script>` }) .join('\n')} <script src='/client-bundle.js'></script> </body> </html> `, { collapseInlineTagWhitespace: true, collapseWhitespace: true, processConditionalComments: true, removeScriptTypeAttributes: true, minifyCSS: true } ) return minifyStr }Copy the code

The final method is renderToString, which converts store and routes into strings. The static route tag StaticRouter is also used on the official website. After all, the output is a string, and the server render route is stateless. Unlike BrowserRouter, which is used in the introduction client entry file above, it uses HTML5’s history API (pushState, replaceState, and PopState events) to keep the UI and URL in sync.

The StaticRouter passes the context parameter, which will appear as a staticContext on the props property in isomorphic code, as described later.

Context.css. length determines whether there is any style data content

JSON. Stringify (store.getState())) {context.state ();}} Because only state data is merged, asynchronous data is present on the Store, so that the component can use asynchronous data directly when rendering for the first time. The code will be posted later.

is defined in the string, which is connected to the client package file. This involves the second flow of the initial flow, and the js code on the client side is run on the browser side. Perform operations such as the lifecycle of the component and binding corresponding methods.

You can see that there is also a Loadable configuration here.

Isomorphic code content

This paper introduces the client entry file and server entry file, mainly uses a component as an example to introduce the corresponding processing in the following isomorphism code.

Open the SRC/view/home/index. Js and SRC/view/home/head/index js, fragments of source code is as follows:

// home/index.js
render() {
  const { staticContext } = this.props
  return( <Fragment> <Header staticContext={staticContext} /> <Category staticContext={staticContext} /> <ContentList staticContext={staticContext} /> <BottomBar staticContext={staticContext} /> </Fragment> ) } // head/index.js import InjectionStyle from '.. /.. /.. /components-hoc/injectionStyle' import styles from './index.scss' class Header extends Component { constructor(props) { super(props) this.state = {} } render() { const { staticContext } = this.props return ( <div className={styles['header']}> <SearchBar staticContext={staticContext} /> <img className={styles['banner-img']} src="//xs01.meituan.net/waimai_i/img/bannertemp.e8a6fa63.jpg" /> </div> ) } } Header.propTypes = { staticContext: PropTypes.any } export default InjectionStyle(Header, styles)Copy the code

If it is a nested component, you need to pass the staticContext along, otherwise the child component will not get the content of the staticContext, and the staticContext will be used by the server to collect and process the style data of the current component.

InjectionStyle(Header, styles);

import React, { Component } from 'react'
export default (CustomizeComponent, styles) => {
  return class NewComponent extends Component {
    componentWillMount() {
      const { staticContext } = this.props
      if (staticContext) {
        staticContext.css.push(styles._getCss())
      }
    }
    render() {
      return <CustomizeComponent {. this.props} / >}}}Copy the code

It’s simply wrapping the component and pushing the component’s style data into staticContext.css. You might ask, “Where is the CSS defined?” If you can understand the initial flow, it’s easy to imagine that the variables are defined before the server assembs the data. We then pass it down through the context parameter StaticRouter. You can view the contents of the server entry file.

In the isomorphic code there is a more key is the store configuration, entry file source code is as follows:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducers from './rootReducers'

const sagaMiddleware = createSagaMiddleware()

export const configureClientStore = (a)= > {
  const defaultState = window.context.state
  return {
    ...createStore(reducers, defaultState, applyMiddleware(sagaMiddleware)),
    runSaga: sagaMiddleware.run
  }
}

export const configureServerStore = (a)= > {
  return {
    ...createStore(reducers, applyMiddleware(sagaMiddleware)),
    runSaga: sagaMiddleware.run
  }
}
Copy the code

ConfigureClientStore (configureClientStore) and configureServerStore (configureClientStore). The only difference is that in configureClientStore there is a merge state operation. And the asynchronous data is defined on the Widnow object so that when the client code runs again, the method here will be executed, fetching the data rendering component directly from the merged store.

Loadable configuration

The react-loadable package is mainly used to implement on-demand loading. It is relatively tedious to add this configuration in SSR, but the official website has basically given detailed steps and detailed configuration process, of course, this source code has also been implemented, but in the configuration process need to pay attention to the point, still is the definition of loadData, as mentioned before, LoadData will only take effect after the component is wrapped, so I will define loadData in the routing configuration file, currently only through this implementation, I feel from the file organization is not very good, SRC /router/index.js source code basically as follows:

import Loadable from 'react-loadable'
import LoadingComponent from '.. /components/loading'
import { getInitData } from '.. /store/home/sagas'

const Home = Loadable({
  loader: (a)= > import('.. /views/Home'),
  modules: ['.. /views/Home'].webpack: (a)= > [require.resolveWeak('.. /views/Home')].loading: LoadingComponent
})

Home.loadData = serverConfig= > {
  const params = {
    page: 1
  }
  return getInitData(serverConfig, params)
}

const NotFound = Loadable({
  loader: (a)= > import('.. /views/NotFound'),
  modules: ['.. /views/NotFound'].webpack: (a)= > [require.resolveWeak('.. /views/NotFound')].loading: LoadingComponent
})

export default[{path: '/'.component: App,
    key: 'app'.routes: [{path: '/'.component: Home,
        exact: true.loadData: Home.loadData,
        key: 'home'
      },
      {
        component: NotFound,
        key: 'notFound'}}]]Copy the code

If you define loadData directly on Home and then use Loadable wrapper, loadData will not exist.

conclusion

Thanks to @delllee for his analysis and personal thoughts and related configurations. That’s all, not a detailed introduction to the code logic, just a few comparison points to describe, in fact, these can also answer the first four questions left. The specific details can be tested by referring to the source code. Although SSR can improve the first screen rendering and enhance THE SEO effect, it also increases the pressure on the server. You can also try using a pre-render scheme.

Afterword.

A previous look at the so-called 0.3s complete render presented a new architecture NSR. Below is the experience before and after optimization:

Below is the rendering flow design:

After reading the whole article, it is the clientAPPAs aSSROne advantage of this design is that clicking on any article in the news list will trigger itSSRRender (so much so that my phone heats up quickly using the UC browser, which turns out to be using me as a service), unlike what I originally introduced to usenodeserviceSSRRender is only triggered on the first screen, and as the article says,NSRYou could say distributedSSR.

It’s a little bit of a puzzle, actually, that eventually you need to haveajaxThe request exists, nothing more thanNativetoAPI serviceRequest data, but there will still be a white screen, there will be a request will appear white screen.

Unless the interaction is to render the 10 news list, secretly send a request to obtain the 10 news content, and then click on the article, the white screen will not appear, but the actual interaction cost is too high, I think it will not be adopted. Then the user can only trigger the click to pull the data, and then to the interface to display the data, this process feels that there will be a white screen.

Understand before there is an error, in fact, at the same time, is in the news in your loading list is set to obtain a unified content via ajax, which actually will have a certain flow loss, but if we can add figure paintings, through portrait can accurately determine which articles can be preloaded, which does not need to be loaded, so get can also increase the user experience to a certain extent.

Generally speaking, his idea is very good, worth learning. If you have a better understanding of this NSR, please leave a comment.

reference

  • react-router-config
  • React source code
  • react-loadable
  • pre-rendered
  • 0.3s render complete! Optimization practice of avoiding UC traffic text