通过vue ssr简单实现来理解代码限制
vue srr 项目的编码是有一些限制的. 这里通过简单了解实现过程来理解这些限制.
原因
ssr 编码限制零零碎碎有非常多, 但这些限制的源头非常容易找, 以源头出发就很容易理解和记忆这些限制.
ssr 项目比 scr 项目多做的事情是服务端读取应用( app )并生成第一屏的html字符串返回给浏览器.
这个特点可以拆分为3个大方向:
- 目标是字符串产生的问题: 因为服务端渲染的目标是”快照” 的字符串, 所以会有没有 js 环境, 没有 bom 环境导致的问题.
- 同构产生的问题: 因为服务器需要读取应用( app )代码. 那么我们写的代码就要考虑在 node 环境中运行的问题.
- 副作用代码产生的问题: 如果应用( app )代码有副作用, 本期望应该在浏览器发生的, 发生在服务端就会产生非预期的行为.
解决方案也有2个方向:
- 在需要的时候判断环境写不同的代码: 判断构建时带上的环境变量, 利用 ssr 不会调用的生命周期函数.
- 框架内部或者是 ssr 框架写一些通用场景的解决方案. (指 vue 和 nuxt )
快照产生的问题
因为服务器读取应用( app )的目的是产生字符串, 所以:
- 和挂载相关的生命周期函数( onMounted 和 onUnmounted )在服务器上执行的时候是获取不到 dom 的. 并且在语义上也没有挂载动作, 只会做字符串拼接. 所以 ssr 场景不会调用这2个生命周期.
- 也因为是字符串, 所以没有 js 环境, 就没有 effect, 所以配合 effect 的响应式 api ( reactive, ref 等)也是没意义的, 在 ssr 场景, 都不会调用这些 api 以节省性能开销.
同构产生的问题
因为我们写页面, 主要是浏览器思维, 所以主要要注意的是 bom 环境的 api. ( window, document )
- 对于 请求类的, 可以使用 axios , ofetch ( nuxt 用的) 来兼容 server / client 端.
- 对于只有 bom 有的 api, 比如 localstorage, 可以写在 onMounted / onUnmounted 生命周期, 上一节已经提过, 这2个生命周期不会在服务端执行.
对于同构产生的问题, 有个更简单的解决方案, 就是通过构建定义的变量( 如 vite 是 import.meta.ssr )来判断.
另外对于 html 的标准 server 和 bom 端有点区别, div 不能作为 p 的子节点.
副作用代码产生的问题
一些本来是浏览器设计的副作用, 在服务器上运行就会产生问题.
这个问题比较难猜, 这里就说一下文档里提到的.
- 设置setInterval的时候, 可以在 onMounted, 和 onUnmounted 中设置. 如果在 setup 中设置, 就会导致服务器内存溢出了.
- 在应用( app )通过多处引入 reactive 变量来实现共享 state 的. 在服务端就变为同一个变量, 产生了跨请求变量污染. 解决方案是每个应用引入单例的变量.
数据获取是怎么实现的
数据可以在服务端获取, 但 reactive 是没有的, 那么代码如何写呢?
问了朋友, 原来是要用到 next / nuxt 提供的方法.
我看了nuxt的useAsyncData
, 我们来看一下他的实现流程.
我们先来看用法, 来自官网:
1 | <script setup> |
写法很容易理解, 是写在 setup 中的.
效果是在服务端取好数据, 直接返回给浏览器, 并且浏览器不需要重新请求.
这样还有个优点, 可以对用户隐藏一些接口.
现在我们来看一下useAsyncData
的流程, 目标是了解服务端和客户端分别做了什么, 并且是如何通信的.
我们关注useAsyncData
返回了什么: 获取到的数据( data ), 刷新方法( refresh ) 和一些状态.
找到了代码, 200多行, 比较简单, 分为以下步骤:
尝试获取缓存.
服务端从
nuxtApp.static
获取, 浏览器从nuxtApp.payload
获取.如果在服务端运行, 通过vue的
onServerPrefetch
生命周期来执行refresh
.如果是浏览器端, 如果有缓存, 则直接使用缓存, 如果没有, 就通过vue的
onMounted
生命周期来执行refresh
.refresh
方法的内容主要是调用获取数据的函数, 并在返回的时候设置缓存. (设置nuxtApp.payload
,nuxtApp.static
是通过插件赋值的)
我们了解了服务端和浏览器是通过payload
放到vue实例上通信的, 但具体的做法没理解, 相关代码文件都有payload
.
简单地从服务器范围的内容来看, 服务器是生成了一份payload
文件, 在 html 中引入的.
简单说明vue的ssr过程
这里的内容来自于hcy的书, 并简化了很多.
服务端渲染过程
应用( app )的最外层是一个组件, vue 通过调用组件中描述的各个生命周期, 并获得 vnode, 最后递归渲染 vnode.
1 | function renderComponent (vnode) { |
renderVNode
的内容是, 如果 vnode 是个组件, 就递归调用renderComponent
, 如果是其他的节点, 就可以输出字符串了.
客户端注水过程
客户端获取到的 html 是的干净的, 他离动态页面还差哪些内容? 一是元素的事件需要绑定, 二是建立 render effect, 也就是mountComponent. ( 其实就是上面renderComponent
函数的浏览器端原型)
hydrate方法接受2个参数: vnode 和 container. 和 csr 的 render 区别也就是 container 是已经有内容的.
1 | function hydrate(vnode, container) { |
mountComponent的代码比较多, 而且属于前置知识点, 主要思路是和上一章节renderComponent
一样的. (并且多了 mounted / unmounted 生命周期函数, 和 render effect 和相关的 reactivity 函数的包裹)
这里还是简单写一下改变的部分:
1 | function mountComponent(vnode, container, anchor) { |
上面是 render effect 的建立过程, 最后还有一个函数hydreateElement
, 用来绑定元素事件.
1 | function hydreateElement(el, vnode) { |
next
看了一些 nuxt 代码, 对 nuxt 也提起了兴趣, 下次分析下 nuxt 的主要流程.
(本文完)
如果你觉得本文对你有帮助, 你可以请我喝一杯咖啡
本文遵循cc协议
你可以在注明出处和非商用的前提下任意复制及演绎