Vuejs设计与实现 —— 同构渲染

简介: Vuejs设计与实现 —— 同构渲染

image.png


前言

Vue.js 是一个构建客户端应用的框架,组件的代码会在浏览器中运行,然后向页面输出 DOM 元素,也就是我们最常用的方式,即 客户端渲染(client-side rendering,CSR).

实际上 Vue.js 还可以在 Node.js 环境中运行,即可将相同组件渲染成相应的字符串,并发送给浏览器进行渲染,这就是 服务端渲染(server-side rendering,SSR).

Vue.js 作为现代前端框架,除了能够分别支持 CSRSSR 渲染之外,还能够同时支持 CSRSSR,这就是所谓的 同构渲染(isomorphic rendering).

客户端渲染(CSR)

渲染流程

客户端渲染大致流程

image.png

对应的 performance 面板的快照

image.png

CSR 优点

通常 客户端渲染 伴随着 单页面应用(single-page application,SPA)前端路由 等,相比于早期的 服务端路由 的渲染方式带来了一定的优势:

  • 用户体验更好
  • 早期的 服务端路由 方式,会导致从 A 页面跳转到 B 页面时,页面会重新刷新并对整个页面重新进行渲染,这个过程会让用户感觉不够流畅,基于 前端路由 的方式并不会真正进行 页面跳转,带来了更高的流畅度
  • 占用服务端资源少
  • 早期的 服务端路由 方式,会将完整的页面返回给客户端,意味着要在 服务端 访问数据库,并且需要将对应的数据和页面进行融合,所以对服务端而言,一次路由访问就需要做这两件事,若访问的并发量高,会导致服务端需要额外处理这些计算,自然会占用服务端有限的资源
  • CSR 渲染则是交由客户端进行处理,服务端不需要关心渲染计算的过程,减轻了服务端的压力

CSR 缺陷

客户端渲染 仍是目前使用最多得渲染模式,除非一些特殊场景下 CSR 无法满足对应的需求:

  • "白屏" 时间较长
  • 主要是因为 CSR 渲染需要 *.js 的支持,而 *.js 又必须保证 *.html 被接收和解析, *.html 又强依赖于当前的 网络环境,因此,在差网环境下回导致 白屏时间过长,特别是在移动网络环境下
  • 对 SEO 的支持不友好
  • 这一点也很好理解,因为 白屏时间较长 导致在一段时间内没有重要的内容能够交由 搜索引擎 进行分析、分类、打标签等,并且 搜索引擎 并不会等待页面渲染完成,因此对 SEO 优化并不友好

服务端渲染(SSR)

渲染流程

简单的渲染流程

image.png

搭建 node 服务

搭建一个简单的 node 服务来观察 SSR 的效果,内容比较简单不过多赘述,其中需要注意的是:

  • Node.js 服务器是长期运行的进程,当代码第一次被导入进程时,它会被执行一次然后 保留在内存里
  • 如果只创建了一个 vue 的单例对象,它将被 每次发来的请求共享,这是不符合实际需求的,因此,需要为每个请求重新生成一个 vue 实例,避免相互影响

效果演示

image.png

以下是 node 环境相关代码:

const express = require("express");
const { createSSRApp } = require("vue");
const { renderToString } = require("@vue/server-renderer");
const app = express();
// Node.js 服务器是长期运行的进程,当代码第一次被导入进程时,它会被执行一次然后保留在内存里
// 如果只创建了一个 vue 的单例对象,它将被每次发来的请求共享,这是不符合实际需求的
// 因此,需要为每个请求,重新生成一个 vue 实例,避免相互影响
function createApp(msg) {
  return createSSRApp({
    data() {
      return {
        msg,
      };
    },
    template: `<h1>{{ msg }}</h1>`,
  });
}
function getHtmlStrWrap(contentStr) {
  return `
    <!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>
  <body>
    <h3><a href="/home"> home </a></h3>
    <h3><a href="/about"> about </a></h3>
    <h3><a href="/test"> error path </a></h3>
    <div id="app">${contentStr}</div>
</html>
    `;
}
app.get("/home", async (req, res, next) => {
  const vueStr = await renderToString(createApp("Home Page!!"));
  const htmlStr = getHtmlStrWrap(vueStr);
  res.end(htmlStr);
});
app.get("/about", async (req, res, next) => {
  const vueStr = await renderToString(createApp("About Page!!"));
  const htmlStr = getHtmlStrWrap(vueStr);
  res.end(htmlStr);
});
app.get("*", async (req, res, next) => {
  const vueStr = await renderToString(createApp("Not Found Page!!"));
  const htmlStr = getHtmlStrWrap(vueStr);
  res.end(htmlStr);
});
app.listen(8000, (err) => {
  if (err) {
    console.error("server fail:", err);
    return;
  }
  console.log("server is runing at http://localhost:8000");
});
复制代码

