preface

After the rotation front update frequency is much lower, mainly for the output of high-quality articles. This article focuses on how to locate and resolve problems when you use the tripartite framework.

The framework involved

  • The SSR framework supports React as well as Vue2 and Vue3
  • Midwayjs Ali’s cloud integrated framework
  • The eggJS node service startup layer

Guidelines on pit

As the saying goes, predecessors plant trees and descendants enjoy the shade, so I’m afraid those who step on pits are in the process of planting trees. This year, the company plans to switch from static pages to an all-in-one, open approach to the cloud to improve the user experience (in the pursuit of excellence).

1. Selection of frame for stepping pits

Frame selection must focus on the selection of the market tempered framework. So you don’t have to plant trees. Just take it. Unless your trial and error costs a lot.

There are many frameworks in the market. At QCon this year, both Ali and Meituan launched their own cloud Integration framework. We chose Ali cloud Integration (SSR server rendering), and the template was based on the official SSR provided by the Midway ZUE-SSR. So that’s where the pit filling came from. Browse through its SSR github warehouse, the framework itself began to rise star that is earlier this year, should start to promote outside the internal. Looks like we passed our internal test. But it has not been thoroughly polished, there must be some problems around the corner. We need to be careful to choose such a framework at this time. We’re testing our online users for bugs. How to deal with the loss of capital.

2. Frame startup mode for stepping pits

The local test and pre-release production must be started and deployed in the same way.

The problem is that user A in the production environment opens the page and finds user B’s data. However, the SSR mode data belongs to others, while the CSR rendered data belongs to us. It wasn’t long before we switched back to CSR in full (which is one of the benefits of the framework, seamlessly switching render modes). This is a very serious problem. It’s a good thing we found it after we went online. So why didn’t the development and test environment catch it? Framework development and production deployment start up slightly differently. That’s one of the reasons we didn’t find the problem until we went live.

3. Frame feedback and communication of stepping pits

The framework used must have an offline communication feedback group, the effectiveness of the issue is poor.

In fact, my problem lasted for several months. When I first started to scan the code into the group for feedback, THERE was no reply at all. Maybe they haven’t had this problem before. Or I was wrong about what I said. In fact, the problems of the framework are only apparent to the user. The inside is a complete black box. So want to feedback the problem or have to read the source code, locate the problem, and then let the author to solve the problem for you. This is also the reason why it is very, very bad.

Filling holes guide

1. Analysis of pit filling

  • There is a problem with the code, try reviewing the code
  • Use the wrong way, first look at the official documentation
  • Through SSR request interface, at the gateway level, our cookie disorder caused
  • There was something wrong with the basic framework of the company (I began to doubt my life, because the official document of the framework was posted and many departments within Ali were using it, so I didn’t dare to question it)
  • There is a problem with the frame

1.1 The initial solution to the problem came from the first three, and we tried to locate the problem through these three paths. However, day by day, we checked these three paths and found no problems, and the Review at the code level was ok. Later, WE thought that there was a problem with AXIos encapsulated by ourselves. When SSR rendering was initialized for many times, I tried to switch the axios instance provided by the official, but found that it also failed. Meanwhile, I also failed to configure the configuration according to the official document. The third way is not as easy to rule out as the first two ways, just keep trying, the local debugger, testing the log, seeing that the cookie of the current user is really your own. This is a headache.

1.2 After several days of writing business code, I changed my mind. I wonder if there is a problem with the multi-environment container in our company’s basic framework layer. So what do you do? You can’t debug your environment online. There are some differences between the test environment and the online environment. The test environment has only one container running, while the online environment has multiple containers running concurrently. Is that the problem? The closest thing to a production environment is a pre-release environment. Then the pre-sent application and production of the same number of containers for scene restoration.


2. Recurrence of pit filling

According to the state of the problem, three prerequisites can be listed:

  • Concurrent access by multiple users
  • The same environment as the line
  • Code consistent with the line
2.1 A/B test

Rule out multiple containers in the corporate infrastructure

In step 4 of the problem analysis, we have configured the pre-delivery environment to be consistent with the online configuration. With multiple users refreshing the page at the same time, the problem actually recurs a few times. User A presented user B’s data, so for this result, compared to the test environment, the most suspicious is multiple containers. Is this the cause? Continue to investigate, each refresh, A/B user switch refresh, reoccurrence of the problem to observe the user log. It is true that many times A/B users are distributed to different containers, but there are also several times that they appear in the same container. We can rule out multiple containers.

