当前位置:   article > 正文

服务端渲染

服务端渲染

同构渲染

Vue.js可用于构建客户端应用程序,组件的代码在浏览器中运行,并输出 DOM 元素。同时,Vue.js 还可以在 Node.js 环境中运行,它可以将同样的组件渲染为字符串并发送给浏览器。这实际上描述了 Vue.js 的两种渲染方式,即客户端渲染(client-side rendering, CSR)以及服务端渲染(server-side rendering, SSR)。另外,Vue.js 作为现代前端框架,不仅能够独立地进行 CSR 或者 SSR,还能够将二者结合,形成所谓的同构渲染(isomorphic rendering)。本篇文章我们将讨论 CSR、SSR 以及同构渲染之间地异同,以及 Vue.js 同构渲染的实现机制。

服务端渲染的工作流程图:

在这里插入图片描述

  1. 用户通过浏览器请求站点
  2. 服务器请求 API 获取数据
  3. 接口返回数据给服务器
  4. 服务器根据模板和获取的数据拼接出最终的 HTML 字符串
  5. 服务器将 HTML 字符串发送给浏览器,浏览器解析 HTML 内容并渲染

当用户再次通过超链接进行页面跳转,会重复上述5个步骤。可以看到,传统的服务端渲染的用户体验是非常差的,任何一个微小的操作都可能导致页面刷新。

与 SSR 在服务端完成模板和数据的融合不同,CSR 是在浏览器中完成模板与数据的融合,并渲染出最终的 HTML 页面。下图是 CSR 的详细工作流程图:

在这里插入图片描述

  • 客户端向服务器或者 CDN 发送请求,获取静态的 HTML 页面。注意,此时获取的 HTML 页面通常是空页面。在 HTML 页面中,会包含

当用户再次的通过点击“跳转”到其他页面时,浏览器并不会真正的进行跳转操作,即不会进行刷新,而是通过前端路由的方式动态地渲染页面,这对用户的交互体验会非常友好。但很明显的是,与 SSR 相比,CSR 会产生所谓的“白屏”问题。实际上,CSR 不仅仅会产生白屏问题,他对 SEO (搜索引擎优化)也不友好。

下表从多个方面比较了 SSR 和 CSR

SSRCSR
SEO友好不友好
白屏问题
占用服务端资源
用户体验

SSR 和 CSR 各有优缺点。SSR 对 SEO 更加友好,而 CSR 对 SEO 不太友好。由于 SSR 的内容到达时间更快,因此他不会产生白屏问题。相对地,CSR 会有白屏问题。另外,由于 SSR 是在服务端完成页面渲染的,所以他需要消耗更多服务端资源。CSR 则能够减少对服务端资源的消耗。对于用户体验,由于 CSR 不需要进行真正的“跳转”,用户会感觉更加流畅,所以 CSR 相比 SSR 具有更好的用户体验。我们需要从项目的实际出发,决定采用哪一个。例如你的项目非常需要 SEO,那么就应该采用 SSR。

同构渲染

同构渲染分为首次渲染(即首次访问或刷新页面)以及非首次渲染。

实际上,同构渲染中的首次渲染与 SSR 的工作流程是一致的。也就是说,当首次访问或者刷新页面时,整个页面是在服务端完成渲染的,浏览器最终得到的是渲染好的 HTML 页面。但是该页面是纯静态的,不能进行任何交互。除此之外,同构渲染所产生的 HTML 页面与 SSR 所产生的 HTML 页面最大的不同是,即前者会包含当前页面所需要的初始化数据(服务器通过 API 请求的数据会被序列化为字符串,并拼接到静态的 HTML 字符串中,最后一并发送给浏览器)。

假设浏览器已经接收到初次渲染的静态 HTML 页面,接下来浏览器会解析并渲染该页面。在解析过程中,浏览器会发现 HTML 代码中存在 和

  • Vue.js 在当前页面已经渲染的 DOM 元素以及 Vue.js 组件所渲染的虚拟 DOM 之间建立联系,即 vnode.el = el。
  • Vue.js 从 HTML 页面中提取由服务端序列化后发送过来的数据,用以初始化整个 Vue.js 应用程序

激活完成后,整个应用完成被 Vue.js 接管为 CSR 应用程序了。后续操作都会按照 CSR 应用程序的流程来执行。当然,如果刷新页面,仍然会进行服务端渲染,然后再进行激活,如此往复。

下表对比了 SSR、CSR 和 同构渲染的优劣。

SSRCSR同构渲染
SEO友好不友好友好
白屏问题
占用服务端资源
用户体验

对于同构渲染最多的误解是,它能够提升可交互时间(TTI)。事实是同构渲染仍然需要像 CSR 那样等待 JavaScript 资源加载完成,并且客户端激活完成后,才能响应用户操作。因此,理论上同构渲染无法提升可交互时间。

同构渲染的“同构”一词的含义是,同样一套代码既可以在服务端运行,也可以在客户端运行。

客户端激活原理

