讲解vue-server-renderer源代码并在react中的完成

序言

​ 在blog开发设计的全过程中,有那样一个要求想处理,便是在SSR开发工具中,服务器端的编码是是立即根据webpack装包成文档(由于里边包括同构的编码,便是服务器端与手机客户端共享资源前面的部件编码),写到硬盘里,随后在运行装包好的通道文档来运行服务项目。可是我不愿意在开发工具把文件打包到硬盘中,想立即装包在运行内存中,那样不但能提升速率,还不容易因开发工具造成不必要文档。也有便是webpack对require的解决,会造成途径投射的难题,包含对require自变量的难题。因此 我也想仅有部件有关的编码开展webpack编译程序,其他不相干的服务器端编码不开展webpack编译程序解决。

可是这正中间有一个难题一直悬而不决,便是怎样引进运行内存中的文档。包含在引进这一文档后,怎样把关系的文档一起引进,如根据require(module)引进的控制模块,因此我想起之前在给vue做ssr的情况下采用的vue-server-renderer这一库,这个是沒有立即搞出文档,只是把文档打进了运行内存中。可是他却能获得到文档,并实行文档获得到結果。因此就打开了此次的科学研究之行。

完成

先讲下新项目这方面的完成步骤,随后在讲讲vue-server-renderer这一包是如何解决这个问题的,为此在react中的完成。