2.2 Framework Issues

Confirmation by means of exclusion should be a framework problem

Then continue to refine the new log, the user request – interface initiated – interface response – store- fetch data render string. When user A/B’s data was abnormal, I observed the log and found that user A’s store data was in the stream returned by user B’s request and response. It is only here that we begin to doubt the framing problem. The reason why the framework has not been suspected is that the author thinks there is something wrong with our code and they have not received the relevant issue.

Because SSR vue instances router/ Store are newly created instances. Store reuse is not possible. Could it be that the Node service layer is responsible for the distribution stream distribution error, causing users to see data that is not their own, at which point we begin to suspect the Node layer. Try the Node service instead of eggjs with Nestjs. But think about the impossibility. These two libraries are recognized by many people. We continue to wonder if it’s an SSR problem.


3. Read the source code for filling pits

Choose the good ones and follow them, and change the bad ones. Reading the source code is the only way to fill the hole guide, and also the channel to improve the ability of the code.

Only by looking at the source code to try to locate the problem, for looking at the source code is certainly rejected by many students, do not know where to start. At the same time, seeing multiple modules associated with each other is a headache. The author of this is a number of modules called each other, but the logic is still very clear

Source three musketeers: simply over again, look carefully, the whole masturbation again (string process is not to let you masturbation code)

3.1 Go through it briefly

Here I’m using the Midway – VUE-SSR template, based on this module. Other modules are available if you are interested

  1. Cli (mainly to generate executable commands SSR start/ SSR build)
  2. Core-vue (mainly render function)
  3. Plugin – Midway (Start MidwayJS)
  4. Plugin-vue (this is mainly to provide the details of cli start/build)
  5. Server-utils (utility classes for other Modules)
  6. Types (Defines type types for various classes of interface TS)
  7. Webpack (packaged build startup service)

In fact, this is the first step, each module went through. Let’s take a quick look at what to do. Next comes the relationship between associated modules. In fact, you may have noticed that plugin-vue and server-utils are called many times by other modules.

3.2 Look it over carefully

Take the essence and discard the dregs

Through the first step, we have understood the primary and secondary relationship of each module, so how to extract its essence? Look at the following code

  import { render } from 'ssr-core-vue'
  
  @Get('/')
  @Get('/detail/:id')
  async handler (): Promise<void> {
    try {
      this.ctx.apiService = this.apiService
      this.ctx.apiDeatilservice = this.apiDeatilservice
      const stream = await render<Readable>(this.ctx, {
        stream: true
      })
      this.ctx.body = stream
    } catch (error) {
      console.log(error)
      this.ctx.body = error
    }
  }
Copy the code

All page requests come from this, and specific pages are rendered by the render function provided by the SSR-core-vue module. Let’s dig deeper into the render function.

