blog icon indicating copy to clipboard operation
blog copied to clipboard

vue-server-renderer插件在ssr中做了什么?

Open rudyxu1102 opened this issue 4 years ago • 0 comments

vue-server-renderer插件

vue-server-renderer是用来做Vue项目的服务端渲染,Vue的服务端渲染框架Nuxt也是基于vue-server-renderer,下面我们来看看这个插件到底做了什么事情。

运行项目

运行demo,同时结合vue-server-renderer源码,带着下面的问题去研究一下vue-server-renderer

  • VueSSRClientPlugin做了什么事情?
  • VueSSRServerPlugin做了什么事情?
  • 服务端渲染获取的数据,如何同步到客户端渲染的页面?

VueSSRClientPlugin源码

VueSSRClientPlugin的作用是生成客户端构建清单,清单的名字默认为vue-ssr-client-manifest.json。这个json对象包含webpack整个构建过程的信息,从而能bundle renderer自动推导出在html需要注入的内容,自动推断出最佳的预加载(preload)和预取(prefetch)指令,以及初始渲染所需的代码分割 chunk。。

var VueSSRClientPlugin = function VueSSRClientPlugin (options) {
  if ( options === void 0 ) options = {};

  // 默认输出名称为vue-ssr-client-manifest.json
  this.options = Object.assign({
    filename: 'vue-ssr-client-manifest.json'
  }, options);
};

VueSSRClientPlugin.prototype.apply = function apply (compiler) {
    var this$1 = this;

  // 监听emit事件,emit事件在生成资源到 output 目录之前触发。
  compiler.plugin('emit', function (compilation, cb) {
    var stats = compilation.getStats().toJson();

    // 所有的文件
    var allFiles = uniq(stats.assets
      .map(function (a) { return a.name; }));

    // 初始化的文件数组
    var initialFiles = uniq(Object.keys(stats.entrypoints)
      .map(function (name) { return stats.entrypoints[name].assets; })
      .reduce(function (assets, all) { return all.concat(assets); }, [])
      .filter(isJS));

    // 异步文件数组
    var asyncFiles = allFiles
      .filter(isJS)
      .filter(function (file) { return initialFiles.indexOf(file) < 0; });

    // 最后输出的json对象
    var manifest = {
      publicPath: stats.publicPath,
      all: allFiles,
      initial: initialFiles,
      async: asyncFiles,
      modules: { /* [identifier: string]: Array<index: number> */ }
    };

    // 设置manifest.modules的值,表示模块属于哪个chunk文件
    var assetModules = stats.modules.filter(function (m) { return m.assets.length; });
    var fileToIndex = function (file) { return manifest.all.indexOf(file); };
    stats.modules.forEach(function (m) {
      // ignore modules duplicated in multiple chunks
      if (m.chunks.length === 1) {
        var cid = m.chunks[0];
        var chunk = stats.chunks.find(function (c) { return c.id === cid; });
        if (!chunk || !chunk.files) {
          return
        }
        var files = manifest.modules[hash(m.identifier)] = chunk.files.map(fileToIndex);
        // find all asset modules associated with the same chunk
        assetModules.forEach(function (m) {
          if (m.chunks.some(function (id) { return id === cid; })) {
            files.push.apply(files, m.assets.map(fileToIndex));
          }
        });
      }
    });

    // 输出json对象
    var json = JSON.stringify(manifest, null, 2);
    compilation.assets[this$1.options.filename] = {
      source: function () { return json; },
      size: function () { return json.length; }
    };
    cb();
  });
};

打包生成的vue-ssr-client-manifest.json文件

{
  "publicPath": "",
  // 所有的资源文件   
  "all": [
    "0.js",
    "1.js",
    "2.js",
    "clientBundle.js" // webpack.client.conf.js里面entry的key
  ],
  // 初始化需要的文件
  "initial": [
    "clientBundle.js"
  ],
  // 异步的文件
  "async": [
    "0.js",
    "1.js",
    "2.js"
  ],
  // 模块
  "modules": {  
    // entry-client.js模块属于all[3], 即clientBundle.js
    "2ce556e0": [
      3
    ],
    // App.vue模块属于all[3], 即clientBundle.js
    "96f2ad42": [
      3
    ],
    //...
  }
}

