With the introduction of more and more new front-end frameworks, the SSR concept is becoming more and more popular in the front-end development field, and more and more projects are implementing this technical solution. What is the background of SSR? What is the applicable scenario? How does it work? I hope you find the answers you are looking for in this article.

When it comes to SSR, many people think of it as “server render”, but I prefer to call it “isomorphism”, so let’s start with a simple analysis of “client render”, “server render”, and “isomorphism” :

Client rendering: For client rendering, there is no webpage display content in the HTML page initially loaded on the page, so the React code in the JavaScript file needs to be loaded and executed to generate the page through JavaScript rendering. Meanwhile, the JavaScript code will complete the binding of page interaction events. The detailed process can be seen in the following picture (taken from fullstackAcademy.com) :

Server-side rendering: The user requests the server, which generates the HTML content directly and returns it to the browser. With server-side rendering, the content of the page is generated by the Server. In general, server-side rendering has limited page interaction capabilities, and if you want to implement complex interactions, you still need to introduce JavaScript files to help with the implementation. Server-side rendering is a concept that can be applied to any backend language.

Isomorphism: Isomorphism is a concept found in new front-end frameworks like Vue and React, where isomorphism is actually a combination of client-side rendering and server-side rendering. We put the presentation and interaction of the page together and let the code execute twice. This command is executed on the server to implement server rendering and again on the client to take over page interaction. For details, see the following figure (based on fullstackAcademy.com) :

Normally, when we write code with React, the page is generated by the client executing JavaScript logic to dynamically hang the DOM, which means that this common single-page application is actually rendered in client-side mode. In most cases, client-side rendering is perfectly adequate for our business needs, so why do we need SSR as an isomorphic technology?

Main factors for the use of SSR technology:

  1. The TTFP (Time To First Page) of CSR project takes a long Time. Referring To the previous legend, in the Page rendering process of CSR, the HTML file should be loaded First, and then the JavaScript file required by the Page should be downloaded. The JavaScript file is then rendered to generate the page. There are at least two HTTP request cycles involved in this rendering process, so it takes a bit of time, which is why the initial page will appear white when you visit a normal React or Vue application at low network speeds.

  2. The SEO capability of CSR projects is extremely weak, and it is basically impossible to have a good ranking in search engines. Because at present most search engines mainly recognize the content or HTML, the recognition of JavaScript file content is still relatively weak. If a project’s traffic entry point is from a search engine, then it’s not appropriate to use CSR for development.

SSR is produced mainly to solve the two problems mentioned above. Using SSR technology in React, we let React code be executed first on the server side, so that the HTML downloaded by users already contains all the page display content. In this way, the page display process only needs to go through one HTTP request cycle, and TTFP time is more than doubled.

At the same time, because the HTML already contains all the content of the page, so the SEO effect of the page will become very good. After that, we let the React code execute on the client again, adding data and event bindings to the content of the HTML page, and the page has all the interaction capabilities of React.

But the idea of SSR is not easy to implement. Let’s take a look at the architecture of implementing SSR technology in React:

Using SSR as a technique would make a simple React project very complex, make the project less maintainable, and make it difficult to trace code problems.

Therefore, while using SSR to solve problems, it will also bring a lot of side effects. Sometimes, these side effects are much more harmful than the advantages brought by SSR technology. Speaking from personal experience, I generally recommend against using SSR unless your project is heavily dependent on search engine traffic or has specific requirements for first screen time.

Ok, if you do come across a scenario where SSR is used in the React project and you decide to use SSR, then let’s start the analysis of the difficulties of SSR technology points with the SSR architecture diagram above.

Before starting, let’s analyze the relationship between virtual DOM and SSR.

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

As mentioned above, React code is executed once on the client and once on the server in SSR projects. No problem, you might think. It’s all JavaScript code that runs in both the browser and the Node environment. But that’s not the case. If you have code in your React code that manipulates the DOM directly, then you can’t use SSR because there’s no DOM in Node, so it’s not possible to use SSR in Node.

