Interpretation of Vue server renderer source code and its implementation in react

Time:2021-12-30

preface

In the process of blog development, there is a need to be solved in the SSR development environment, The code of the server is directly packaged into a file through webpack (because it contains isomorphic code, that is, the component code of the front end shared by the server and the client), write it to the disk, and then start the service by starting the packaged entry file. However, I don’t want to package the file to the disk in the development environment, but I want to package it directly into the internal storage, so that I can not only optimize the speed, but also avoid generating redundant files due to the development environment. What’s more, I can It is the webpack’s handling of require, which will lead to the problem of path mapping, including the problem of require variables. So I think only the code related to the component is webpack compiled, and other irrelevant server code is not webpack compiled.

But there is a problem hanging in the middle, that is, how to introduce files in memory. This includes how to import the associated files after importing this file, such as throughrequire(module)The module introduced, so I thought of the module I used to do SSR for Vuevue-server-rendererThis library does not directly print out the file, but puts the file into memory. But he can get the file and execute the file to get the results. So I started this research trip.

realization

Let’s talk about the implementation process of the project first, and thenvue-server-rendererHow does this package solve this problem and implement it in react.

|-- webpack
|   |-- webpack.client.js // entry => clilent-main.js
|   |-- webpack.server.js // entry => server-main.js
|--Client // client code
|   |-- app.js
|   |-- client-main. JS // client packaging entry
|   |-- server-main. JS // server side packaging code entry
|--Server // server side code
|   |-- ssr. JS // SSR startup entry
  1. client-main.js, the client packages a piece of code, which is normal packaging and packages the corresponding file.

    import React, { useEffect, useState } from 'react'
    import ReactDom from 'react-dom'
    import App from './app'
    
    loadableReady(() => {
      ReactDom.hydrate(
        <Provider store={store}>
          <App />
        </Provider>,
        document.getElementById('app')
      )
    })
  2. server-main.js, because it is SSR, you also need to package a corresponding JS file on the server for SSR rendering. I’m going to directly process the data related to the component and return the HTML. At that time, the server will directly import the file, obtain the HTML and return it to the front end. This is the processing of my project. The Vue official demo will be a little different. It is an app instance returned directly(new Vue(...), and thenvue-server-rendererParse this instance in the library, and finally return the parsed HTML string. There will be a difference here. The principle is the same.

    //Return a function, so that you can pass in some parameters to pass in some data on the server
    import { renderToString } from 'react-dom/server'
    export default async (context: IContext, options: RendererOptions = {}) => {
      //Get component data
      ...
    
      //Get the DOM information of the component corresponding to the current URL
      const appHtml = renderToString(
        extractor.collectChunks(
          <Provider store={store}>
            <StaticRouter location={context.url} context={context as any}>
              <HelmetProvider context={helmetContext}>
                <App />
              </HelmetProvider>
            </StaticRouter>
          </Provider>
        )
      )
    
      //Render template
      const html = renderToString(
        <HTML>{appHtml}</HTML>
      )
      context.store = store
      return html
    }
  3. ssr.js, because these files are typed in memory. So I need to parse the files in memory to getserver-main.jsFunction in, execute it and return HTML to the front end.

    //The start method is to execute the node side code of the webpack, which is used to put the compiled file into memory.
    import { start } from '@root/scripts/setup'
    
    //The createbundlerender method is used to parse the code packaged on the server side
    start(app, ({ loadableStats, serverManifest, inputFileSystem }) => {
     renderer = createBundleRenderer({
     loadableStats,
     serverManifest,
     inputFileSystem
     })
    })
    
    //Execute server main JS and get HTML
    const html = await renderer.renderToString(context)
    ctx.body = html

It’s easy for the client to say that by creating an HTML template and introducing the resources (JS, CSS,…) corresponding to the current route, the browser can directly pull the resources when accessing (this is through)@loadable/webpack-plugin@loadable/server@loadable/componentTo load and obtain resources, which will not be introduced here. This article does not focus on this).
The focus of this is how to parse in memoryserver-main.jsThe packaged code that needs to be referenced on the server side.

Let’s look at the official code of Vue SSR: Vue hackernews-2.0

const VueSSRServerPlugin = require('vue-server-renderer/server-plugin')