SSR 优势

  • 不存在 白屏时间过长 问题
  • 更快的内容呈现,尤其是网络连接缓慢或设备运行速度缓慢的时候,服务端标记 不需要等待所有的 JavaScript 都被下载并执行之后才显示,所以用户可以更快看到完整的渲染好的内容
  • 更好的搜索引擎优化 (SEO)
  • 搜索引擎爬虫会直接读取 完整渲染 出来的页面
  • 通过 API 调用获取的内容,爬虫是不会等待页面加载完成

SSR 缺点

  • 需要保证开发一致性
  • 浏览器特有的代码只能在特定的生命周期钩子中使用
  • 一些外部库在服务端渲染应用中可能需要经过特殊处理
  • 需要更多的构建设定和部署要求
  • 不同于一个完全静态的 SPA 可以部署在任意的静态文件服务器,服务端渲染应用需要一个能够运行 Node.js 服务器的环境
  • 更多的服务端负载
  • Node.js 中渲染一个完整的应用,会比仅供应静态文件产生更密集的 CPU 运算
  • 若访问流量很高,就必须要准备好与其负载相对应的服务器,以及采取 合理的缓存策略

SSR 和 预渲染

基于以上 SSR 的优缺点对比,只有明确具体页面的具体需求才能更好的决定是否需要使用 SSR,如果只是希望通过 SSR 来改善一些 推广页面 (如 //about/contact 等) 的 SEO,那么应该优先考虑 预渲染 的方式.

SSR 是一个 动态编译 HTML 的 web 服务器,而 预渲染 可以在 构建时 为指定的路由 生成静态 HTML 文件,且预渲染的设置比 SSR 更加简单,也支持生成为一个完全静态的 HTML 文件.

预渲染 需要和 打包构建工具(webpack、rollup 等) 进行配合,如 webpack,就可通过 prerender-spa-plugin 来支持 预渲染.

同构渲染(isomorphic rendering)

基于 CSRSSR 各自的优缺点,如果可以将它们进行结合,那么就可以实现互补,而这也就是 同构渲染 需要做的事,其中的 同构 就是指 应用代码的主体 可以同时运行在 服务端客户端.

同构流程

服务端渲染应用快照

在服务端,Vue 组件会被渲染为静态的 HTML 字符串,然后发送给客户端浏览器,服务端生成的 HTML 内容是在当前数据状态下应用的快照:

  • 生成应用快照的同时,还会生成当前数据状态的 初始数据,用于提供给客户端做初始化处理
  • 应用快照不具备事件绑定能力,即定义好的事件不会被注册到对应的 DOM
  • 应用快照不具备数据响应式的能力,即不具备和用户进行数据交互的能力,不会执行 beforeUpdate、updated 生命周期
  • 应用快照不具备节点挂载的能力,即不需要在服务端运行时进行节点挂载操作,不会执行 beforeMount、mounted 生命周期钩子
  • 应用快照不具备组件销毁的能力,即不会执行组件的 beforeUnMount、unMount 生命周期钩子

服务端渲染时不提供上述的功能是因为在服务端渲染根本不需要关注这些,另外也是为了使服务端的渲染压力更小,关注更少的内容。

客户端激活

在浏览器端,需要渲染这段从服务端返回的 HTML 内容,即此时页面中已经存在 组件对应的 DOM 元素,除此之外该组件还会被打包到一个 JavaScript 文件中,并在客户端被 下载、解析、执行,也就是进入 客户端激活,后续页面内容的渲染都不需要服务器进行处理动态编译处理。

客户端的 JavaScript 脚本处理核心内容:

  • 将当前页面已渲染的 DOM 元素与 Vue.js 所渲染的 虚拟 DOM 之间建立联系
  • 由于 真实 DOM虚拟 DOM 对象都是树形结构,并且节点间存在相互对应关系,激活 就可以通过递归地在 真实 DOM虚拟 DOM 之间建立联系,即 vnode.el = el,并保证是从容器元素的第一个子节点开始,即 el.firstChild
  • 为页面中的 DOM 元素添加事件绑定,使得页面本身支持事件交互
  • Vue.jsHTML 页面中提取由服务端序列化后发送过来的数据,用于初始化整个 Vue.js 的应用程序

同构编码注意点

组件生命周期

当组件代码在服务端运行时,由于不会对组件进行真正的挂载,即不会把虚拟 DOM 渲染为真实 DOM 并且服务端渲染只是一个应用的快照,不存在数据变化后的更新渲染,因此只有 beforeCreate、created 会被执行,因此涉及相关逻辑需要注意编写生命周期钩子。

还需要注意的是,在服务端渲染时和定义器相关的一些操作是没有任何意义的,因此一般需要通过 环境变量 的方式控制对应逻辑是否需要被执行,又或者将定时器移动到只有客户端才会执行的生命周期钩子中。

跨平台 API

由于组件代码既可以运行在浏览器端,也可以运行在服务端,因此,在编码时要特别注意特定平台特有的 API,如浏览器特有的 window、document 等对象,涉及到特定 API 要么使用跨平台的第三方库来作为兼容处理,如 axios,否则仍然需要通过上述的环境变量和调动生命周期的方式达到目的。

不可控的第三方模块

通常,自己编写的组件代码是可控的,但代码中的第三方模块代码确不能保证其可控性,而很多时候我们本身也不能去修改第三方模块的代码,因此可以通过动态引入模块的方式实现加载,如 import(...),这样在配合环境变量的方式就能实现只在某一端引入模块。

避免应用状态污染

在服务端渲染时,需要为每个请求创建一个全新的应用实例,主要是为了避免不同请求共用同一个应用实例导致应用状态被污染,同时要注意在编写组件代码中可能会导出的全局变量。

构建同构渲染

服务端 要渲染 Vue 组件 意味着需要处理 *.vue*.css*.ts 等依赖模块,而这些是 node 本身就不能处理的内容,也不是 renderToString 能够处理的,因此需要借助 打包构建工具(如 webpack) 进行处理.

客户端 实际也需要一个独立的客户端构建版本,虽然最新版本的 Node.js 完全支持 ES2015 特性,但对于旧的浏览器仍然需要对代码进行转译、兼容处理.

基本思路,使用 webpack 同时打包客户端和服务端应用,其中服务端的包会被引入到服务端用来渲染 HTML,同时客户端的包会被送到浏览器用于 激活静态标记.

与之对应的两个入口文件就是:entry-client.jsentry-server.js

image.png

篇幅有限,更多具体的配置可参见 官方文档

效果演示

以下是根据官方文档配置得到运行效果:

image.png

目录
相关文章
|
2月前
|
JavaScript
vue异步渲染
vue异步渲染
|
17天前
|
监控 JavaScript 前端开发
Vue 异步渲染
【10月更文挑战第23天】Vue 异步渲染是提高应用性能和用户体验的重要手段。通过理解异步渲染的原理和优化策略,我们可以更好地利用 Vue 的优势,开发出高效、流畅的前端应用。同时,在实际开发中,要注意数据一致性、性能监控和调试等问题,确保应用的稳定性和可靠性。
|
2月前
|
JavaScript 前端开发
Vue学习笔记8:解决Vue学习笔记7中用v-for指令渲染列表遇到两个问题
Vue学习笔记8:解决Vue学习笔记7中用v-for指令渲染列表遇到两个问题
|
2月前
|
JavaScript 前端开发 API
Vue学习笔记7:使用v-for指令渲染列表
Vue学习笔记7:使用v-for指令渲染列表
|
2月前
|
人工智能 JavaScript 索引
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
这篇文章讨论了在Vue中进行列表渲染时遇到的“Duplicate keys detected”错误。这个错误通常发生在使用 `v-for` 指令渲染列表时,如果没有为每个循环项指定一个唯一的 `key` 属性,或者指定的 `key` 属性值重复了。文章提供了导致错误的原始代码示例,并给出了修正后的代码,通过在 `key` 绑定中加入索引确保 `key` 的唯一性。此外,文章还解释了为什么需要唯一 `key` 以及如何解决这个问题。
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
|
2月前
|
JavaScript 前端开发 UED
组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList
该文章详细介绍了如何使用Vue3结合TypeScript来开发全局Header组件和列表数据渲染组件ColumnList,并提供了从设计到实现的完整步骤指导。
|
3月前
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
67 1
|
3月前
|
JavaScript
Vue学习之--------列表渲染、v-for中key的原理、列表过滤的实现(2022/7/13)
这篇博客文章详细介绍了Vue中列表渲染的基础知识、`v-for`指令的使用、`key`的原理和列表过滤的实现。通过代码实例和测试效果,展示了如何遍历数组和对象、使用`key`属性优化渲染性能,以及如何实现列表的动态过滤功能。
Vue学习之--------列表渲染、v-for中key的原理、列表过滤的实现(2022/7/13)
|
3月前
|
JavaScript 前端开发
Vue学习之--------绑定样式、条件渲染、v-show和v-if的区别(2022/7/12)
这篇博客文章讲解了Vue中绑定样式和条件渲染的方法,包括类样式绑定的不同写法、`v-show`和`v-if`的条件渲染区别以及它们的使用场景和特点,并通过代码实例和测试效果来展示具体应用。
Vue学习之--------绑定样式、条件渲染、v-show和v-if的区别(2022/7/12)
|
3月前
|
JavaScript UED
强制 Vue 重新渲染组件的5种方法,解决你开发过程中数据和视图无法同步的Bug。
强制 Vue 重新渲染组件的5种方法,解决你开发过程中数据和视图无法同步的Bug。