What is the SSR

SSR is the full name of Server Side Rendering, and its Corresponding Chinese name is: Server Side Rendering, which means Rendering is done on the Server Side.

This approach has been around for a long time, long before Ajax came along, with the server returning the full HTML content to the browser. Once the browser has a complete structure, it can directly parse the DOM, build it, load resources, and render it.

SSR pros and cons

This way of page (HTML) straight out can make the first screen of the page quickly displayed to the user, friendly to search engines, crawler can easily find the content of the page, very conducive to SEO.

The disadvantage is that the loading of all pages requires the server to request complete page content and resources, which will cause certain pressure to the server when the traffic is large. In addition, the experience of frequent refreshing and skipping between pages is not very friendly.

What is the CSR

SSR is CSR, which stands for Client Side Rendering.

It is the mainstream rendering mode in The current Web application. Generally, the Server side returns the initial HTML content, and then JS asynchronously loads the data, and then completes the page rendering.

The most popular development mode in client-side rendering mode is SPA (single-page application), so the rest of this article will be based on SPA.

In this mode, the server only returns the frame and JS script resources of a page, but does not return specific data.

Advantages and disadvantages of CSR (SPA)

Jumping from page to page does not refresh the entire page, but only parts of it, which greatly improves the experience.

In a single page application, only when entering or refreshing for the first time, the server will be requested, only js CSS resources need to be loaded once, the routing maintenance of the page is in the client, the jump between pages is to switch related components, so the switching speed is fast, in addition, data rendering is completed in the client. The server only needs to provide an interface to return data, greatly reducing the strain on the server.

So the term Web app was coined to highlight how much the experience is like a Native app.

SPA client-side rendering is a great improvement in the overall experience, but it still has its drawbacks – SEO unfriendly, and pages can have long white screen times when first loading.

SSR VS CSR(SPA)

The orange part is the background color of the page, which corresponds to the white screen time in the conventional sense. It can be seen that SSR is faster than CSR in terms of the visible time of content.

This is because of the working principle of SSR, which determines its advantages. This difference will be more obvious in the weak network environment.

React SSR

Identify the problem: There are really only two questions

  • SEO is not friendly
  • First blank screen waiting.

Perfect combination of SSR + SPA

In fact, it is meaningless to only implement SSR, there is no development and progress in technology, otherwise SPA technology would not appear.

However, SPA alone is not perfect, so the best solution is a combination of these two experiences and technologies. The first visit to the page is server-side rendering, and the subsequent interaction based on the first visit is the SPA effect and experience, which does not affect the SEO effect, which is a bit perfect.

Simple SSR implementation is very simple, after all, this is a traditional technology, no language, just use PHP, JSP, ASP, Node and so on can be achieved.

However, in order to realize the combination of the two technologies, maximize the reuse of code (isomorphism) and reduce development and maintenance costs, front-end frameworks such as React or VUE combined with Node (SSR) are needed to achieve this.

This article focuses on React SSR technology, of course, vUE is the same, but the technology stack is different.

Core principles