module.exports = merge(base, {
  target: 'node',
  devtool: '#source-map',
  entry: './src/server-main.js',
  output: {
    filename: 'server-bundle.js',
    libraryTarget: 'commonjs2'
  },
  plugins: [
    new VueSSRServerPlugin()
  ]
})

It uses onevue-server-renderer/server-plugin, what is the main function of this plug-in? In fact, it processes the resources in the webpack and prints all the JS resources in a JSON file.

The source code is as follows:

//A Vue server plugin is customized on webpack
compiler.hooks.emit.tapAsync('vue-server-plugin', (compilation, cb) => {
  //Get all resources
  var stats = compilation.getStats().toJson();,
  var entryName = Object.keys(stats.entrypoints)[0];
  var entryInfo = stats.entrypoints[entryName];

  //No entry file exists
  if (!entryInfo) {
    return cb()
  }
  var entryAssets = entryInfo.assets.filter(isJS);

  //The entry has multiple JS files, only one is needed: Entry: '/ src/entry-server. js'
  if (entryAssets.length > 1) {
    throw new Error(
      "Server-side bundle should have one single entry file. " +
      "Avoid using CommonsChunkPlugin in the server config."
    )
  }

  var entry = entryAssets[0];
  if (!entry || typeof entry !== 'string') {
    throw new Error(
      ("Entry \"" + entryName + "\" not found. Did you specify the correct entry option?")
    )
  }

  var bundle = {
    entry: entry,
    files: {},
    maps: {}
  };
  //Traverse all resources
  stats.assets.forEach(function (asset) {
    //If it is a JS resource, it is stored in the bundle In the files field.
    if (isJS(asset.name)) {
      bundle.files[asset.name] = compilation.assets[asset.name].source();
    }Else if (asset. Name. Match (/ \. JS \. Map $/) {// the sourcemap file is stored in the maps field to track errors
      bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
    }
    //Delete the resource because JS and JS The map has been saved in the bundle, and the required resources have been saved. There is no need to package others.
    delete compilation.assets[asset.name];
  });

  var json = JSON.stringify(bundle, null, 2);
  var filename = this$1.options.filename; // => vue-ssr-server-bundle.json

  //Store the bundle in assets, so that there is only Vue SSR server bundle in assets JSON this JSON file,
  /* 
    vue-ssr-server-bundle.json
    {
      entry: 'server-bundle.js',
      files: [
        'server-bundle.js': '...',
        '1.server-bundle.js': '...',
      ],
      maps: [
        'server-bundle.js.map': '...',
        '1.server-bundle.js.map': '...',
      ]
    }
  */
  compilation.assets[filename] = {
    source: function () { return json; },
    size: function () { return json.length; }
  };
  cb();
});

The processing of this plug-in is also extremely simple, that is, it intercepts resources and reprocesses them. Generate a JSON file to facilitate direct parsing at that time.

Then let’s look at the entry file of the node service to see how to get HTML and parse it

const { createBundleRenderer } = require('vue-server-renderer')
//Bundle: read Vue SSR server bundle Data in JSON,
/* 
    bundle => vue-ssr-server-bundle.json
    {
      entry: 'server-bundle.js',
      files: [
        'server-bundle.js': '...',
        '1.server-bundle.js': '...',
      ],
      maps: [
        'server-bundle.js.map': '...',
        '1.server-bundle.js.map': '...',
      ]
    }
*/
renderer = createBundleRenderer(bundle, {
  template: fs. Readfilesync (templatepath, 'UTF-8'), // HTML template
  //The client-side JSON file also exists in memory and is also used to intercept webpack resources. It will not be introduced here, but the principle is similar. Read the corresponding resources, put them into the HTML template, perform secondary rendering on the client side, bind Vue events, etc
  clientManifest: readFile(devMiddleware.fileSystem, 'vue-ssr-client-manifest.json'), 
  Runinnewcontext: false // share global objects in the node sandbox without creating new ones
}))
const context = {
  title: 'Vue HN 2.0', // default title
  url: req.url
}
renderer.renderToString(context, (err, html) => {
  if (err) {
    return handleError(err)
  }
  res.send(html)
})

By viewing the entry file started by the server-side project above, you can usecreateBundleRendererMediumrenderToStringTo return HTML directly, so come tovue-server-rendererThis library, let’s see what’s in it

