https://ssr.vuejs.org/zh/universal.html

基本用法

  通过vue-server-renderer插件的createRenderer方法创建一个renderer,再调用这个renderer的renderToString(app),将一个带有template和data的vue实例渲染为字符串。

  渲染出来的字符串可以拼接到html字符串中再返回,也可以使用模板(createRenderer创建renderer的时候指定template,这个模板中使用 vue-ssr-outlet 来指定插入位置),在这个模板中使用{{}}可以渲染出转义后的字符串,{{{}}}可以渲染出没转义的字符串,以上两个插值可以通过renderToString传入的context来指定。

  以上的模板支持更高级的用法:

  1. 当时用vue单文件的时候自动注入关键css
  2. 当时用客户端清单的时候,自动注入资源link
  3. 当vuex状态数据在客户端进行混合的时候,能防范CSS

代码规范

  同一份代码需要运行在客户端和服务器。

  对于每个请求服务器都需要创建一个新的app(避免出现跨请求的状态污染),渲染这个app之前需要再服务器端预先获取数据,也就意味着在开始渲染之前,app的状态就应该是确定的,所以,响应式的数据变化是没有必要的,默认被禁止,禁止数据转换为响应式对象。

  服务端渲染仅仅会执行beforeCreate和created这两个钩子。不要在这两个钩子中使用setInterval,而应该把这些带副作用的api放在beforeMount和mounted中。

  跨平台的代码不应该使用平台限定的api,如window和document。而应该使用通用api或者库(如axios,提供了服务端和客户端相同的api来访问网络)。如果要访问浏览器api,应该放到客户端的钩子函数中执行

代码结构

  不在每次请求来的时候直接创建新的vue实例,而是导出一个工厂函数createApp来创建vue实例(同样的方式创建router,store和event bus)。也可以不通过工厂函数来生成实例,仅当使用bundle render而且配置了{runInNewContext:true},但这么做的话会消耗性能,因为每个请求都会创建一个新的vm context。

  使用webpack来打包客户端(client bundle用于发给浏览器来进行html混合)和服务器(server bundle被服务器使用,用于SSR)。

  router.js:返回一个工厂函数,内部创建并且配置了路由组件的映射,使用的组件是动态import的异步组件。

  app.js:在这里导出一个createApp工厂函数来返回vue实例和router,内部创建vue实例,获取router.js获取路由,然后将router注入到vue实例中,获取store.js,注入store。这个文件用于为其他地方提供vue实例。

  entry-client.js:前端逻辑,在浏览器运行,引入app.js,获取vue实例和router,当router.onReady之后才进行挂载。通过store.replaceState(window.__INITIAL_STATE__)将state序列化回来使用

  entry-server.js:在服务器运行,每次SSR都会调用这里的代码,进行数据预获取和获取实例。导出一个函数,该函数返回一个promise。内引入app.js,获取vue实例,根据函数参数context脚本化地设置路由router.push(url),当router.onReady之后,通过router获取到当前的组件(服务端渲染仅仅是渲染即将要显示的组件,其他异步组件还是要等到要显示的时候才在浏览器下载和渲染),调用当前组件的异步获取数据方法,获取到了之后,将store.state设置到context.state中(这里相当于把异步数据通过context传递到了外部的server.js),最后对vue实例进行resolve(app)。用webpack将这个文件打包为server-bundle.js

  server.js:处理get * 请求,然后获取req.url。引入server-bundle.js,给他传递一个context(里面包含了req.url),返回了一个promise,在promise中对get请求进行响应,把app renderToString出去(以上resolve(app)就把vue实例传递了过来)。当renderer(由createBundleRenderer创建)启用了template设置,则context传递给renderToString渲染时,context.state就会自动序列化到html中。

  在客户端必须要onReady之后再挂载,再服务器必须在onReady之后再进行vue实例的renderToString和响应。这么做的原因是:

the router must resolve async route components ahead of time in order to properly invoke in-component hooks.

数据预获取

  在SSR之前需要把页面依赖的异步数据,预先获取出来存放进store中,然后把state序列化内联到html中,客户端直接把内联的数据state获取出来,然后挂载vue实例即可

  store.js:返回一个工厂函数createStore,函数返回一个新的store,store可以根据id,设置自己state.item。

  在组件中的computed对象通过this.$store.state.item获取store中的item,组件的定义对象中写一个方法,该方法获取store和router,根据路由参数dispatch一个action。这个方法对于组件来说是静态方法。

  服务端的数据预获取是通过把state序列化到html中。客户端的数据预获取先不看