我们知道,对于同构渲染来说,组件的代码会在服务端和客户端分别执行一次。在服务端,组件会被渲染为静态的 HTML 字符串,然后发送给浏览器,浏览器再把这段纯静态的 HTML 渲染出来。这意味着,页面已经存在对应的 DOM 元素。当组件的代码在客户端执行时,不需要再创建相应的 DOM 元素。但是,组件代码在客户端运行时,仍然需要做两件重要的事:

  • 在页面中的 DOM 元素与虚拟节点对象之间建立联系
  • 为页面中的 DOM 元素添加事件绑定
编写同构代码
  1. 组件的生命周期

    我们知道,当组件的代码在服务端运行时,由于不会对组件进行真正的挂载操作,即不会把虚拟 DOM 渲染为真实 DOM 元素,所以组件的 beforeMount 与 mounted 两个钩子函数不会执行。又因为服务端渲染的是应用的快照,所以不存在数据变化后的重新渲染,因此,组件的 beforeUpdate 和 updated 这两个钩子函数也不会执行。另外,在服务端渲染时,也不会发生组件被卸载的情况,所以组件的 beforeUnmount 与 unmounted 这两个钩子函数也不会执行。实际上只有 beforeCreate 和 created 这两个钩子函数会在服务端执行。

    快照指的是在当前数据状态下页面呈现的内容。在服务端渲染时,定时器内的代码是没有任何意义的。遇到此类问题,解决方案通常有两个:

    • 将创建定时器的代码移动到 mounted 钩子中,即只在客户端执行定时器
    • 使用环境变量包裹这段代码,让其不在服务端运行
  2. 使用跨平台的 API

    编写同构代码的另一个关键点是使用跨平台的 API 。由于组件的代码既运行于浏览器,又运行于服务器,所以在编写代码的时候要避免使用平台特有的 API。例如,仅在浏览器环境中才存在的 window、document 等对象。

  3. 只在某一端引入模块

    通常情况下,我们自己编写的组件代码是可控的,这时我们可以使用跨平台的 API 来保证代码“同构”。然后,第三方模块的代码非常不可控。

    方案1:使用 import.meta.env.SSR 来做代码守卫。

    方案2:条件引入,使用 import.meta.env.SSR 来实现特定环境下的模块加载;通常我们还需要再实现一个具有同样功能并且可运行于服务端的模块。

    <script>
    let storage;
    if (!import.meta.env.SSR) {
      // 用于客户端
      storage = import('./storage.js');
    } else {
      // 用于服务端
      storage = import('./storage-server.js');
    }
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
  4. 避免交叉请求引起的状态污染

    在服务端渲染时,我们会为每一个请求创建一个全新的应用实例,例如:

    improt { createSSRApp } from 'vue';
    import { renderToString } from '@vue/server-renderer';
    import App from 'App.vue';
    
    // 每一个请求到来,都会执行一次 render 函数
    async function render(url, manifest) {
      // 为当前请求创建应用实例
      const app = createSSRApp(App);
      const ctx = {};
      const html = await renderToString(app, ctx);
      return html;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可以看到,每次调用 render 函数进行服务端渲染时,都会当前请求调用 createSSRApp 函数来创建一个新的应用实例。这是为了避免不同请求共用一个应用实例所导致的状态污染。

    状态污染还可能发生在单个组件的代码中,如下:

    <script>
      // 模块级别的全局变量
      let count = 0;
      export default {
        create() {
          count++;
        }
      }
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码在浏览器中运行没有问题,因为浏览器与用户是一对一的关系,每一个浏览器都是独立的。但是运行在服务器中会有问题,因为服务器与用户是一对多的关系。假如用户A和用户B都发送请求到服务器时,此时count++会被执行两次,对于用户A和用户B来说,会相互受到影响,造成了交叉污染。

  5. 组件

    这是一个对编写同构代码非常有帮助的组件。在日常开发中,使用第三方模块不一定对 SSR 友好。

    <template>
      <SsrIncompatibleComp />
    </template>
    
    • 1
    • 2
    • 3

    假如 是一个不兼容 SSR 的第三方组件。我们可以自行实现一个 的组件,该组件可以让模板的一部分内容仅在客户端渲染,代码如下:

    <template>
      <ClientOnly>
      	<SsrIncompatibleComp />
      </ClientOnly>
    </template>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如上代码,在服务端渲染时就会忽略该组件,且该组件仅会在客户端被渲染。 组件时利用了 CSR 和 SSR 的差异,如下是 组件的实现:

    import { ref, onMounted, defineComponent } from 'vue';
    
    export const ClientOnly = defineComponent({
      setup(_, { slots }) {
        // 标记变量,仅在客户端渲染时为 true
        const show = ref(false);
        // onMounted 钩子只会在客户端渲染执行
        onMounted(() => {
          show.value = true;
        });
        // 在服务器端什么都不渲染,在客户端才会渲染 ClientOnly 组件的插槽内容
        return () => (show.value && slots.default ? slots.default() : null)
      }
    });
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    整体实现非常简单,原理是利用了 onMounted 钩子函数只会在客户端执行的特性。另外 ClientOnly 组件并不会导致客户端激活失败。因为在客户端激活的时候, mounted 钩子函数还没有触发,所以服务端和客户端渲染的内容一致,即什么都不渲染。等激活完成后,且 mounted 钩子触发执行之后,才会在客户端将 ClientOnly 组件的插槽内容渲染出来。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Monodyee/article/detail/307739
推荐阅读
相关标签
  

闽ICP备14008679号