VueSSRServerPlugin源码

生成server buddle文件,默认为vue-ssr-server-bundle.json,用于获取资源文件的内容,比如0.js、1.js、serverBundle.js的内容。

var VueSSRServerPlugin = function VueSSRServerPlugin (options) {
  if ( options === void 0 ) options = {};

  // 默认输出名字为vue-ssr-server-bundle.json
  this.options = Object.assign({
    filename: 'vue-ssr-server-bundle.json'
  }, options);
};

VueSSRServerPlugin.prototype.apply = function apply (compiler) {
  var this$1 = this;

  validate(compiler);

  compiler.plugin('emit', function (compilation, cb) {
    var stats = compilation.getStats().toJson();
    var entryName = Object.keys(stats.entrypoints)[0];
    var entryInfo = stats.entrypoints[entryName];

    var entryAssets = entryInfo.assets.filter(isJS);

    var entry = entryAssets[0];
   
    var bundle = {
      entry: entry,
      files: {},
      maps: {}
    };

    stats.assets.forEach(function (asset) {
      if (asset.name.match(/\.js$/)) {
        bundle.files[asset.name] = compilation.assets[asset.name].source();
      } else if (asset.name.match(/\.js\.map$/)) {
        bundle.maps[asset.name.replace(/\.map$/, '')] = JSON.parse(compilation.assets[asset.name].source());
      }
      // do not emit anything else for server
      delete compilation.assets[asset.name];
    });

    var json = JSON.stringify(bundle, null, 2);
    var filename = this$1.options.filename;

    compilation.assets[filename] = {
      source: function () { return json; },
      size: function () { return json.length; }
    };

    cb();
  });
};

输出的vue-ssr-server-bundle.json:

{
  "entry": "serverBundle.js",
  "files": {
    "0.js": "...",
    "1.js": "...",
    "2.js": "...",
    "serverBundle.js": "...", // webpack.server.conf.js里面entry的key
  }

生成客户端清单vue-ssr-client-manifest.json和服务端bundle对象vue-ssr-server-bundle.json,就可以通过createBundleRenderer渲染把vue文件

const bundle = require(path.resolve(__dirname, 'dist/vue-ssr-server-bundle.json'));

const renderer = require('vue-server-renderer').createBundleRenderer(bundle, {
    template: fs.readFileSync(path.resolve(__dirname, 'index.ssr.html'), 'utf-8'),
    clientManifest: require(path.resolve(__dirname, 'dist/vue-ssr-client-manifest.json')),
    basedir: path.resolve(__dirname, './dist'),
});

vue-server-renderer的作用

const Vue = require('vue')
const app = new Vue({
    template: `<div>Hello World</div>`
})
const renderer = require('vue-server-renderer').createRenderer()
    renderer.renderToString(app, (err, html) => {
if (err) throw err
    console.log(html)
})

// => <div data-server-rendered="true">Hello World</div>

render的核心代码在vue目录的server文件夹

服务端渲染获取的数据,如何同步到客户端渲染的页面?

服务端获取数据,保存到服务端的store中。在renderer中会在代码中加入<script>window.__INITIAL_STATE__ = ...</script>。在客户端初始化store时,判断是否存在window.__INITIAL_STATE__,如果存在就替换store.state。

TemplateRenderer.prototype.renderState = function renderState (context, options) {
  var ref = options || {};
    var contextKey = ref.contextKey; if ( contextKey === void 0 ) contextKey = 'state';
    var windowKey = ref.windowKey; if ( windowKey === void 0 ) windowKey = '__INITIAL_STATE__';
  var autoRemove = process.env.NODE_ENV === 'production'
    ? '(function(){var s;(s=document.currentScript||document.scripts[document.scripts.length-1]).parentNode.removeChild(s);}());'
    : '';
  return context[contextKey]
    ? ("<script>window." + windowKey + "=" + (serialize(context[contextKey], { isJSON: true })) + autoRemove + "</script>")
    : ''
};

rudyxu1102 avatar Nov 15 '20 12:11 rudyxu1102