源码如下:
// 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: {} }; // 遍历所有资源 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端进行二次渲染,绑定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;