In general, the React server rendering principle is not complicated, and the core content is isomorphism. (Isomorphism means that the front and back ends share a common set of code. For example, our component can be rendered on the server side or the client side, but it is the same component. This approach should be several blocks away from the traditional way.

Of course, there is another advantage to building a homogeneous app: both ends use the same language – javascript.

The Node server receives the request from the client, obtains the current REQ URL path, searches for the component in the existing routing table, obtains the requested data, and passes the data to the component as props, context, or store. RenderToString () or renderToNodeStream() are used to render components as HTML strings or streams. The data needs to be injected into the browser (water injection) before the final HTML output. After the server outputs (response), the browser can get the data (dehydration), and the browser starts rendering and node comparison. Then execute the component’s componentDidMount for event binding and some interaction within the component, the browser reuses the HTML nodes that the server outputs, and the process ends.

There are a lot of technical points, but most of them are architecture and engineering, which need to be linked and integrated.

Put an architecture diagram here

Two – end comparison mechanism

import ReactDOMServer from 'react-dom/server'
Copy the code

The ReactDOMServer class helps us render the component on the server side – to get the HTML string of the component.

ReactDOMServer.renderToString(element)
Copy the code

Render a React component as raw HTML. We can use this method to generate an HTML string on the server side, and then return the string to the browser side, completing the initialization of the page content, and enabling the search engine to crawl your page for SEO optimization purposes.

After the component is rendered on the server side, it is rendered again on the browser side to complete logic such as component interaction. When rendering, react calculates the component’s data-react-checksum property value on the browser side. If the value is found to be the same as that calculated by the server side, client rendering is not performed. Only when the comparison fails, the client content is used for rendering, and React tries to reuse as many existing nodes as possible. The data-react-checksum property is used to perform a two-end comparison of components.

If the props and DOM structures of the two components are the same, then the calculated value of this property is consistent.

If the props and DOM structure of a dual-rendered component are the same, the component will only be rendered once, and the client side will use the server rendered result, only for event binding and other processing, which will make our application have a very efficient first loading experience.

I. Isomorphism of routes

The React router will be used to process the route

import { StaticRouter} from 'react-router';
Copy the code

This component is mainly used for server-side rendering and can help us complete the route lookup function without manual matching.

The basic idea is to replace it with stateless.

Passes the path received on the server to this component for matching, and supports the passing in context feature, which automatically matches the target component for rendering.

The context property is a normal JavaScript object.

At component rendering time, attributes can be added to the object to store information about the render, such as 302, 404, and other result states, and the server can then respond specifically to different states.

2. On-demand routing based rendering

  • next.js

Automatic code segmentation by page, no configuration required.

  • Egg-react-ssr implementation

Egg + React + SSR server render

  {
      path: '/news/:id',
      exact: true,
      Component: () => (__isBrowser__ ? require('react-loadable')({
        loader: () => import(/* webpackChunkName: "news"* /'@/page/news'),
        loading: function Loading () {
          return React.createElement('div')
        }
      }) : require('@/page/news').default // Use this method to make the server bundle not to be packaged in chunks.'page',
      handler: 'index'
    }
Copy the code

Using the React-loadable library implementation, the server bundle is not packaged into multiple files, but remains a single file because the server handles static routes directly.

There is a pit in this configuration, so Loadable does not know in advance which components are wrapped, so there is no way to call loadable.preloadReady () directly to preload.

Manually call the component’s preload method with the preloadComponen method

The source code

import { pathToRegexp } from 'path-to-regexp'
import cloneDeepWith from 'lodash.clonedeepwith'
import { RouteItem } from './interface/route'

const preloadComponent = async (Routes: RouteItem[]) => {
  const _Routes = cloneDeepWith(Routes)
  for (let i in _Routes) {
    const { Component, path } = _Routes[i]
    let activeComponent = Component()
    if(Activecomponent.preload && pathToRegexp(path).test(location.pathName)) {// Get the real component only if the path you are accessing is the same as the component. Otherwise, return Loadable Compoennt to make the first screen not load these components activeComponent = (await Activecomponent.preload ()).default} _Routes[i].Component = () => activeComponent }return _Routes
}

export {
    preloadComponent
}
Copy the code

This method is then called during client rendering

Const clientRender = async () = > {const clientRoutes = await preloadComponent (Routes) / / client rendering | | hydrate ReactDOM[window.__USE_SSR__ ?'hydrate' : 'render'<BrowserRouter> {// Call getInitialProps clientroutes.map (({path, exact, Component }) => { const activeComponent = Component() const WrappedComponent = getWrappedComponent(activeComponent) const Layout = WrappedComponent.Layout || defaultLayoutreturn <Route exact={exact} key={path} path={path} render={() => <Layout><WrappedComponent /></Layout>} />
        })
      }
    </BrowserRouter>
    , document.getElementById('app'))

  if (process.env.NODE_ENV === 'development' && module.hot) {
    module.hot.accept()
  }
}
Copy the code

Our front end is react-loadable

The service side

import React from 'react'; Static router import {StaticRouter} from'react-router-dom';
import ReactDOMServer from 'react-dom/server';
import Loadable from 'react-loadable'; Import {getBundles} from from the react-loadable server'react-loadable/webpack'; //webpack's json file import stats from'.. /build/react-loadable.json'; // The react-router route is removed so that the import AppRoutes from can be shared between the browser and server'src/AppRoutes'; Class SSR {render(url, data) {render(url, data) {render(url, data) {letmodules = []; const context = {}; const html = ReactDOMServer.renderToString( <Loadable.Capture report={moduleName => modules.push(moduleName)}> <StaticRouter location={url} context={context}> <AppRoutes initialData={data} /> </StaticRouter> </Loadable.Capture> ); // Get the array of components rendered by the serverlet bundles = getBundles(stats, modules);
  return{ html, scripts: this.generateBundleScripts(bundles), }; GenerateBundleScripts (bundles) {// Convert SSR components into script tags and throw them into HTML.return bundles.filter(bundle => bundle.file.endsWith('.js')).map(bundle => {
   return `<script type="text/javascript" src="${bundle.file}"></script>\n`;
  });
 }

 static preloadAll() {
  returnLoadable.preloadAll(); }}export default SSR;

Copy the code

When compiling this file, use target: “node” and externals in the Webpack configuration, and add the React-loadable plugin to the webpack configuration of your packaged front-end app


const ReactLoadablePlugin = require('react-loadable/webpack')
 .ReactLoadablePlugin;

module.exports = {
 //...
 plugins: [
  //...
  new ReactLoadablePlugin({ filename: './build/react-loadable.json',]}})Copy the code

Add loadable plugin to.babelrc:

{
 "plugins": [
   "syntax-dynamic-import"."react-loadable/babel"["import-inspector", {
    "serverSideRequirePath": true}}]]Copy the code

Configuration publicPath:/${settings.projectName}/${settings.staticVersion}/.

Path simply tells Webpack where the results are stored, whereas publicPath is used by many Webpack plug-ins to update url values embedded in CSS and HTML files in production mode.

The above configuration lets react-Loadable know which components end up being rendered on the server side, and then insert them directly into the HTML script tag. It also takes SSR components into account during front-end initialization to avoid reloading

SSR packaging

//webpack.ssr.js
const nodeExternals = require('webpack-node-externals');

module.exports = {
 //...
 target: 'node',
 output: {
  path: path.resolve(settings.ssrOutDir),
  filename: `[name].js`,
  chunkFilename: `[name][chunkhash].js`,
  libraryExport: 'default',
  libraryTarget: 'commonjs2'}, // Avoid packing all the libraries in node_modules. This SSR js will run directly on the node side, so it doesn't need to be packaged into the final file. Externals will be loaded automatically from node_modules at runtime: [nodeExternals()], //... }Copy the code

Tread pit remember:!! At the beginning, the SSR configuration was as follows. The server kept reporting errors and could not find JS, but it was successfully packaged. Later, it changed to path to add JS /

 filename: `js/[name].js`,
 chunkFilename: `js/[name][chunkhash].js`,
Copy the code

Third, data processing

1. Data prefetch

Learn about the implementation of industry frameworks like Next-js and Egg-react-SSR.

Of course, there is also UMI, but the core part of UMI SSR code is also contributed by the Egg-React-SSR team

  • Next. Js data prefetch code
import React from 'react'

export default class extends React.Component {
  static async getInitialProps({ req }) {
    const userAgent = req ? req.headers['user-agent'] : navigator.userAgent
    return { userAgent }
  }

  render() {
    return (
      <div>
        Hello World {this.props.userAgent}
      </div>
    )
  }
}
Copy the code

The data is loaded when the page is rendered, using an asynchronous method called getInitialProps. It gets data asynchronously and binds to props. When the service renders, getInitialProps will serialize the data, just like json.stringify.

When the page is first entered,getInitialProps is executed only on the server side. The client only executes getInitialProps when a route jump (Link component jump or API method jump) occurs.

Also, this method can only be used within a page component, not within a child component.

  • Egg-react – SSR data prefetch code
import React from 'react'
import { Link } from 'react-router-dom'
import './index.less'

function Page (props) {
  return (
    <div className='normal'>
      <div className='welcome' />
      <ul className='list'> {props. News && props. News.map (item => (<li key={item.id}> <div> article title: {item.title}</div> <div className='toDetail'><Link to={`/news/${item.id}</Link></div> </li>))} </ul> </div>)} Page. GetInitialProps = async (CTX) => {// SSR render mode only gets data from Node on the server, The CSR rendering mode only gets the data from the client through HTTP requests, and the getInitialProps method is executed only once during the page life cyclereturn __isBrowser__ ? (await window.fetch('/api/getIndexData')).json() : ctx.service.api.index()
}

export default Page
Copy the code

When the page is initialized, the server determines which component to render based on the requested path. GetComponent is a method to find a matching component in the routing table based on the path and check whether the component has the getInitialProps static method. So you don’t have to instantiate it to get the method.

If so, this method is called, passing the data as props for the component so that the component can read the data to the server via props. XXX.

The component added the static method getInitialProps. The server called the matchRoute method based on the path of the current request to find the corresponding route and obtain the specific component. Then it determined whether the getInitialProps method exists on the component and prefetched the data.

Finally, the data is used as props of the component, which can be obtained in the component via the props. InitialData fixed property.

On the whole, the implementation of this app is very similar to egg-react-SSR, Next-js, which is probably the default common practice in the industry.

2. Data dehydration

Take a look at the way the server exports data from the runtime page.

  • next.js

After the data is directly exported to the page, it is wrapped by script tag with type=”application/json”, and the tag contains JSON data directly.

  • egg-react-ssr

Also loaded as a script, the data is stored in the window.__initial_data__ global variable.