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

前言

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

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

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

客户端渲染(CSR)

渲染流程

客户端渲染大致流程

对应的 performance 面板的快照

CSR 优点

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

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

CSR 缺陷

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

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

服务端渲染(SSR)

渲染流程

简单的渲染流程

搭建 node 服务

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

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

效果演示

以下是 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 元素,除此之外该组件还会被打包到一个 JavaScript 文件中,并在客户端被下载和解释执行,也就是进入 客户端激活,后续页面内容的渲染都不需要服务器进行处理动态编译处理.

构建同构渲染

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

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

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

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

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

效果演示

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

源代码

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7115646231640014885
今日推荐