async function render (ctx: ISSRContext, options? : UserConfig) { const config = Object.assign({}, defaultConfig, options ?? {}) const { isDev, chunkName, stream } = config const isLocal = isDev || process.env.NODE_ENV ! == 'production' const serverFile = resolve(cwd, `./build/server/${chunkName}.server.js`) if (isLocal) { // clear cache in development environment delete require.cache[serverFile] } if (! ctx.response.type && typeof ctx.response.type ! Content-type ctx.response.type = 'text/ HTML '; // Midway/KOA set content-type ctx.response.type = 'HTML'; charset=utf-8' } else if (! (ctx as ExpressContext).response.hasHeader? .('content-type')) {// Express scenario (CTX as ExpressContext).response.setheader? .('Content-type', 'text/html; charset=utf-8') } const { serverRender } = require(serverFile) const serverRes = await serverRender(ctx, config) if (stream) { const stream = mergeStream2(new StringToStream('<! DOCTYPE html>'), renderToStream(serverRes)) stream.on('error', (e: any) => { console.log(e) }) return stream } else { return `<! DOCTYPE html>${await renderToString(serverRes)}` } }Copy the code

As you can see, the main thing is to call page.server.js in the packaged build/server directory and pass in the Node layer context and configuration. Export at most one VUE instance. Render pages using vue-server-renderer provided by VUE.

In fact, the follow-up to see here may have been unable to go deep. Here is the build file. It’s full of compressed JS. But the source code calls the serverRender method of this JS. We can go deeper again. Those of you who remember may have noticed this during the first stage of foreplay. Ssr-vue-plugin server-entry.ts below module entry, which is the method we called in the packaged JS above.

const serverRender = async (ctx: ISSRContext, config: IConfig): Promise<Vue.Component> => { const { cssOrder, jsOrder, dynamic, mode, customeHeadScript, customeFooterScript, chunkName, parallelFetch, disableClientRender, Prefix} = config const router = createRouter() // Create a new router const store = createStore() // Create a new store const Base = prefix ?? Const viteMode = process.env.build_tool === 'vite' sync(store, router) let {path, url } = ctx.request if (base) { path = normalizePath(path, base) url = normalizePath(url, Base)} const routeItem = findRoute<IFeRouteItem>(FeRoutes, path) RouteItem) {throw new Error(' Failed to find component, please confirm current path: } let dynamicCssOrder = cssOrder if (dynamic &&! viteMode) { dynamicCssOrder = cssOrder.concat([`${routeItem.webpackChunkName}.css`]) dynamicCssOrder = await addAsyncChunk(dynamicCssOrder, routeItem.webpackChunkName) } const manifest = viteMode ? {} : await getManifest() const isCsr = !! (mode === 'csr' || ctx.request.query? .csr) let layoutFetchData = {} let fetchData = {} if (! isCsr) { const { fetch } = routeItem const currentFetch = fetch ? (await fetch()).default : If (parallelFetch) {layoutFetchData, if (parallelFetch) {layoutFetchData, fetchData] = await Promise.all([ layoutFetch ? layoutFetch({ store, router: router.currentRoute }, ctx) : Promise.resolve({}), currentFetch ? currentFetch({ store, router: router.currentRoute }, ctx) : Promise.resolve({}) ]) } else { layoutFetchData = layoutFetch ? await layoutFetch({ store, router: router.currentRoute }, ctx) : {} fetchData = currentFetch ? await currentFetch({ store, router: router.currentRoute }, ctx) : {} } } else { logGreen(`Current path ${path} use csr render mode`) } const combineAysncData = Object.assign({}, layoutFetchData ?? {}, fetchData ?? {}) const state = Object.assign({}, store.state ?? {}, // @ts-expect-error const app = new Vue({// create Vue instance}) return app}Copy the code

The appeal source has been annotated in part. So here we can actually carry out the third step, the whole thing through the whole process

3.3 The whole one time

And this is the crucial part, you think you’re at the end and you’re at the origin.

Here is just the beginning, so how do we start the service from SSR start ➡️ the user initiates a page request ➡️egg calls the render function ➡️ after calling the build page.server.js to show the page the user visited.

Then the starting point is SSR start. The simple code analysis in the first step of appeal has mentioned the SSR-CLI module, which provides SSR start and SSR build commands. This is where you get an asymptotic view of the overall flow.

yargs .command('start', 'Start Server', {}, async (argv: Argv) => {spinner.start() // console loading await handleEnv(Argv, spinner) // Initialize environment variable const {parseFeRoutes, LoadPlugin} = await import('ssr-server-utils') // get utility class await parseFeRoutes() // get page path information under Pages Debug (' require ssr-server-utils time: ${date.now () -start} ms') const plugin = loadPlugin() ${Date.now() - start} ms`) spinner.stop() debug(`parseFeRoutes ending time: ${date.now () - start} ms') await plugin.clientplugin?.start?.(argv) ${date.now () -start} ms') await cleanOutDir() await plugin.serverplugin?.start?.(argv  ending time: ${Date.now() - start} ms`) })Copy the code

I’ve made some simple comments here. You can see the logic behind SSR Start.

You can skip the first two steps and go directly to parseFeRoutes, loadPlugin, to get these two functions through server-utils. So what did parseFeRoutes do? Please see the source code!!

const parseFeRoutes = async () => { const isVue = require(join(cwd, './package.json')).dependencies.vue const viteMode = process.env.BUILD_TOOL === 'vite' if (viteMode && ! Dynamic) {console.log('vite mode forbid to disable dynamic ') return} let routes = "const declaretiveRoutes = await AccessFile (join(getFeDir(), './route.ts')) // Whether a custom route exists if (! Const pathRecord = ["] // @ts-expect-error const route: ParseFeRouteItem = {} let arr = await renderRoutes(pageDir, pathRecord, Route) // Recursive page directory Production route array if (routerPriority) {// Route priority sorting...... } if (routerOptimize) {// Route filter...... } debug('Before the result that parse web folder to routes is: ', arr) if (isVue) { const layoutPath = '@/components/layout/index.vue' const accessVueApp = await accessFile(join(getFeDir(), './components/layout/App.vue')) const layoutFetch = await accessFile(join(getFeDir(), './components/layout/fetch.ts')) const store = await accessFile(join(getFeDir(), './store/index.ts')) const AppPath = `@/components/layout/App.${accessVueApp ? 'vue' : 'TSX '}' // Process routing table information data structure.... } else {// React scenario...... }} else {routes = (await fs.readfile (join(getFeDir()), './route.ts'))).toString() } debug('After the result that parse web folder to routes is: ', routes) await writeRoutes(routes) then say /build/ ssr-temporal-routes.js}Copy the code

You can see that the ultimate goal is to generate routing table information structures based on directories

Let’s go ahead and see what happens after the routing table is generated! Plugin.js was read from our project structure to package and deploy the webPack-serve client, and then launch midway services

const { midwayPlugin } = require('ssr-plugin-midway') const { vuePlugin } = require('ssr-plugin-vue') module.exports = {  serverPlugin: midwayPlugin(), clientPlugin: vuePlugin() }Copy the code

Let’s look at what the midwayPlugin does first, and then at the vuePlugin.

import { exec } from 'child_process' import { loadConfig } from 'ssr-server-utils' import { Argv } from 'ssr-types' const { cliFun } = require('@midwayjs/cli/bin/cli') const start = (argv: Argv) => { const config = loadConfig() exec('npx cross-env ets', async (err, Stdout) => {if (err) {console.log(err) return} console.log(stdout) // Pass parameters to midway-bin argv._[0] = 'dev' argv.ts = true argv.port = config.serverPort await cliFun(argv) }) } export { start }Copy the code

As you can see here, it is relatively simple to read the server configuration and start by calling cliFun from the MidwaySJS export.

Here’s what vuePlugin Start is doing.

export function vuePlugin () { return { name: 'plugin-vue', start: Async () => {// Do careful dependency separation when developing locally, Const {startServerBuild} = await import(' SSR-webpack/CJS /server') // 1 Const {getServerWebpack} = await import('./config/server') // 2. Get SSR server cofing const serverConfigChain = new WebpackChain() // 3. Generate a default webpackChain if (process.env.build_tool === 'vite') {await startServerBuild(getServerWebpack(serverConfigChain))  } else { const { startClientServer } = await import('ssr-webpack') // 4. Const {getClientWebpack} = await import('./config') // 5 Const clientConfigChain = new WebpackChain() // 6 Generate a default webpackChain await promise.all ([startServerBuild(getServerWebpack(serverConfigChain))), StartClientServer (getClientWebpack(clientConfigChain))])}}, // Build method..... }}Copy the code

Here I have only posted about start, and I have added a comment for the corresponding code at the top. Packaging is simply passing the packaging configuration into WebPack. Webpack is left to do, but it skips over how to pack. You just need to know how to get the corresponding WebPack Config for the configuration.

const startClientServer = async (webpackConfig: webpack.Configuration): Promise<void> => {
  const { webpackDevServerConfig, fePort, host } = config
  return await new Promise((resolve) => {
    const compiler = webpack(webpackConfig)

    const server = new WebpackDevServer(compiler, webpackDevServerConfig)
    compiler.hooks.done.tap('DonePlugin', () => {
      resolve()
    })

    server.listen(fePort, host)
  })
}

Copy the code

You can see that the startClientServer source code just does the packaging and starts webPack-Server

const startServerBuild = async (webpackConfig: webpack.Configuration) => {
  const { webpackStatsOption } = loadConfig()
  const stats = await webpackPromisify(webpackConfig)
  console.log(stats.toString(webpackStatsOption))
}
Copy the code

StartServerBuild source code is also only packaged SSR server side

I’m going to skip the packaging and go straight to the key steps: How do I get configuration?

Through the source code can be known in the plugin-vue/config module directory has serve end and client end related webpack configuration content, we can click server.ts to see

const getServerWebpack = (chain: WebpackChain) => {const config = loadConfig() const {isDev, CWD, getOutput, chainServerConfig, WhiteList, chunkName} = config getBaseConfig(chain, true) merge preset base config config chain.devtool(isDev? 'inline-source-map' : false) chain.target('node') chain.entry(chunkName) .add(loadModule('.. /entry/server-entry')) // Load entry.end ().output.path (getOutput().serverOutput).filename('[name].server.js') LibraryTarget (' commonjs). The end () / / other configuration... ChainServerConfig (chain) // Merge user-defined configuration return chain.toconfig ()}Copy the code

As can be seen in the source code, loading the preset Webpack configuration while configuring the server side entry. Then return to WebPackConfig. For details about how to obtain the webpackConfig of Webpack, you can read the source code carefully. The most important thing here is the entry. When it comes to entry, it is back to the second step of reading the source code. And that’s what sets up the whole process.

Those of you reading this are sure to have strung together everything behind SSR Start.


4. Solve problems by filling pits

Source code come to an end. You can locate the problem by reading the source code. So why did user A get user B’s data? You know what?!

4.1 Locating Faults

Also by restoring the online scene to start the deployment service via egg-script, the log is printed after each store under the plugin-vue/entry/serverRender function. The result is obvious. Store is not initialized every time node runs. The second user logs in and gets the value of the first user’s store. Here we have identified a framework problem.

As you can see in the image above, the log printed in the console is null at first, but when another user comes in, a value appears in the console.

4.2 Troubleshooting

Through the appeal, it has been found that store reuse is the cause of the bug, (then put this bug up to the author recognized as a framework problem, ridicule all here then we can also solve it)

Store is a global variable, and concurrency causes the store variable to be polluted. If the store variable is a global variable, concurrency causes the store variable to be polluted. A deepclone was not performed on the store. Do a DeepClone on createStore. It really solved the problem.

If you want to delete an SSR start and an egg-script start, you can see the difference in the render of ssR-core-vue and delete require.cache

async function render (ctx: ISSRContext, options? : UserConfig) { const config = Object.assign({}, defaultConfig, options ?? {}) const { isDev, chunkName, stream } = config const isLocal = isDev || process.env.NODE_ENV ! == 'production' const serverFile = resolve(cwd, './build/server/${chunkName}.server.js') if (isLocal) { // Clear cache in development environment delete require.cache[serverFile]}..... Omitted code...... const { serverRender } = require(serverFile) const serverRes = await serverRender(ctx, config) ..... Omitted code...... }Copy the code

The first time require loads a module in the Node environment, Node caches the module, and subsequent loads are fetched from the cache. So user B will take the value of user A. It is ok for the author to modify the store in this way. The reason is that every store the user gets is from the same DeepClone store and does not operate the original store, so the clone store is the original store every time


So there are three solutions:

4.2.1 The first way is to release delete require.cache, but this is also the lowest solution
async function render (ctx: ISSRContext, options? : UserConfig) { const config = Object.assign({}, defaultConfig, options ?? {}) const { isDev, chunkName, stream } = config const isLocal = isDev || process.env.NODE_ENV ! == 'production' const serverFile = resolve(cwd, `./build/server/${chunkName}.server.js`) if (isLocal) { // clear cache in development environment delete Require. Cache [serverFile] // release this line} if (! ctx.response.type && typeof ctx.response.type ! Content-type ctx.response.type = 'text/ HTML '; // Midway/KOA set content-type ctx.response.type = 'HTML'; charset=utf-8' } else if (! (ctx as ExpressContext).response.hasHeader? .('content-type')) {// Express scenario (CTX as ExpressContext).response.setheader? .('Content-type', 'text/html; charset=utf-8') } const { serverRender } = require(serverFile) const serverRes = await serverRender(ctx, config) if (stream) { const stream = mergeStream2(new StringToStream('<! DOCTYPE html>'), renderToStream(serverRes)) stream.on('error', (e: any) => { console.log(e) }) return stream } else { return `<! DOCTYPE html>${await renderToString(serverRes)}` } }Copy the code
4.2.2 The second method is actually the operation of the author. Deepclone creates a new one each time to prevent the original store from being operated
4.2.3 The third method is also a method that I came up with by myself. Combined with the first method, it is feasible to delete every time before require store.
function createStore () { delete require.cache[require.resolve("@/store/index.ts")] const store = require("@/store/index.ts") return new Vuex.Store(store ?? {})}Copy the code

conclusion

Hope to be helpful to XDM. If the article has the wrong place, looks the message area to correct. It is, after all, a half-way to the front end.