The practice of synchronizing changes between SSR application and original CSR application

75034a7bd064540e7a5adb88a60e4bba.gif

As mentioned in the previous article “SSR Transformation Practice of Tmall Auto Dealer Details Page”, in order to avoid affecting online applications, our integrated application (hereinafter referred to as SSR application) is a new one based on the original CSR project. application repository.

7443077d8775647bd591f87b5d42c444.png

background

When there is a new demand iteration for the business details, and the CSR warehouse changes, the SSR application will also change accordingly. The change process is as follows:

5cdfbc51a8e69e38ba381648ec766571.png

That is, with the continuous iteration of requirements, the technical side needs to maintain two code warehouses and two R&D applications at the same time, and the cost of testing, CR, and release will double.

This has a heavy maintenance cost both for the front end and for testing. In order to solve this problem, initially we wanted to keep the consistency of SSR and CSR applications on the code side, and only configure the switch ssr in build.json:

{
  "targets": [
    "web"
  ],
  "web": {
    "ssr": true, // Let the warehouse application publish mpa or ssr resources by adjusting the ssr configuration
    "mpa": true
  },
}

After a local test run, there is no problem with the development and construction after switching the configuration. However, when accessing the R&D platform of Ali Group, we encountered obstacles: The platform does not support accessing two applications in the same code warehouse. It was later discovered that the SSR application will publish the CSR link of the relevant page at the same time when it is released, so that only one application needs to be maintained on the R&D platform and released once. This gave me the hope of merging warehouses and applications. The expected merged process is as follows:

768f61ef5d2383a4e4f1c99c9ee2f288.png

The following is our transformation process.

5b0278d39a8e91e0f4dceb47822071e9.png

routing configuration

From the user’s point of view, routing is an important part of web links. For example, there are webpages a, b, and c, respectively through www.taobao.com/a.html, www.taobao.com/page/b.html, www.taobao.com/page/blog/index/c.html to visit, the part marked in red after the domain name is the route of the page.

From the perspective of development, routing is not only related to page source directory, but also related to engineering configuration.

There are four routing related in the SSR application:

  1. Page source directory: usually placed in the root directory of src/pages/, less nesting;

  2. app.json configuration: configure the access routes and corresponding page resources of each page in app.json;

  3. SSR render function directory: where the rendering logic of the SSR page is located, under src/apis/render/, there may be nesting;

  4. PAGE_NAME in the render function: the page resource path that the render function needs to use, and the server generates a document with content by executing this page file.

These four configurations affect each other, and the relevant documents are vague. For example, PAGE_NAME is not mentioned in the official documents, but only a sentence is commented in the initialization project: “The name of the page corresponds to the directory name under pages by default”. But from a practical point of view, this parameter is very important, and it even directly determines whether the server can render successfully.

The following is my verification and understanding of these routing configurations.

? Source directory and routing configuration

In a multi-page application, the source code of different pages is written in the same directory under src/pages/:

├── src
│ ├── app.json # Routing and page configuration
│ ├── components/ # Custom business components
│ ├── apis/ # server code
│ └── pages # page source directory
│ ├── a page
│ ├── b page
| └── c page
├── build.json # project configuration
├── package.json
└── tsconfig.json

When no routing configuration is performed, it is accessed through domain name/a.html by default, that is, using the directory name (lowercase) of the page under pages.

If you need to customize routing, modify the configuration in app.json, such as the following two page configurations:

{
  "routes": [
    {
      "name": "myhome",
      "source": "pages/Home/index"
    },
    {
      "name": "pages/about",
      "source": "pages/About/index"
    }
  ]
}

source specifies the source code location of the page, and name specifies the page route, so that we can pass domain name/myhome.html, domain name/pages /about.html visits the page.

The storage path of the build result reads the name configuration:

└──build
     └── web # The construction result of csr resources is placed in the web directory
        ├── myhome.html/js/css
        └── pages
              └── about.html/js/css

? PAGE_NAME in the render function

The render function is the rendering logic we define on the nodejs server side when the user visits the page.

Let’s take a look at how PAGE_NAME is used:

// page name, the default corresponds to the directory name under pages
const PAGE_NAME = 'pages/index/index';


export default withController({
  middleware: [
    downgradeOnError(PAGE_NAME), // downgrade middleware
  ],
}, async () => {
  const ctx = useContext();
  // The business logic of the nodejs server
  //...
  // generate rendered document
  const ssrRenderer = await useSSRRenderer(PAGE_NAME);
  await ssrRenderer.renderWithContext(ctx);
});

PAGE_NAME is passed to useSSRRenderer to generate the SSR document.

From the source code of useSSRRenderer, we can see how PAGE_NAME is consumed:

17f90d9b1761463c11fcad0f7c04da52.png

Inside the function, use PAGE_NAME to stitch together the path after the page code is built, then find the corresponding file from this path and return it to the ssrRender object, and finally execute it to generate a document.

So where is the page code placed after it is built? Take a look at the build results under the SSR project:

└──build
      └── client # client resource directory
      | └── web # csr resources are still in the web directory
      │ ├── myhome.html/js/css
      | └── pages
      | └── about.html/js/css
      |
      └── node # server resource directory
           ├── myhome.js # Node only generates js files
           └── pages
                └── about.js

The directory structure of node resources is configured according to name in app.json.

Therefore, the value of PAGE_NAME is not the so-called “default corresponding to the directory name under pages”, but the construction directory corresponding to the page resource, that is, the page name configuration in app.json.

? Directory path to the render function

Let me talk about the conclusion first, the directory path of the render function directly determines the access route of the SSR link.