客户端的混合

  在客户端处把服务器发来的html进行转换为可由vue来管理的dom,仅仅执行挂载即可。当vue发现根元素上有data-server-rendered="true"这个值时,就会以混合的方式进行挂载转换。如果已有的html与客户端vue实例中的结构不一致,则混合失败(注意tbody)。在开发模式,混合失败会抛弃现有的html,全部在客户端重新生成;而对于产品模式,则保留当前的html,而不重新生成。

  在客户端进行混合后,data-server-rendered="true" 会从根元素上消失。如果混合失败的话,浏览器会报错:

[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.

  服务器端把App这个组件(跟元素为#app)渲染到了插槽的位置上,到了浏览器端(App这个组件的dom已经存在了),需要把App这个组件重新挂载回#app上。template仅仅写个插槽即可,而不需要写一个空的div#app标签。

bundle Renderer

  服务器通过require使用server bundle,当修改了源码,则需要重启服务器,而且因为nodeJS不支持sourceMap,对server bundle的调试很不方便。

  可以通过vue-server-renderer提供的createBundleRenderer来解决这个问题(可以使用sourceMap、hmr、关键css注入,注入用户清单等)。使用方式如下:通过webpack插件,生成server-bundle.json,然后将这个json传递给createBundleRenderer。

  回顾之前的renderer获取方式:通过 require('vue-server-renderer').createRenderer()来获取的。使用都是在server.js中使用

构建配置

  createBundleRenderer需要知道server-bundle以及客户端的清单文件信息,这样生成出来的renderer,才能往生成出来的html注入preload标签,以及客户端要执行的脚本标签。

对于runInNewContext这个设置:

  1. true(默认),对于每个请求,server bundle都会运行一次,而且每次运行的环境不同,并且与server线程的环境是隔离的(global对象不同)
  2. false,仅在第一次渲染的时候,执行server bundle,对于其他的请求到来时,不再执行server bundle。因为entry-server.js写成是一个模块,所以这种执行结果更加符合模块的执行思路(仅在第一次需要的时候执行一次,其余都只是读取值,而不执行)。而且server bundle的执行环境与server进程一致,共享同一个global对象
  3. "once",与false类似,唯一的区别在于server bundle的执行环境与server进程是隔离的,即global对象不同。

结论:使用false或“once”能提升性能,因为server bundle仅执行一次,使用once更好,因为环境隔离,防止污染服务器进程

服务端

  将entry-server.js作为entry、new VueSSRServerPlugin作为插件,会打包生成一个server-bundle.json,只需要将这个文件路径传递给createBundleRenderer作为第一个参数即可。

  entry-server.js用于数据预获取,然后返回app。如果没有异步操作预获取数据的话,直接在export default app即可,否则有异步操作,要求代码如下:

import { app } from './app.js'

export default context => {
    return new Promise((resolve, reject) => {
        // 处理异步数据、router onReady等
        // 在异步回调中执行 resolve(app) 即可
    })
}

客户端

  生成清单文件。通过entry-client.js生成client-manifest.json清单文件,再传递给createBundleRenderer。

  这个json文件包含了其他一些打包生成的js文件的信息(每个异步组件打包成一个独立的js文件):

{
  "publicPath": "/dist/",
  "all": [
    "0.bundle.js",
    "1.bundle.js",
    "main.bundle.js",
    "manifest.bundle.js",
    "0.bundle.js.map",
    "1.bundle.js.map",
    "main.bundle.js.map",
    "manifest.bundle.js.map"
  ],
  "initial": [
    "manifest.bundle.js",
    "main.bundle.js"
  ],
  "async": [
    "0.bundle.js",
    "1.bundle.js"
  ],
  "modules": {
    "58087528": [
      2,
      6
    ],
    "145842ed": [

  manifest.bundle.js作用是加载chunk,有如下代码(这个在router例子中是通过commonChunkPlugin生成的):

/******/         // start chunk loading
/******/         var head = document.getElementsByTagName('head')[0];
/******/         var script = document.createElement('script');
/******/         script.type = 'text/javascript';
/******/         script.charset = 'utf-8';
/******/         script.async = true;
/******/         script.timeout = 120000;
/******/
/******/         if (__webpack_require__.nc) {
/******/             script.setAttribute("nonce", __webpack_require__.nc);
/******/         }
/******/         script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";

  main.bundle.js体积最大,行数为15k以上,里面包含了entry-client.js的逻辑(估计还有vue的代码才会这么大)。

  entry-client.js中为客户端需要执行的逻辑,但template中没有这个脚本对应的标签,需要通过createBundleRenderer来自动注入,所以要生成客户端的清单文件,传递给这个函数。

手动注入资源

  当我们需要更加细粒度地控制资源或者不通过模板注入资源的时候,可以手动注入资源。

  在创建renderer的时候,设置 inject:false即可。

CSS的管理

  推荐在vue单文件的style标签内管理css,它有如下好处:

  • 提供作用域
  • postcss预处理器
  • 开发阶段的热加载
  • vue-loader内置的vue-style-loader在我们使用bundleRender的时候,可以提供组件的关键css(要启用template选项)。
  • main chunk中的css被抽取出来,有利于缓存(使用的是extra-text-webpack-plugin),抽取出来的css被自动注入到template中(需要做额外配置)。

管理header

  提供了一种手段来从组件内设置外部的context,即在组件created或者beforeCreated的时候,通过this.$ssrContext可以直接访问到context,添加数据后可以通过context插值的形式将组件内的数据显示到template上。

  因为浏览器端装在的时候,并没有$ssrContext这个对象,所以需要进行环境判断(DefinePlugin)

缓存

  虽然vue ssr已经很快了,但性能比不上纯字符串模板渲染,因为ssr需要创建组件实例和虚拟dom节点,所以巧妙使用缓存策略,可以提升性能。

页面级别的缓存

  大部分时候应用程序需要依赖额外的数据,动态的内容不能被缓存,然而如果内容和用户无关(not user-specific),如相同的url对所有用户都显示相同的页面,我们可以使用一个策略叫做“微缓存”来大幅度提升高拥堵时程序的性能。这通常在nginx层实现,但我们也可以在node层实现,思路如下:首先根据请求,判断响应的数据是否可以被缓存,可以的话就去拿缓存看看能不能命中。命中则直接返回缓存,否则进行渲染renderToString,然后把结果缓存起来(url为key,html为value,缓存的期限较短,如1s)。这种策略可以使服务器最多1s渲染一次页面。

组件级别的缓存

  vue-server-renderer内置了这种缓存,启用这个功能,只需要在创建renderer的时候,提供一个缓存实现。启用组件级别的缓存,对组件的定义有如下要求:

  1. 拥有一个唯一的name属性,缓存的时候会用这个name作为key
  2. 拥有一个能反映组件外观的映射函数serverCacheKey,这个函数返回一个常量(如总是返回true)的话会导致组件总是被缓存,这在静态组件中比较好用。猜测这个原理:组件渲染的内容为value,而key = comp.name + comp.serverCacheKey(comp.props),以两个值来映射一个渲染结果,因为很好理解组件的渲染结果 = 组件 + 组件的props。

  因为当一个组件缓存被renderer命中时,这个组件的子组件也不会再次渲染。所以当子组件依赖于全局状态、或者子组件会对context进行修改的时候,这个组件就不应该被缓存

vue-ssr 文档备注的更多相关文章

  1. Vue 学习文档

    Vue 学习文档 vue 起步 引包 启动 new Vue(options) options: el 目的地(可以用类名.标签名等,也可以直接用mod元素) #elementId .elementCl ...

  2. 解放生产力,自动化生成vue组件文档

    一.现状 Vue框架在前端开发中应用广泛,当一个多人开发的Vue项目经过长期维护之后往往会沉淀出很多的公共组件,这个时候经常会出现一个人 开发了一个组件而其他维护者或新接手的人却不知道这个组件是做什么 ...

  3. 打造自己的Vue组件文档生成工具

    程序员最讨厌的两件事情,第一种是写文档,另一种是别人没有写文档.有没有直接根据vue组件生成文档的呢?当然是有的的.但第三方使用起来不一定能和现有项目结合使用,往往需要额外的注释用来标记提取信息.使用 ...

  4. vite插件-自动生成vue组件文档

    特点 支持热更新 快速启动,依赖于 vite,无需另起服务 自动生成组件导航 ui 采用了vant-ui的样式 核心方法覆盖率达到了 92.86% 使用 yarn add vite-plugin-vu ...

  5. Vue.Draggable 文档总结

    本文章转自https://blog.csdn.net/zjiang1994/article/details/79809687 Vue.Draggable学习总结 Draggable为基于Sortabl ...

  6. Vue.js文档

    参考网址:https://vuefe.cn/ 第一  安装 1.下载到本地后使用<script>标签直接引入 2.使用CDN引入 例如:使用CDN引入 <script src=&qu ...

  7. Vue.js文档学习

    Vue细碎小点 生命周期钩子:created().mounted().updated().destroyed() 不要在选项属性或回调上使用箭头函数,比如 created: () => cons ...

  8. vue路由文档笔记

    引入router this.$router 和 router 使用起来完全一样.我们使用 this.$router 的原因是我们并不想在每个独立需要封装路由的组件中都导入路由 可以在任何组件内通过 t ...

  9. vue ssr

    https://mp.weixin.qq.com/s/v1c69bJ5PxGcqt-ZU4FVXw https://juejin.im/entry/590ca74b2f301e006c10465f h ...

随机推荐

  1. C 语言实例 - 二进制与十进制相互转换

    C 语言实例 - 二进制与十进制相互转换 C 语言实例 C 语言实例 二进制转与十进制相互转换. 实例 - 二进制转换为十进制 #include <stdio.h> #include &l ...

  2. css margin padding 四个方向

    margin 和padding虽然有四个单独的方向属性,如margin-left,padding-bottom等等. 但是可以用margin:2px 3px 4px 5px     四个参数的含义:上 ...

  3. 开源Html5+Websocket+Mqtt实时聊天室

    本应用示例使用Coolpy7作为Mqtt服务器并启用Websocket代理完美支持高并发大流量即时通过能力,本示以即时通信聊天为为例.还可以应用到其他软件应用如:网页客服系统.网站信息通知.网页即时通 ...

  4. 重构学习day01 类型码 类型码的上层建筑 与类型码相关的重构方法 1.使用子类代替类型码 2.使用状态或策略模式代替类型码

    名词:类型码 类型码的上层建筑 重构方法 1.使用子类代替类型码 2.使用状态/策略模式代替类型码 类中存在方法把某个字段当作条件,根据字段值的不同,进行不同的处理.(自定义概念)则这个字段叫做:类型 ...

  5. STP-4-每VLAN生成树和Trunk上的STP

    如果在有冗余链路且有多个VLAN的交换网络中只使用 STP实例,那么在稳定状态中,仍会有一些端口处于阻塞状态不被使用,冗余链路实际上变成了备份链路. PVST+特性能为每个VLAN创建一个STP实例. ...

  6. 线段树-区间更新-HDU 1689

    #include <iostream> #include <cstdio> #include <string> #include <cstring> # ...

  7. 利用apache限制IP并发数和下载流量控制

    一,为什么要对IP并发数,下载流量进行控制 说正题之前,先给大家讲个故事,那是在2007年,我进了一家公司,当时我们正在给达芙妮做电子商务网,www.idaphne.com.从三月份开始做的吧,九月份 ...

  8. Java编程基础-运算符

    Java中的运算符大致分为:算术运算符.赋值运算符.关系运算符.逻辑运算符和位运算符五类. (1).算术运算符:+  -  *  /  %  ++  -- (2).赋值运算符:=  +=  -=  * ...

  9. WEB 前端菜鸟,感觉很迷茫,该怎么做?

    前几天看到这样的问题 先说问题吧:感觉前端涉及到的东西太多了,自己也很浮躁,看了挺多书,可是代码缺敲得却不多.技术菜,又什么都想学,比如现在纠结要不要先学scss或者php或者angularjs,ba ...

  10. JavaScript30-7 数组的一些基本方法

    本次来学习数组的一些方法,之前学习的js数组的方法是在第四课里面(没有写到随笔里面) 之前第四课主要讲的是 filter() ,map() 这次课程主要介绍的是 some()`.`every()`.` ...