|-- webpack
|   |-- webpack.client.js // entry => clilent-main.js
|   |-- webpack.server.js // entry => server-main.js
|-- client // 手机客户端编码
|   |-- app.js
|   |-- client-main.js // 手机客户端装包通道
|   |-- server-main.js // server端装包编码通道
|-- server // server端编码
|   |-- ssr.js // ssr运行通道
  1. client-main.js, 手机客户端装包一份编码,便是一切正常的装包, 装包出相匹配的文档。

    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,由于是SSR,因此 在服务器端也必须装包一份相匹配的js文件,用以ssr3D渲染。我这里是准备在这方面立即解决完部件有关的数据信息,回到html,那时候服务器端立即引进这一文档,获得html回到给前面就可以了。这是我的新项目的解决,vue官方网demo会有点儿差别,他是立即回到的app案例(new Vue(...), 随后在vue-server-renderer库文件分析这一案例,最终一样也是回到分析好的html字符串数组。这儿会有点儿差别,基本原理或是一样。

    // 回到一个涵数,那样能够传到一些主要参数,用于传到服务器端的一些数据信息
    import { renderToString } from 'react-dom/server'
    export default async (context: IContext, options: RendererOptions = {}) => {
      // 获得部件数据信息
      ...
    
      // 获得当今url相匹配的部件dom信息内容
      const appHtml = renderToString(
        extractor.collectChunks(
          <Provider store={store}>
            <StaticRouter location={context.url} context={context as any}>
              <HelmetProvider context={helmetContext}>
                <App />
              </HelmetProvider>
            </StaticRouter>
          </Provider>
        )
      )
    
      // 3D渲染模版
      const html = renderToString(
        <HTML>{appHtml}</HTML>
      )
      context.store = store
      return html
    }
    

3. `ssr.js`, 由于这种文档我全是打在运行内存中的。因此 我需要分析运行内存中的文档,来获得`server-main.js`中的涵数,实行他,回到html给前面。

```typescript
// start方式是实行webpack的node端编码,用以把编译程序的文档打进运行内存中。
import { start } from '@root/scripts/setup'

// 实行他,createBundleRenderer方式便是用于分析在server端装包的编码
start(app, ({ loadableStats, serverManifest, inputFileSystem }) => {
  renderer = createBundleRenderer({
    loadableStats,
    serverManifest,
    inputFileSystem
  })
})

// 实行server-main.js中的涵数并获得html
const html = await renderer.renderToString(context)
ctx.body = html

手机客户端的好说,根据建立html模版,随后把当今路由器相匹配的資源(js, css,..)引进,浏览的情况下,电脑浏览器立即获取資源就可以了(这方面是根据@loadable/webpack-plugin@loadable/server@loadable/component来开展資源的载入与获得,这里不做太多详细介绍,该文关键没有这一)。
这方面的关键便是怎样在运行内存中分析server-main.js这一被装包出去的必须在服务器端引入的编码。

大家看来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()
  ]
})

上边采用了一个vue-server-renderer/server-plugin, 这一软件的关键作用是干啥呢,实际上便是对webpack中的資源干了下解决,把在其中的js資源全打在了一个json文档中。

源代码以下:

// webpack上自定了一个vue-server-plugin软件
compiler.hooks.emit.tapAsync('vue-server-plugin', (compilation, cb) => {
  // 获得全部資源
  var stats = compilation.getStats().toJson();,
  var entryName = Object.keys(stats.entrypoints)[0];
  var entryInfo = stats.entrypoints[entryName];

  // 不会有通道文档
  if (!entryInfo) {
    return cb()
  }
  var entryAssets = entryInfo.assets.filter(isJS);

  // 通道具备好几个js文件,只需一个就可以了: 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: {}
  };
  // 解析xml全部資源
  stats.assets.forEach(function (asset) {
    // 是js資源,就存进bundle.files字段名中。
    if (isJS(asset.name)) {
      bundle.files[asset.name] = compilation.assets[asset.name].source();
    } else if (asset.name.match(/\.js\.map$/)) { // sourceMap文档,存进maps字段名中,用于跟踪不正确
      bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
    }
    // 删掉資源,由于js跟js.map早已存到bundle中了,必须的資源早已存起来了,其他没必要装包出来。
    delete compilation.assets[asset.name];
  });

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

  // 把bundle存进assets中,那般assets中就仅有vue-ssr-server-bundle.json这一json文档了,
  /* 
    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();
});

这一软件的解决也以及简易,便是阻拦了資源,对其再次干了下解决。转化成一个json文档,那时候便捷立即开展分析解决。

随后大家看来node服务项目的通道文档,看来怎样获得html,并开展分析的

const { createBundleRenderer } = require('vue-server-renderer')
// bundle: 载入vue-ssr-server-bundle.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模版
  // client端json文档,也存有于运行内存中,也是对webpack資源的阻拦解决,这儿不开多详细介绍,基本原理类似。载入相匹配的資源放进html模版中,在client端开展二次3D渲染,关联vue事件这些
  clientManifest: readFile(devMiddleware.fileSystem, 'vue-ssr-client-manifest.json'), 
  runInNewContext: false // 在node沙盒游戏中国共产党用global目标,不建立新的
}))
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)
})

根据查询上边server端新项目运行的通道文档,里边用createBundleRenderer中的renderToString来立即回到html,因此 赶到vue-server-renderer这一库讨论一下这一里边究竟干了哪些

function createRenderer(ref) {
  return {
      renderToString: (app, context, cb) => {
        // 分析app: app => new Vue(...),便是vue实例目标
        // 这方面便是对vue组件的编译程序分析,最终获得相匹配的html string
        // 关键没有这,这里都不做太多详细介绍
        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;
    // 关系的js資源內容
    files = bundle.files;
    // sourceMap內容
    // createSourceMapConsumers方式功效就是根据require('source-map')控制模块来跟踪不正确文档。由于大家都开展了資源阻拦,因此 这方面也必须自身完成对不正确的恰当途径投射。
    maps = createSourceMapConsumers(bundle.maps);

    // 启用createRenderer方式获得renderer目标
    var renderer = createRenderer(rendererOptions);

    // 这方面便是解决运行内存文档中的编码了,
    // {files: ['entry.js': 'module.exports = a']}, 是我载入entry.js文件中的內容,他是字符串数组, 随后node如何处理的,解决完以后获得結果。
    // 下边这一方式开展详细描述
    var run = createBundleRunner(
      entry,
      files,
      basedir,
      rendererOptions.runInNewContext
    );
    return {
      renderToString: (context, cb) => {
        // 实行run方式,就能获得我还在server-main.js通道文档里边 回到的new Vue案例
        run(context).then(app => {
          renderer.renderToString(app, context, function (err, res) {
            // 打印错误投射的恰当文件路径
            rewriteErrorTrace(err, maps);
            // res: 分析好的html字符串数组
            cb(err, res);
          });
        })
      }
    }
  }
}
var createBundleRenderer = createBundleRendererCreator(createRenderer$1);
exports.createBundleRenderer = createBundleRenderer;
  1. 上边逻辑性也较为清楚一目了然,根据createBundleRunner方式来分析通道文档的字符串数组编码,vue server-main.js通道文档回到是一个Promise涵数,Promise回到的是new Vue(),因此 分析出去的結果就new Vue案例。
  2. 根据RenderContext等案例分析回到的new Vue案例,获得到相匹配的html字符串数组。
  3. 根据source-map控制模块对不正确开展恰当的文件路径投射。

那样就完成了在运行内存中实行文档中的编码,回到html,做到ssr的实际效果。此次文章内容的关键是怎样实行那一段通道文档的 字符串数组 编码。

大家赶到createBundleRunner方式,讨论一下里边到底是怎样完成的。


function createBundleRunner (entry, files, basedir, runInNewContext) {
  var evaluate = compileModule(files, basedir, runInNewContext);
  if (runInNewContext !== false && runInNewContext !== 'once') {
    // 这方面runInNewContext不传false 跟 once这两个选择项得话,每一次都是会转化成一个新的前后文自然环境,大家同用一个前后文global就可以了。因此 这方面也不考虑到
  } else {
    var runner;
    var initialContext;
    return function (userContext) {
      // void 0 === undefined, 由于undefined可被彻底改变,void无法彻底改变,因此 用void 0 肯定是undefined
      if ( userContext === void 0 ) userContext = {};

      return new Promise(function (resolve) {
        if (!runner) {
          // runInNewContext: false, 因此 这儿前后文是指的global
          var sandbox = runInNewContext === 'once'
            ? createSandbox()
            : global;
          // 根据启用evaluate方式回到通道文档的涵数。编码完成: evaluate = compileModule(files, basedir, runInNewContext)
          // 去到compileModule方式看里边是怎样完成的
          /* 
            vue官方网demo的server-main.js文件,回到的时一个Promise涵数,因此 runner就是这个涵数。
            export default context => {
              return new Promise((resolve) => {
                const { app } = createApp()
                resolve(app)
              })
            }
          */
         // 传到通道文件夹名称,回到通道涵数。
          runner = evaluate(entry, sandbox);
        }
        // 实行promise回到 app,到此app就获得了。
        resolve(runner(userContext));
      });
    }
  }
}

// 这一方式回到了evaluateModule方式,也就是上边evaluate方式
// evaluate = function evaluateModule(filename, sandbox, evaluatedFiles) {}
function compileModule (files, basedir, runInNewContext) {
  var compiledScripts = {};

  // filename: 依靠的文件夹名称,比如 server.bundle.js 或 server.bundle.js依靠的 1.server.bundle.js 文档
  // 在根据vue-ssr-server-bundle.json中的files字段名获得这一文件夹名称相匹配的文档內容  相近:"module.exports = 10"字符串数组
  // 根据node的module控制模块来包囊这一段编码,编码其实不是很难粗鲁,封裝变成一个涵数,传到大家熟识的commonjs标准中的require、exports这些自变量
  /* 
    Module.wrapper = [
      '(function (exports, require, module, __filename, __dirname, process, global) { ',
      '\n});'
    ];
    Module.wrap = function(script) {
      return Module.wrapper[0]   script   Module.wrapper[1];
    };

    結果: 
    function (exports, require, module, __filename, __dirname, process, global) {
      module.exports = 10
    }
  */
  // 根据vm控制模块建立沙盒游戏自然环境,来实行这一段JS代码。
  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]
    }

    // 获得这一实行这一段编码的沙盒游戏自然环境
    var script = getCompiledScript(filename);
    // 沙盒游戏自然环境应用的前后文  runInThisContext => global
    var compiledWrapper = runInNewContext === false
      ? script.runInThisContext()
      : script.runInNewContext(sandbox);
    var m = { exports: {}};
    var r = function (file) {
      file = path$1.posix.join('.', file);
      // 当今js依靠的装包文档,存有,再次建立沙盒游戏自然环境实行
      if (files[file]) {
        return evaluateModule(file, sandbox, evaluatedFiles)
      } else {
        return require(file)
      }
    };
    // 实行涵数编码。留意webpack要装包成commonjs标准的,要不然这儿就不一样了。
    compiledWrapper.call(m.exports, m.exports, r, m);
    // 获得传参
    var res = Object.prototype.hasOwnProperty.call(m.exports, 'default')
      ? m.exports.default
      : m.exports;
    evaluatedFiles[filename] = res;
    // 回到結果
    return res
  }
  return evaluateModule
}

createBundleRunner涵数里的完成实际上也很少。便是建立一个沙盒游戏自然环境来实行获得到的编码

全部逻辑性关键构思以下

  1. 根据阻拦webpack assets 转化成一个json文档,包括全部js文件数据信息
  2. 根据通道文档到转化成好的json文档里边取下来那一段字符串数组编码。
  3. 根据require('module').wrap把字符串数组代码转换成涵数方式的字符串数组编码,commonjs标准
  4. 根据require('vm')建立沙盒游戏自然环境来实行这一段编码,回到結果。
  5. 假如通道文档有依靠其他文档,再度实行 2 - 4流程,把通道文档换为依靠的文档就行,比如,路由器一般全是懒加载的,因此 在浏览特定路由器时,webpack装包出去也会获得这一相匹配的路由器文档,依靠到通道文档里边。
  6. 根据沙盒游戏自然环境实行获得到的回到結果,在vue-hackernews-2.0新项目中是 new Vue案例目标。
  7. 分析这一vue案例,获得到相匹配的html字符串数组,放进html模版中,最终回到给前面。

那样就完成了载入运行内存文档,获得相匹配的html数据信息。关键便是根据 vm控制模块跟module控制模块来实行这种编码的。实际上这方面的全部编码也或是非常简单的。并没什么繁杂的逻辑性。

由于新项目是根据reactwebpack5的,因此 在编码的解决上面有一些不一样,可是完成计划方案基本上或是一致的。

实际上说到实行编码,js里边还有一个方式能够实行编码,便是eval方式。可是eval方式在require的情况下全是在当地控制模块中开展搜索,存有于运行内存中的文档我发现了无法去开展require搜索。因此 或是用的vm控制模块来实行的编码,终究能够调用require方式

新项目详细编码:GitHub 库房

blog全文详细地址

自己新创建了一个相互学习的群,不管你是提前准备入行的新手,或是中途入门的同学们,期待大家能一起共享与沟通交流。
QQ群:810018802, 点一下添加

QQ群 微信公众号
前面杂活群
QQ群:810018802
东瓜书斋
公众号:冬瓜书屋

评论(0条)

刀客源码 游客评论