function createRenderer(ref) {
  return {
      renderToString: (app, context, cb) => {
        //Parsing app: app = > New Vue (...), Is the Vue instance object
        //This is the compilation and analysis of Vue components, and finally get the corresponding HTML string
        //That's not the point. I won't introduce it here
        const htmlString = new RenderContext({app, ...})
        return cb(null, htmlString)
      }
  }
}
function createRenderer$1(options) {
  return createRenderer({...options, ...rest})
}
function createBundleRendererCreator(createRenderer) {
  return function createBundleRenderer(bundle, rendererOptions) {
    entry = bundle.entry;
    //Associated JS resource content
    files = bundle.files;
    //Sourcemap content
    //The createsourcemapconsumers method is used to track error files through the require ('source map ') module. Because we have all intercepted resources, we also need to implement the correct path mapping for errors.
    maps = createSourceMapConsumers(bundle.maps);

    //Call the createrenderer method to get the renderer object
    var renderer = createRenderer(rendererOptions);

    //This is the code for processing memory files,
    //{files: ['entry. JS':'module. Exports = a ']}, that is, I read the entry JS file, which is a string, and then how node handles it. After processing, we get the result.
    //This method is described in detail below
    var run = createBundleRunner(
      entry,
      files,
      basedir,
      rendererOptions.runInNewContext
    );
  
    return {
      renderToString: (context, cb) => {
        //By executing the run method, you can get my server main The new Vue instance returned in the JS entry file
        run(context).then(app => {
          renderer.renderToString(app, context, function (err, res) {
            //Print the correct file path for the wrong mapping
            rewriteErrorTrace(err, maps);
            //Res: parsed HTML string
            cb(err, res);
          });
        })
      }
    }
  }
}
var createBundleRenderer = createBundleRendererCreator(createRenderer$1);
exports.createBundleRenderer = createBundleRenderer;
  1. The above logic is also relatively clear, throughcreateBundleRunnerMethod to parse the string code of the entry file, Vueserver-main.jsThe entry file returns a promise function, and promise returnsnew Vue()Therefore, the parsed result isnew Vueexample.
  2. adoptRenderContextReturned by instance parsingnew VueInstance to get the corresponding HTML string.
  3. adoptsource-mapThe module performs the correct file path mapping for errors.

In this way, the code in the file is executed in memory and html is returned to achieve the effect of SSR. This article focuses on how to execute the string code of the entry file.

We comecreateBundleRunnerMethod to see how it is implemented.

function createBundleRunner (entry, files, basedir, runInNewContext) {
  var evaluate = compileModule(files, basedir, runInNewContext);
  if (runInNewContext !== false && runInNewContext !== 'once') {
    //If this runinnewcontext does not pass the options of false and once, a new context environment will be generated every time. We can share a context global. So this one is not considered
  } else {
    var runner;
    var initialContext;
    return function (userContext) {
      //Void 0 = = = undefined, because undefined can be redefined, void cannot be redefined, so void 0 must be undefined
      if ( userContext === void 0 ) userContext = {};

      return new Promise(function (resolve) {
        if (!runner) {
          //Runinnewcontext: false, so the context here refers to global
          var sandbox = runInNewContext === 'once'
            ? createSandbox()
            : global;
          //The function that returns the entry file by calling the evaluate method. Code implementation: evaluate = compilemodule (files, basedir, runinnewcontext)
          //Go to the compilemodule method to see how it is implemented
          /* 
            Server main of Vue official demo JS file, a promise function is returned, so the runner is this function.
            export default context => {
              return new Promise((resolve) => {
                const { app } = createApp()
                resolve(app)
              })
            }
          */
         //Pass in the entry file name and return the entry function.
          runner = evaluate(entry, sandbox);
        }
        //Execute promise to return the app, and the app will be.
        resolve(runner(userContext));
      });
    }
  }
}

