Server Side Rendering is not a complex technology. Server Rendering and Server isomorphic Rendering are two different concepts, focusing on: Isomorphism, it is not a simple thing to make a set of code run perfectly on the browser and server, and there is no particularly satisfactory scheme in the industry at present, which requires more or less differentiated treatment for different environments.

The goal and meaning of isomorphic rendering

Usually isomorphic rendering is mainly for:

  • Conducive to SEO search engine collection
  • Speed up first screen display time
  • At the same timeSingle page (SPA)andMultipage routingUser experience

Usually isomorphic rendering needs to do:

  • Browsers and servers reuse the same set of code.
  • The first page the user visits (the first screen) is rendered by the server for SEO and faster rendering.
  • After the first screen is rendered by the server, the browser builds on it without redoing the work, including requesting the data again.
  • Any other pages the user visits are no longer rendered by the server to reduce server stress and reachSingle page (SPA)User experience.
  • To refresh the browser during a later interaction, you need to keep the current page and re-render it by the serverMultipage routingUser experience.

Isomorphic rendering difficulty with the golden key

Get initialization data

Homogeneous when rendering the main difficulty is that the Client end of the rendering component lifecycle hooks bearing too many functions and side effects, such as: to get the data, routing, on-demand loaded, modularization, etc, these logic are scattered in various components with the rendering dynamic execution of components, and their execution and cause a component to render them again. To put it simply:

Render -> Hooks -> Effects -> ReRender -> Hooks -> Effects…

This rendering process is not possible on the Server side because Sever will not ReRender normally, so all side effects must be performed in advance and then Render in one time.

Effects -> State -> Render

The solution is to take these side effects as far as possible out of the component lifecycle hooks and introduce a separate state management mechanism to manage them, making UI rendering pure PrueRender, which is the state-driven concept advocated by @medux.

Asynchronous load on demand

When rendering on the Client side, we often subcontract the code chunk to improve loading speed and use asynchronous loading on demand to optimize the user experience. On the Server side this becomes unnecessary and slows down the load. How do I replace asynchronous code with synchronous code on the server side? Just as @medux sees module loading as a configuration strategy, it makes it easy to switch between synchronous and asynchronous module loading.

Run the Demo

This project forks from medux-react-admin, a single page WEB application developed using medux +React+Antd4+Hooks+Typescript. You can see from this project how to quickly convert a SinglePage(SinglePage application) into a multi-page application with SEO support.

Project address: Medux-React-SSR

  • The online preview

Open the following page and right-click “View page source” to see if Html is output

  • /login
  • /register
  • /article/home
  • /article/service
  • /article/about

The installation

Trigger trigger Git config --global core. Trigger trigger Git config -- trigger trigger Git config --global corefalse
git clone https://github.com/wooline/medux-react-ssr.git
cd medux-react-ssr
yarn install
Copy the code

Run in development mode

  • runyarn start, will automatically start a development server.
  • In development mode, use the latest React Fast Refresh solution and install the latest React Developer Tools.

Run in production mode

  • Running YARN build-local first compiles the code to the /dist/local directory
  • Then go to the /dist/local directory and run Node start.js, which will start a production server Demo, but it is recommended to use Nginx to run it online. The output directory contains Nginx configuration samples for reference

Description of main transformation steps

Identify goals and tasks

This is a typical background management system, the page is mainly divided into two types:

  • The first type is visible after the user logs in, such as /admin/ XXX
  • The second type is non-login: /article/ XXX

The reason why we want to use SSR to transform it is mainly in order to make the second type of page can be caused by search included (SEO), and for the first type of page, because users need to log in, so it is not meaningful for the search engine, we still use pure browser rendering.

Two entries, one set of code, two sets of output

Differentiated start entry

Given the isomorphism, we certainly don’t want to do too much differentiation for both platforms, but there will still be a little custom code. For example, the start entry, which used to be./ SRC /index.ts, now needs to be distinguished as:

  • Client.ts the original browser-side entry file that builds the app using the buildApp() method
  • Server.ts adds server-side entry files to build applications using the buildSSR() method

With these two different portals, we focused on building some ShiMs and smoothing out some platform differentiation.

Distinguish between webPack build configurations

Code running on Sever doesn’t need to load asynchronously on demand, handle CSS, images, etc., so we use two webpack configurations to compile and package and export to dist/client/ and dist/server/ respectively.

The output from the Sever side is actually a single main.js file.

Compile and run

How do I deploy and run compiled output code? This project has written a simple express sample for reference. The directory structure looks like this:

Dist ├─ package.json // Run the required rely On ├─ start.js // nodejs start up import ├─ pm2.json // Pm2 deployment Configuration ├─ nginx.conf // Nginx configuration sample ├─ Env. json // Run environment variables to Configure ├─ 404. HTML // 404 Error page ├─ 50x.html // 500 Error page ├─ index.html // SSR Template ├─ Mock mock Data directory ├─ │ ├─ ├─ register.html │ ├─ article ├─ server │ ├─ main.js // SSR main program code ├ ─ ─ client / / client side output directory │ ├ ─ ─ CSS / / generate the CSS file directory │ ├ ─ ─ imgs / / without engineering processing the images directory │ ├ ─ ─ media / / the webpack │ the processed images directory ├ ─ js // browser run js directoryCopy the code
  • Dist/Client is a static directory that you can deploy using Nginx

  • Dist/Server is a JS Module file main. JS, which has only one default export method:

    export default function render(
      location: string
    ) :Promise<{
      html: string | ReadableStream<any>;
      data: any;
      ssrInitStoreKey: string; } >.Copy the code

    You can execute it using any Node server (such as Express) and get rendered data, HTML, and keys for dehydrated data. How you want the server to output these results, and how you handle exceptions and errors during execution, is up to you. For example:

Process initialization data

Effects -> State -> Render: Effects -> State -> Render

In the Medux framework, data processing is encapsulated in the Model, and initialization is usually done by listening on the Modu. Init Action to Effect the data and convert it to moduleState. When a Module is loaded, both the Client and the Server trigger this Action, so in this ActionHandler we should note that the Client does not need to repeat the work that the Server has already done. IsHydrate can be used to determine whether the current moduleState has been handled by the server, for example:

// src/modules/app/model.ts

@effect(null)
protected async ['this.Init'] () {if (this.state.isHydrate) {
    GetProjectConfig () does not need to be executed if it has already been rendered by SSR server
    const curUser = await api.getCurUser();
    this.dispatch(this.actions.putCurUser(curUser));
    if (curUser.hasLogin) {
      this.getNoticeTimer();
      this.checkLoginRedirect(); }}else {
    // If it is the first rendering, it may run on the client side or the server side
    const projectConfig = await api.getProjectConfig();
    this.updateState({projectConfig});
    // The server is a tourist and does not need to obtain user information
    if(! isServer()) {const curUser = await api.getCurUser();
      this.dispatch(this.actions.putCurUser(curUser));
      if (curUser.hasLogin) {
        this.getNoticeTimer();
        this.checkLoginRedirect(); }}}}Copy the code

Handling user logins

We only SSR the pages that do not require users to log in, so on the Server side users are assumed to be tourists. In the global error Handler, when encountering an error that requires a login:

  • If the Client is deployed, the route is displayed on the login page or the login window is displayed
  • If you are currently on the Server, stop rendering and throw a 303 error. (We can catch a 303 error on the server and send the uniform index.html directly)
// src/modules/app/model.ts

@effect(null)
protected async [ActionTypes.Error](error: CustomError) {
  if (isServer()) {
    // The server middleware will catch 301 and redirect to the URL
    if (error.code === CommonErrorCode.redirect) {
      throw {code: '301', detail: error.detail};
    } else {
      // The server stops rendering directly and changes to client-side rendering
      // Server middleware will catch 303 error and send uniform index.html directly
      throw {code: '303'}; }}... }Copy the code

Handles asynchronous on-demand loading

As mentioned earlier, we must replace the asynchronous on-demand loading side of the module with synchronous side code on the Server side. Medux controls whether modules are loaded synchronously or asynchronously in SRC /modules/index.ts:

// Load asynchronously
export const moduleGetter = {
  app: (a)= > {
    return import(/* webpackChunkName: "app" */ 'modules/app');
  },
  adminLayout: (a)= > {
    return import(/* webpackChunkName: "adminLayout" */ 'modules/admin/adminLayout'); },... };// Use synchronous loading instead
export const moduleGetter = {
  app: (a)= > {
    return require('modules/app');
  },
  adminLayout: (a)= > {
    return require('modules/admin/adminLayout'); },... };Copy the code

Simply replace import with require, which you can do with a simple webpack-loader provided with this project:

@medux/dev-utils/dist/webpack-loader/server-replace-async

It also supports partial module replacement with parameters to reduce the size of server-side JS files, such as:

// build/webpack.config.js

{
  test: /\.(tsx|ts)? $/.use: [{loader: require.resolve('@medux/dev-utils/dist/webpack-loader/server-replace-async'),
      options: {modules: ['app'.'adminLayout'.'articleLayout'.'articleHome'.'articleAbout'.'articleService']}}, {loader: 'babel-loader'.options: {cacheDirectory: true.caller: {runtime: 'server'}}},],},Copy the code

Other processing

Use a lot of details we directly look at the source code, you can ask me questions, welcome to discuss.