Take the following structure as an example:

└── src
      └── apis # client resource directory
            └── render # csr resources are still in the web directory
                 ├── myhome.ts
                 └── pages
                       └── child
                              └── about.ts

The project generates two SSR links: ssr domain name/myhome, ssr domain name/pages/child/about. It can be seen that the directory path of the render function is its access route. Since the SSR link accesses a service, not a document resource, the link does not end in .html.

The names of the render file and the pages directory are kept the same for ease of understanding. According to the introduction to PAGE_NAME, we know that which page resource is used for server-side rendering has nothing to do with the name of the render file. It doesn’t matter if you name it abcd if the business requires it.

Therefore, there are almost no restrictions on the directory path of the render function, except in the following case.

When starting locally, the application will open the first generated link in the browser by default, and the generated link here is determined by app.json. If there is no corresponding resource in the directory path corresponding to your render, and the browser automatically opens the link for you, the server will report an error, or even disconnect directly. So the best practice for the render function is to be consistent with the name configuration of the corresponding page in app.json.

? Summary

To sum up, the relationship between relevant paths and page routing in SSR applications is shown in the following figure:

70cce8345328873a5e24125f95a6e9b4 .png

  1. The pages page resource path determines the source configuration in app.json

  2. The name configuration of app.json determines the location of the built product (CSR/SSR products all rely on this value), CSR access route

  3. The path of the render function file determines the SSR access route

  4. The PAGE_NAME in the render function determines the page resource used by the function, which needs to be consistent with the name value configuration of the corresponding page

b4e89e0115ef7f8e724352a8f866f601.png

technical problem

? Cloud build questions

After figuring out the above routing problem, the migration from CSR application routing to SSR application was quickly completed, the page was successfully launched locally, and the functions of the SSR page and CSR page were successfully verified.

But the accident still happened: when the pre-release cloud was built, the familiar window error appeared-is the environment wrong?

Checked the diff, and deleted all the objects with window in the transformation, and tried again, and the error still persisted, and continued to delete and delete until the change was deleted in pieces, and the error still existed.

Recalling that, jsdom was introduced to simulate the server environment. Even if the simulation is not good enough, the window will not exist, not to mention the success of local development and construction.

This error seems to have executed the page code during the build, so the problem was submitted to the architecture team.

Soon, the students in the architecture group confirmed the existence of the problem and gave a solution: a builder that has not been officially released.

? Manually simulate the code execution environment

While testing pre-issued SSR links, a new environmental issue emerged.

49fbd6adcacfaf7f254bda7c731246bd.png

After locating the problem, I found that it is a pot of debugging plug-ins. Sort out its execution logic, probably like this:

window.__mito_result = 'something'; // define variable, mount on window


console.log(__mito_result); // When using variables, window is not passed

The variable is hung under the window, but it is not accessed through the window. This method of directly reading the undefined variable will be found from the globalThis of the current environment at runtime. The globalThis on the node side is not the window, so the variable is not found, and the report not define is wrong.

There are several solutions to this problem:

  1. Modify the timing of plug-in execution

  2. Modify plug-in injection timing: Because the server only needs to generate document content, this plug-in is irrelevant to the document and does not need to be injected into the pre-release resource

  3. Environmental Simulation: use framework capabilities to simulate specific variables

Because this plugin will only be injected into the pre-release files and will not affect the officially released files. Therefore, it feels a bit heavy to modify the plug-in or function implementation. On the whole, the environmental simulation scheme was selected.

The integrated application simulates user-defined environment variables on the framework side with the following steps:

// The first step, configure mockEnvBrowser in build.json
// build.json
{
  "web": {
    "ssr": {
      "mockBrowserEnv": true
    },
  },
}


// The second step is to pass parameters in the render function
// src/apis/render/render-function-path.js
export default withController({
  middleware: [
    downgradeOnError(PAGE_NAME),
    phaIntercept(PAGE_NAME),
  ],
}, async () => {
  // . . .
  const ssrRenderer = await useSSRRenderer(PAGE_NAME, {
    mockBrowserEnv: true, // need to be configured as true again
    globalVariableNameList: [ // List of variable names to be simulated
      '__mito_data',
      '__mito_result'
    ],
    // It is not necessary to implement the above variable, then the value of the above variable is undefined during execution
    browserEnv: {}, // The corresponding implementation of the variable to be simulated
  });
  // . . .
}

Here, by the way, I have studied the implementation of environment variables on the framework side, which is quite interesting. In the source code of useSSRRenderer, if you find that mockBrowserEnv: true is configured, you will go to the following logic, the core of which is the string construction function:

d0395e18a91fb1f7b5b30416f41438 0e.png

Strip off its execution core logic:

// Define an execution function
function mockEnvFn (...globalVariableNameList) { // define parameters
  execute('page. js');
  // The page is in the context of the mockEnvFn function, and the window needs to be used in the page logic, which can be obtained from the parameter of the function
}


// execute the function
mockEvnFn(globalVariableList); // pass in actual parameters

The framework layer wraps an outer function for the page function, defines the formal parameter list for the outer function, and then executes the outer function, so that the page function is in the context of the formal parameter, thereby realizing the environment simulation.

? Media/script tag injection

In the app.json of the CSR, there are metas attributes and scripts attributes, which can insert some media attributes and related tool library scripts before the page code is executed into the document. However, in SSR mode, these two attributes will not take effect and need to be rewritten into documents. One difference from the metas tag is that the js execution script is best placed in the dangerouslySetInnerHTML attribute, and the execution location is above the