//This method returns the evaluatemodule method, which is the above evaluate method
// evaluate = function evaluateModule(filename, sandbox, evaluatedFiles) {}
function compileModule (files, basedir, runInNewContext) {
  var compiledScripts = {};

  //Filename: the dependent file name, such as server bundle. JS or server bundle. JS dependent 1 server. bundle. JS file
  //Via Vue SSR server bundle The files field in JSON gets the file content corresponding to the file name, which is similar to the string "module. Exports = 10"
  //The code is wrapped through the module module of node. In fact, the code is very simple and crude. It is encapsulated into a function and passed in the required, exports and other variables in the well-known commonjs specification
  /* 
    Module.wrapper = [
      '(function (exports, require, module, __filename, __dirname, process, global) { ',
      '\n});'
    ];
    Module.wrap = function(script) {
      return Module.wrapper[0] + script + Module.wrapper[1];
    };

    result: 
    function (exports, require, module, __filename, __dirname, process, global) {
      module.exports = 10
    }
  */
  //Create a sandbox environment through the VM module to execute this JS code.
  function getCompiledScript (filename) {
    if (compiledScripts[filename]) {
      return compiledScripts[filename]
    }
    var code = files[filename];
    var wrapper = require('module').wrap(code);
    var script = new require('vm').Script(wrapper, {
      filename: filename,
      displayErrors: true
    });
    compiledScripts[filename] = script;
    return script
  }


  function evaluateModule (filename, sandbox, evaluatedFiles) {
    if ( evaluatedFiles === void 0 ) evaluatedFiles = {};

    if (evaluatedFiles[filename]) {
      return evaluatedFiles[filename]
    }

    //Get the sandbox environment that executes this code
    var script = getCompiledScript(filename);
    //Context used by sandbox environment runinthiscontext = > Global
    var compiledWrapper = runInNewContext === false
      ? script.runInThisContext()
      : script.runInNewContext(sandbox);
    var m = { exports: {}};
    var r = function (file) {
      file = path$1.posix.join('.', file);
      //The packaging file that JS currently depends on exists. Continue to create a sandbox environment for execution
      if (files[file]) {
        return evaluateModule(file, sandbox, evaluatedFiles)
      } else {
        return require(file)
      }
    };
    //Execute function code. Note that the webpack should be packaged into the commonjs specification, otherwise it won't be right here.
    compiledWrapper.call(m.exports, m.exports, r, m);
    //Get return value
    var res = Object.prototype.hasOwnProperty.call(m.exports, 'default')
      ? m.exports.default
      : m.exports;
    evaluatedFiles[filename] = res;
    //Return results
    return res
  }
  return evaluateModule
}

createBundleRunnerThere are not many implementations in the function. Is to create a sandbox environment to execute the obtained code

The core idea of the whole logic is as follows

  1. Generate a JSON file by intercepting webpack assets, which contains all JS file data
  2. Get the string code from the generated JSON file through the entry file.
  3. adoptrequire('module').wrapConvert the string code into the string code in the form of function, and the commonjs specification
  4. adoptrequire('vm')Create a sandbox environment to execute this code and return the results.
  5. If the entry file depends on other files, perform steps 2 – 4 again to replace the entry file with the dependent file. For example, routes are generally lazy loaded, so when accessing the specified route, the webpack package will also obtain the corresponding route file and rely on the entry file.
  6. The returned results obtained by executing in the sandbox environment are in the vue-hackernews-2.0 projectnew VueInstance object.
  7. Parse thisvueInstance, get the corresponding HTML string, put it into the HTML template, and finally return it to the front end.

In this way, you can read the memory file and get the corresponding HTML data. Mainly throughvmModule heelmoduleModule to execute these codes. In fact, the whole code of this piece is still relatively simple. There is no complicated logic.

Because the project is based onreactandwebpack5Therefore, the code processing will be somewhat different, but the implementation scheme is basically the same.

In fact, when it comes to executing code, there is another method in JS that can execute codeevalmethod. howeverevalMethod inrequireI always search in the local module. I find it impossible to search the files in memoryrequireFind. So it’s still usedvmModule, after all, you can override the require method

Project complete code: GitHub warehouse

Blog original address

I have created a new group to learn from each other. Whether you are Xiaobai who is ready to enter the pit or students who are halfway into the industry, I hope we can share and communicate together.
QQ group: 810018802, click to join

QQ group official account
Front end miscellaneous group
Interpretation of Vue server renderer source code and its implementation in react
White gourd Bookstore
Interpretation of Vue server renderer source code and its implementation in react

Recommended Today

JS generate guid method

JS generate guid method https://blog.csdn.net/Alive_tree/article/details/87942348 Globally unique identification(GUID) is an algorithm generatedBinaryCount Reg128 bitsNumber ofidentifier , GUID is mainly used in networks or systems with multiple nodes and computers. Ideally, any computational geometry computer cluster will not generate two identical guids, and the total number of guids is2^128In theory, it is difficult to make two […]