3.可开箱即用的 SSR 脚手架
src ├── components ├── App.vue ├── app.js ----通用 entry ├── entry-client.js ----仅运行于浏览器 └── entry-server.js ----仅运行于服务器app.js 导出 createApp 函数工厂,此函数是可以被重复执行的,从根 Vue 实例注入,用于创建router,store 以及应用程序实例。
import Vue from 'vue' import App from './App.vue' // 堆代码 duidaima.com // 导出一个工厂函数,用于创建新的应用程序、router 和 store 实例 export function createApp () { const app = new Vue({ render: h => h(App) }) return { app } }entry-client.js 负责创建应用程序,挂载实例 DOM ,仅运行于浏览器。
import { createApp } from './app' const { app } = createApp() // #app 为根元素,名称可替换 app.$mount('#app')entry-server.js 创建返回应用实例,同时还会进行路由匹配和数据的预处理,仅运行于服务器。
import { createApp } from './app' export default context => { const { app } = createApp() return app }服务端和客户端代码编写原则
vue-SSR-server-bundle.json { "entry": , "files": { A:包含了所有要在服务端运行的代码列表 B:入口文件 } }client Bundle 用于生成 vue-SSR-client-manifest.json,包含所有的静态资源,首次渲染需要加载的 script 标签,以及需要在客户端运行的代码。
vue-SSR-client-manifest.json { "publicPath": 公共资源路径文件地址, "all": 资源列表 "initial":输出 html 字符串 "async": 异步加载组件集合 "modules": moduleIdentifier 和 all 数组中文件的映射关系 }在先决条件中我们提到了一个重要的包 vue-server-renderer,那我们来重点看看这个包里面的值得我们学习关注的内容。
const Vue = require('vue') const app = new Vue()2.生成 renderer,值得关注的两个对象 render 和 templateRenderer
const renderer = require('vue-server-renderer').createRenderer() // createRenderer 函数中有两个重要的对象: render 和 templateRenderer function createRenderer (ref) { // render: 渲染 html 组件 var render = createRenderFunction(modules, directives, isUnaryTag, cache); // templateRenderer: 模版渲染,clientManifest 文件 var templateRenderer = new TemplateRenderer({ template: template, inject: inject, shouldPreload: shouldPreload, shouldPrefetch: shouldPrefetch, clientManifest: clientManifest, serializer: serializer });经过这个过程的 render 和 templateRenderer 并没有被调用,这两个函数真正的调用是在项目实例化 createBundleRenderer 函数的时候,即第三步创建的函数。
var vm = require('vm'); // 调用 createBundleRunner 函数实例对象,rendererOptions 支持可配置 var run = createBundleRunner( entry, ----入口文件集合 files, ----打包文件集合 basedir, rendererOptions.runInNewContext。 );}在 createBundleRunner 方法的源码到其实例了一个叫 compileModule 的一个方法,这个方法做了中有两个函数:getCompiledScript 和 evaluateModule
function createBundleRunner (entry, files, basedir, runInNewContext) { //触发 compileModule 方法,找到 webpack 编译形成的 code var evaluate = compileModule(files, basedir, runInNewContext); }getCompiledScript: 编译 wrapper ,找到入口文件的 files 文件名及 script 脚本的编译执行
function getCompiledScript (filename) { if (compiledScripts[filename]) { return compiledScripts[filename] } // 在入口文件 files 中找到对应的文件名称 var code = files[filename]; var wrapper = NativeModule.wrap(code); // 在沙盒上下文中执行构建 script 脚本 var script = new vm.Script(wrapper, { filename: filename, displayErrors: true }); compiledScripts[filename] = script; return script }evaluateModule: 根据 runInThisContext 中的配置项来决定是在当前上下文执行还是单独上下文执行。
function evaluateModule (filename, sandbox, evaluatedFiles) { if ( evaluatedFiles === void 0 ) evaluatedFiles = {}; if (evaluatedFiles[filename]) { return evaluatedFiles[filename] } var script = getCompiledScript(filename); // 用于判断是在当前的那种模式下面执行沙盒上下文,此时存在两个函数的相互调用 var compiledWrapper = runInNewContext === false ? script.runInThisContext() : script.runInNewContext(sandbox); // m: 函数导出的 exports 数据 var m = { exports: {}}; // r: 替代原生 require 用来解析 bundle 中通过 require 函数引用的模块 var r = function (file) { ... return require(file) }; }上述的函数执行完成之后会调用 compiledWrapper.call,传参对应上面的 exports、require、module, 我们就能拿到入口函数。错误抛出容错和全局错误监听 renderToString: 在没有 cb 函数时做了 promise 的返回,那说明我们在调用次函数的时候可以直接做 try catch的处理,用于全局错误的抛出容错。
renderToString: function (context, cb) { var assign; if (typeof context === 'function') { cb = context; context = {}; } var promise; if (!cb) { ((assign = createPromiseCallback(), promise = assign.promise, cb = assign.cb)); } ... return promise }, }renderToStream:对抛错做了监听机制, 抛错的钩子函数将在这个方法中触发。
renderToStream: function (context) { var res = new PassThrough(); run(context).catch(function (err) { rewriteErrorTrace(err, maps); // 此处做了监听器的容错 process.nextTick(function () { res.emit('error', err); }); }).then(function (app) { if (app) { var renderStream = renderer.renderToStream(app, context); ... } } }防止交叉污染
// rendererOptions.runInNewContext 可配置项如下 true: 新上下文模式:创建新上下文并重新评估捆绑包在每个渲染上。 确保每个应用程序的整个应用程序状态都是新的渲染,但会产生额外的评估成本。 false: 直接模式: 每次渲染时,它只调用导出的函数。而不是在上重新评估整个捆绑包 模块评估成本较高,但需要结构化源代码 once: 初始上下文模式 仅用于收集可能的非组件vue样式加载程序注入的样式。特别说明一下 false 和 once 的场景, 为了防止交叉污染,在渲染的过程中对作用域要求很严格,以此来保证在不同的对象彼此之间不会形成污染。
if (!runner) { var sandbox = runInNewContext === 'once' ? createSandbox() : global; initialContext = sandbox.__VUE_SSR_CONTEXT__ = {}; runner = evaluate(entry, sandbox); //在后续渲染中,_VUE_SSR_CONTEXT_uu 将不可用 //防止交叉污染 delete sandbox.__VUE_SSR_CONTEXT__; if (typeof runner !== 'function') { throw new Error( 'bundle export should be a function when using ' + '{ runInNewContext: false }.' ) } }应用输出
function createRenderFunction ( modules, directives, isUnaryTag, cache ) { return function render ( component, write, userContext, done ) { warned = Object.create(null); var context = new RenderContext({ activeInstance: component, userContext: userContext, write: write, done: done, renderNode: renderNode, isUnaryTag: isUnaryTag, modules: modules, directives: directives, cache: cache }); installSSRHelpers(component); normalizeRender(component); // 渲染 node 节点,绑定用户作用上下文 var resolve = function () { renderNode(component._render(), true, context); }; // 等待组件 serverPrefetch 执行完成之后,_render 生成子节点的 vnode 进行渲染 waitForServerPrefetch(component, resolve, done); } }在经过上面的编译流程之后,我们已经拿到了 html 字符串,但如果要在浏览器中展示页面还需js, css 等标签与这个 html 组装成一个完整的报文输出到浏览器中, 因此需要模版渲染阶段来将这些元素实现组装。
TemplateRenderer.prototype.bindRenderFns = function bindRenderFns (context) { var renderer = this ;['ResourceHints', 'State', 'Scripts', 'Styles'].forEach(function (type) { context[("render" + type)] = renderer[("render" + type)].bind(renderer, context); }); context.getPreloadFiles = r**erer.ge****:**reloadFiles.bind(renderer, context); };在具体渲染模版时,会有以下两种情况:
TemplateRenderer.prototype.render = function render (content, context) { // parsedTemplate 用于解析函数得到的包含三个部分的 compile 对象, // 按照顺序进行字符串模版的拼接 var template = this.parsedTemplate; if (!template) { throw new Error('render cannot be called without a template.') } context = context || {}; if (typeof template === 'function') { return template(content, context) } if (this.inject) { return ( template.head(context) + (context.head || '') + this.renderResourceHints(context) + this.renderStyles(context) + template.neck(context) + content + this.renderState(context) + this.renderScripts(context) + template.tail(context) ) } else { ... } };至此我们了解了 Vue SSR 的整体架构逻辑和 vue-server-renderer 的核心代码,当然 SSR 也是有很多开箱即用的脚手架来供我们选择的。
Angula: Nest.js