Fortunately, the React framework introduced a concept called virtual DOM. Virtual DOM is a JavaScript object mapping of the real DOM. When React does page operations, it actually does not directly operate on the DOM, but instead operates on the virtual DOM. That is, manipulating ordinary JavaScript objects, which makes SSR possible. On the server, I can manipulate JavaScript objects, determine that the environment is the server environment, and we map the virtual DOM to string output; On the client side, I can also manipulate JavaScript objects to determine that the environment is the client environment, I will directly map the virtual DOM to the real DOM, complete the page mount.

Other frameworks, such as Vue, implement SSR by introducing the same virtual DOM technology as React.

Ok, now let’s go back to the flow chart, and forget the first two steps, server rendering must first send a request to the Node server. The important step is step 3. As you can see, the server has to determine what kind of page to display based on the requested address. This step is called server-side routing.

In step 10, when the client receives the JavaScript file, it determines which component is currently displayed in the browser based on the current path, and performs a client rendering again. At this time, it also goes through a client routing (front-end routing).

So, here’s the difference between server-side and client-side routing.

The difference between client rendering and server rendering routing code in SSR

To implement React’s SSR architecture, we need to have the same React code executed once on the client and once on the server. Note that the React code is the same as the component code we write, so in isomorphism, only component code can be shared, but routing code can’t be shared. Why? In fact, the reason is very simple, on the server side needs to find the routing component through the request path, and on the client side needs to find the routing component through the web address in the browser, are completely different mechanisms, so this part of the code is certainly not public. Let’s look at the implementation code for the front and back end routing in SSR:

Client routing:

const App = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <div>
          <Route path='/' component={Home}>
  		</div>
      </BrowserRouter>
    </Provider>
  )
}

ReactDom.render(<App/>, document.querySelector('#root'))
Copy the code

The client-side routing code is very simple, and I’m sure you’re familiar with it, and BrowserRouter is automatically displayed from the browser address, matching the corresponding routing component.

Server side routing code:

const App = () => {
  return 
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          <Route path='/' component={Home}>
        </div>
      </StaticRouter>
    </Provider>
}

Return ReactDom.renderToString(<App/>)
Copy the code

The server-side routing code is a bit more complicated, requiring you to pass the location (the current request path) to the StaticRouter component, so that the StaticRouter can figure out who the required component is based on the path. (PS: StaticRouter is a routing component that react-Router provides for server-side rendering.)

With BrowserRouter we can match the routing component that the browser is going to display. For the browser, we need to convert the component into a DOM, so we need to mount the DOM using the reactdom.render method. StaticRouter matches the component to be displayed on the server side. On the server side, we want to convert the component to a string. In this case, we just need to call renderToString provided by ReactDom to get the HTML string corresponding to the App component.

For a React application, the route is usually the entry point of the entire program. In SSR, the server side route is different from the client side route, which means the server side entry code is different from the client side entry code.

As we know, React code is packaged with Webpack to run. The code that runs in steps 3 and 10 is actually the code generated after the source code is packaged. As mentioned above, only part of the code in server and client rendering is consistent, and the rest is different. So, Webpack differently depending on where your code is running.

Packaging differences between server-side and client-side code

Write two Webpack configuration files as DEMO:

Client Webpack configuration:

{
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [{
      test: /\.js? $/, loader:'babel-loader'}, {test: /\.css? $/, use: ['style-loader', {
        loader: 'css-loader',
        options: {modules: true}}}, {test: /\.(png|jpeg|jpg|gif|svg)? $/, loader:'url-loader',
      options: {
        limit: 8000,
        publicPath: '/'}}}}]Copy the code

Server side Webpack configuration:

{
  target: 'node',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  externals: [nodeExternals()],
  module: {
    rules: [{
      test: /\.js? $/, loader:'babel-loader'}, {test: /\.css? $/, use: ['isomorphic-style-loader', {
        loader: 'css-loader',
        options: {modules: true}}}, {test: /\.(png|jpeg|jpg|gif|svg)? $/, loader:'url-loader',
      options: {
        limit: 8000,
        outputPath: '.. /public/',
        publicPath: '/'}}}};Copy the code

As we said above, in SSR, the Entry routing code is different between the server-side rendering code and the client-side code, so in Webpack, the Entry configuration must first be different.

Code that runs on the server side. Sometimes we need to introduce some core modules from Node. We need Webpack to be able to identify similar core modules when packaging them. In the Webpack configuration on the server side, you simply add the target: node configuration.

Server side rendering code, if the third-party modules are loaded, these third-party modules do not need to be packaged into the final source code, because the Node environment has already installed these packages via NPM, and they can be referenced directly without additional packaging into the code. To solve this problem, we can use the webpack-nod-externals plugin, the nodeExternals in the code refers to this plug-in, with which we can solve this problem. If you don’t understand the webpack-Nod-externals article or document, you will be able to understand the problem.

Moving on, when we introduce some CSS style code into our React code, the server-side packaging process will process the CSS once, and the client will process it again. Viewing the configuration, we can see that isomorphic-style-loader is used for server-side packaging. When processing CSS, it only generates the class name on the corresponding DOM element, and then returns the generated CSS style code.

In the client-side code packaging configuration, we use csS-Loader and style-loader. The CSS-Loader not only generates the class name in the DOM, parses the CSS code, but also mounts the code to the page using style-loader. However, since the style on the page is actually added by the client side during rendering, the page may have no style at the beginning. To solve this problem, we can get the style code returned by isomorphic-style-Loader when rendering on the server side. It is then added as a string to the server-side rendered HTML.

And for a type of document, such as image url – loader will be code on the server side and client code package respectively in the process of the package, here, I stole a lazy, regardless of package on the server or client package, I have packaged the generated files stored in the public directory, such, although file will pack out twice. However, the later package overwrites the previous one, so it still looks like there is only one file.

Of course, this is not very performance and elegant, just to give you a small idea, if you want to optimize, you can make the image packaging only once, with some Webpack plug-ins, it is not difficult to do this, you can even write your own loader, to solve this problem.

If you don’t have asynchronous data retrieval in your React app and just do static content display, you’ll find that a simple SSR app can be implemented very quickly with the configuration above. However, in a real React project, we would definitely have to have asynchronous data access, and most of the time, we would also use Redux to manage the data. If you want to do that in SSR applications, it’s not that easy.

Acquisition of asynchronous data in SSR + use of Redux

In client rendering, the use of asynchronous data combined with Redux follows the following process (corresponding to Step 12 in the figure) :

  1. Create the Store
  2. Display components by route
  3. Send the Action to get the data
  4. Update the data in the Store
  5. Component Rerender

On the server side, once the content of the page is determined, there is no way to Rerender. This requires that the data of the Store should be ready when the component is displayed. Therefore, the process of server-side asynchronous data combined with the use of Redux is as follows (corresponding to step 4 in the figure) :

  1. Create the Store
  2. Analyze Store data based on routes
  3. Send the Action to get the data
  4. Update the data in the Store
  5. Combine data and components to generate HTML that is returned once

Now, let’s look at the server-side rendering process:

  1. 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 code:
const store = createStore(reducer, defaultState)
export default store;
Copy the code

On the server side, however, this is a problem because the Store on the server side is used by all users. If you build the Store like this, the Store becomes a singleton, and all users share the Store, then obviously there is a problem. So in server-side rendering, the Store should be created like this, returning a function that is re-executed when each user accesses it, providing a separate Store for each user:

const getStore = (req) => {
  return createStore(reducer, defaultState);
}
export default getStore;
Copy the code
  1. Analyze the data required in the Store according to the route: In order to achieve this step, on the server side, we first need to analyze all the components to be loaded in the current way. At this time, we can use some third-party packages, such as React-router-config. For details about how to use this package, we don’t have to explain too much, you can check the documentation and use this package. Pass in the server request path and it will help you figure out all the components to display in that path.

  2. Next, we add a method for fetching data on each component:

Home.loadData = (store) => {
  return store.dispatch(getHomeList())
}
Copy the code

This method requires you to pass in the rendered Store on the server side, and its purpose is to help the server side Store get the data needed for the component. So, with this method on the component and all the components we need for the current route, we can call the loadData method on each component in turn to get all the data content we need for the route.

  1. Update the data in the Store: When we execute step 3, we are already updating the data in the Store, but we need to ensure that all the data is obtained before generating the HTML. How about this?
MatchedRoutes. ForEach (item => {if(item.route.loadData) { const promise = new Promise((resolve, reject) => { item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }}) Promise. All (promises). Then (() = > {} / / generates HTML logic)Copy the code

In this case, we use promises to solve this problem. We build a Promise queue and wait until all promises have been executed, which is when all store.Dispatchings have been executed, before generating HTML. In this way, we have realized the SSR process combined with Redux.

Above, we said that the page data is fetched by the loadData function during server-side rendering. Data acquisition, and on the client side, still need to be done, because if this page is your visit to the first page you see is the content of the server side rendering, but if after the react – the router routing jump the second page, then this page is completely renders the client, so the client also need to take data.

To get data on the client side, we use the way we are most used to, through componentDidMount to get data. Note that componentDidMount is executed only on the client side, not on the server side for this lifecycle function. So we don’t have to worry about componentDidMount colliding with loadData. This is why data acquisition should be placed in componentDidMount rather than componentWillMount to avoid conflicts between server and client data acquisition.

Node is just a middle tier

In the previous section we talked about getting data. In SSR architectures, Node is usually just a middle tier, used for server-side rendering of React code. The data needed by Node is usually provided by the API server alone.

This is done both for engineering decoupling and to avoid some of the computational performance issues associated with Node servers.

Please pay attention to steps 4, 12 and 13 in the diagram. We will analyze these steps next.

For server-side rendering, there is no problem with directly requesting data from the API server’s interface. However, there may be cross-domain problems on the client side. Therefore, at this time, we need to build Proxy function on the server side. The client does not directly request the API server, but requests the Node server, and obtains the data from the API server through Proxy forwarding.

Here you can use a tool like Express-http-proxy to help you quickly set up the proxy function, but remember to configure the proxy server to not only forward the request, but also carry the cookie, so that you don’t have permission verification problems.

// Node.use (app.use)'/api', proxy('http://apiServer.com', {
  proxyReqPathResolver: function (req) {
    return '/ssr'+ req.url; }}));Copy the code

Conclusion:

Here, the principle of key knowledge points in the whole SSR process system is connected in series. If you have applied the SSR framework before, I believe that the sorting of these knowledge points can help you very well from the principle level.

Of course, I also consider that most of the students who read this article may have very limited basic knowledge of SSR, and may be confused by the article. To help these students, I have written a very simple SSR framework. The code is put here:

Files.alicdn.com/tpsservice/…

Beginners combined with the above flow chart, step by step to sort out the logic in the flow chart, after combing, come back to read this article again, I believe you will suddenly be enlightened.

Of course, in the process of truly realizing SSR architecture, the difficulty is sometimes not the idea of implementation, but the details of processing. For example, how to set different titles and descriptions for different pages to improve your SEO performance. In this case, you can use a tool like React-helmet to help you achieve this goal. This tool is recommended for both client and server rendering. There are some such as the design of the engineering directory, 404,301 redirection and so on, but these problems, we only need to encounter in practice when one by one to break it.

Well, that’s all about SSR. I hope this article helped you a little.

Reference documentation

  • Webpack official site
  • What is React Server Side Rendering and should I use it?
  • StaticRouter
  • The Pain and the Joy of creating isomorphic apps in ReactJS

The article can be reproduced at will, but please keep the original link. If you are passionate enough to join ES2049 Studio, please send your resume to caijun.hcj(at)alibaba-inc.com.