前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >那就讲讲所谓的vue-ssr(服务端渲染)的来龙去脉吧!

那就讲讲所谓的vue-ssr(服务端渲染)的来龙去脉吧!

作者头像
用户7413032
发布2024-01-09 14:04:18
2800
发布2024-01-09 14:04:18
举报
文章被收录于专栏:佛曰不可说丶佛曰不可说丶

最近单位要为了seo改造 ssr,需要前期做个调研,作为股肱之臣的我(也可能是头号混子)身先士卒接受了这个任务,

毕竟,临近年终,所谓天下熙熙皆为利来,天天嚷嚷皆为利往,写业务不是不能被领导看中升职加薪,而是很难

你想想,大家都写,凭什么你能写出花来。难道是你有什么长处?

所以光写那是不行的,得会写(也就是会舔),

俗话说得好,不怕领导有原则,就怕领导没爱好

领导喜欢什么人啊? 当然是积极的人啊。

那个领导,我最积极了!!!!!!

image.png
image.png

坦率的讲,刚刚接触 ssr 的时候,我是一头雾水,一脸懵逼

这么高大上的词,应该很难吧,然后当我慢慢深入的时候发现,

我被骗了,这么简单的东西,怎么配这么高大上的词呢?

他其实应该叫套模板

不信? 那我就跟大家一起搭一套

什么是 ssr

SSR 的全称是 Server Side Rendering,对应的中文名称是:服务端渲染,也就是将页面的 html 生成工作放在服务端进行。

所谓的 ssr 听起来很唬人,其实,他只是我们在现在的单页面应用时代下发明的时髦的词, 他还有个通俗的名字叫做-套模板,因为在前端旧石器时代,所有的网页都是服务端渲染(套模板)。

区别在于在之前用的是 java、php、jsp、asp、.net 等服务端语言,而现在我们用的是 js 语言。

之前是前端只是切图,后端套模板,而现在 套模板这个操作无聊且简单的操作,前端用一套更先进的技术来实现,这就是 ssr

而在浏览器得到完整的结构后就可直接进行 DOM 的解析、构建、加载资源及后续的渲染。

SSR 优缺点

优点

服务器端渲染的优势就是容易 SEO,首屏加载快,因为客户端接收到的是完整的 HTML 页面

缺点

渲染过程在后端完成,那么肯定会耗费后端资源,所以,基于 node 的服务端渲染,难得不是渲染而是高可用的 node 服务才是麻烦的地方

SSR 与 CSR 的区别

与 SSR 对应的就是 CSR,全称是 Client Side Rendering,也就是客户端渲染。也就是我们现在的单页面应用(spa项目)

它是目前 Web 应用中主流的渲染模式,一般由 Server 端返回初始 HTML 内容,然后再由 JS 去异步加载数据,再完成页面的渲染。

这种模式下服务端只会返回一个页面的框架和 js 脚本资源,而不会返回具体的数据。

CSR(SPA) 优缺点

优点

页面之间的跳转不会刷新整个页面,而是局部刷新,体验上有了很大的提升。同时极大的减轻服务器压力

缺点

SPA 这种客户端渲染的方式在整体体验上有了很大的提升,但是它仍然有缺陷 - 对 SEO 不友好,页面首次加载可能有较长的白屏时间。

SSR VS CSR(SPA)

一图胜千言

image.png
image.png

在之前的内容中,我们毫不费力的分析了关于SSR 以及CSR 的区别以及优缺点,然后,接踵而至的问题就来了,有没有一个完美的方案来兼顾两者的优点呢?摒弃两者的缺点呢?

答案很简单,那就是合体,做个缝合怪

SSR + SPA 完美的结合

只实现 SSR 没什么意义,技术上没有任何改进,否则 SPA 技术就不会出现。

但是单纯的 SPA 又不够完美,所以最好的方案就是这两种技术和体验的结合。

第一次打开页面是服务端渲染,基于第一次访问,用户的后续交互是 SPA 的效果和体验,于此同时还能解决 SEO 问题,这就有点完美了。

于是 vue + node SRR 就出现了,

好了,片汤话讲完,总结起来,就是讲了ssrspa的一些区别和作用,这种类似的话,我相信各位 jym听的耳朵都起茧子了。

坦率的讲,我讲的嘴也起泡了, 因为历史前辈已经讲了一千遍了

但是既然要水文,又似乎不能不讲。

所谓 ssr 的出现,只是最开始没能耐搞不出 spa只能套模板,后来有能耐搞spa了,ssr 的作用只有一个seo,至于什么性能体验装逼高大上、这些不能说不重要,是完全的不重要

吹起牛逼来可以用用,真正的开发,就别扯了,老老实实 spa

总而言之,言而总之,大家就记住一句话即可,自己做能做技术技术决策,如果没有seo要求就老老实实单页面应用

如果自己做不了技术决策的时候,那就听领导的

毕竟领导总是英明的,即使不英明,他也能负责任

image.png
image.png

在开始讲缝合怪vue + node SRR之前,我们为了大家便于理解,先从丘处机路过牛家村开始

常规 SSR

在开始之前,我们先来看看一个常规的 SSR 是怎么实现的,简单的模拟一下史前时代的套模板操作,回顾一下一个前端切图仔的工作流程

代码语言:javascript
复制
问题:怎样实现一个基于 node 的 基础 ssr
  • 创建一个 node 服务
  • 模拟数据请求方法 fetchData
  • 将 fetchData 结果转换为 html 字符串
  • 输出完整的 html 内容

代码如下:

代码语言:javascript
复制
/** @format */

const http = require('http')

//模拟数据的获取
const fetchData = function () {
  return {
    list: [
      {
        name: '包子',
        num: 100,
      },
      {
        name: '饺子',
        num: 2000,
      },
      {
        name: '馒头',
        num: 10,
      },
    ],
  }
}

//数据转换为 html 内容
const dataToHtml = (data) => {
  var html = ''
  data.list.forEach((item) => {
    html += `<div>${item.name}有${item.num}个</div>`
  })

  return html
}

//服务
http
  .createServer((req, res) => {
    res.writeHead(200, {
      'Content-Type': 'text/html',
    })

    const html = dataToHtml(fetchData())

    res.end(`<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>传统 ssr</title>
</head>
<body>
    <div id="root">
       ${html}
    </div>
</body>
</html>
</body>
`)
  })
  .listen(9001)

console.log('server start...9001')

vue-SSR 原理

温习了史前时代的套模板操作之后,我们就该揭秘现在的 SSR 原理。

之前我们说过,现在的 SSR 套路是SSR + SPA 完美的结合,所以他一定需要具备三个特点:

  • 1、必须是同构应用--其实就是前后端一套代码,更容易维护,逻辑也统一
  • 2、首屏需要具备服务端渲染能力,剩余内容需要走 spa --为了更完美的体验
  • 2、必须结合最新技术栈特性比如虚拟 dom --为了更好复用,以及实现同构

在开始之前,我们先得解释一些基础概念

同构应用

::: tip 所谓同构,就是指前后端公用一套代码,也就是我们一个组件在能在前端使用,也能在后端使用 :::

而正是由于 js 语言的特殊性-既能搞前端也能搞后端,所以现代的ssr模式才能被广泛的使用

其实实现同构应用,从本质上来说,就是在服务端生成字符串,在客户端实现 dom,至于用什么技术栈实现并没有限制,我可以用原生 js, 也可以用react,而之所以我选用vue技术栈是因为他具备几个特点:

  • 1、通过虚拟dom这个介质能够更简单的实现同构,渲染组件
  • 2、我熟悉vue技术栈
  • 3、vue官方提供了vue-server-renderer这个库,能够更简单的实现ssr
  • 4、vue来实现可以更高效,写更少的代码,来达到目的

实现更高效的同构应用,我们必须要了解一下虚拟dom

虚拟 dom

::: tip 所谓虚拟 dom,就是一个 js 对象用来描述 dom 元素 :::

比如:

代码语言:javascript
复制
<ul id="list">
  <li class="item">1</li>
  <li class="item">2</li>
  <li class="item">3</li>
</ul>

用虚拟 dom 描述

代码语言:javascript
复制
const tree = {
  tag: 'ul', // 节点标签名
  props: {
    // DOM的属性,用一个对象存储键值对
    id: 'list',
  },
  children: [
    // 该节点的子节点
    { tag: 'li', props: { class: 'item' }, children: ['1'] },
    { tag: 'li', props: { class: 'item' }, children: ['2'] },
    { tag: 'li', props: { class: 'item' }, children: ['3'] },
  ],
}

我们发现虚拟 DOM 除了在渲染时用于提高渲染性能,以最小的代价来更新视图的作用外,其实他还有另一个作用就是为组件的跨平台渲染提供可能。

于是我们就能通跨平台的特性,来更容易的实现同构应用

而我们想到的东西,vue 作者早就想到了,所以他直接在 vue 中内置了,跨平台渲染的能力,也就是vue-server-renderer这个库

vue-server-renderer

vue-server-renderer 说白了就是将 vue 组件变为字符串,并且通过模板引擎将数据注入到字符串中,最后返回一个完整的 html 页面

代码语言:javascript
复制
/** @format */
const http = require('http')
// 此文件运行在 Node.js 服务器上
const { createSSRApp } = require('vue')
// Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下
const { renderToString } = require('vue/server-renderer')
const app = createSSRApp({
  data: () => ({ count: 1 }),
  template: `<button @click="count++">{{ count }}</button>`,
})
http
  .createServer((req, res) => {
    res.writeHead(200, {
      'Content-Type': 'text/html',
    })
    renderToString(app).then((html) => {
      res.end(`<!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>基于vue的ssr</title>
      </head>
      <body>
          <div id="root">
             ${html}
          </div>
      </body>
      </html>
      </body>
      `)
    })
  })
  .listen(9000)

vue-server-renderer

vue-server-renderer 说白了就是将 vue 组件变为字符串,并且通过模板引擎将数据注入到字符串中,最后返回一个完整的 html 页面

代码语言:javascript
复制
/** @format */
const http = require('http')
// 此文件运行在 Node.js 服务器上
const { createSSRApp } = require('vue')
// Vue 的服务端渲染 API 位于 `vue/server-renderer` 路径下
const { renderToString } = require('vue/server-renderer')
const app = createSSRApp({
  data: () => ({ count: 1 }),
  template: `<button @click="count++">{{ count }}</button>`,
})
http
  .createServer((req, res) => {
    res.writeHead(200, {
      'Content-Type': 'text/html',
    })
    renderToString(app).then((html) => {
      res.end(`<!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>基于vue的ssr</title>
      </head>
      <body>
          <div id="root">
             ${html}
          </div>
      </body>
      </html>
      </body>
      `)
    })
  })
  .listen(9000)

输出字符串如图:

image.png
image.png

而他的原理其实就是利用vue中组件初始化之后生成的虚拟dom转换为字符串,我们简单来看下源码

之前我们说过了renderToString 最后的目标就是生成字符串,于是他就可以简单的分为那么几步

  • 1、生成组件vnode(createVNode)
  • 2、初始化以及执行 render 主流程renderComponentVNode
  • 3、创建组件实例createComponentInstance
  • 4、初始化组件执行steup(setupComponent)
  • 5、渲染组件子树 renderComponentSubTree
  • 6、执行组件render函数(ssrRender)
  • 7、获取字符串数组(getBuffer)
  • 8、字符串数组拼接为模板unrollBuffer

到这很多人就有一个疑问,为啥 ssrRender 函数到底是什么结构,他是怎么能得到 buffer 数组的,我们可以看下编译后的代码

image.png
image.png

上图我们可以看出通过 push 函数,最终将模板编译后的render 函数执行,推入 buffer 数组中,进而拼接成模板字符串

与浏览器渲染区别

image.png
image.png

上图中我们可以清楚的看出来客户端主要是调用patch 函数来执行挂载个更新,而在服务端用的是push函数

vue-ssr 搭建

完成了一些概念讲解之后,我们就可以该是着手搭建 ssr 项目了,它至少需要包含两个基本能力

  • 1、 实现同构引用
  • 2、具有友好的开发体验

##目录结构

再开始之前,我们先看东西

image.png
image.png

vue-ssr的搭建核心就是这两个js文件,而这两个文件就是实现同构应用的关键。接下来我们一点点解析

这两个文件,表达的意思其实非常简单,利用 vue内置的能力,在服务端初始化一次vue 实例

代码如下

代码语言:javascript
复制
// 原子组件css 插件
import 'uno.css';
import { renderToString } from 'vue/server-renderer';
import { createApp } from './main';

function renderPreloadLinks(modules, manifest) {
  let links = '';
  const seen = new Set();
  modules.forEach((id) => {
    const files = manifest[id];
    if (files) {
      files.forEach((file) => {
        if (!seen.has(file)) {
          seen.add(file);
          links += renderPreloadLink(file);
        }
      });
    }
  });
  return links;
}

function renderPreloadLink(file) {
  if (file.endsWith('.js')) {
    return `<link rel="modulepreload" crossorigin href="${file}">`;
  } else if (file.endsWith('.css')) {
    return `<link rel="stylesheet" href="${file}">`;
  } else {
    return '';
  }
}

function renderTeleports(teleports) {
  if (!teleports) return '';
  return Object.entries(teleports).reduce((all, [key, value]) => {
    if (key.startsWith('#el-popper-container-')) {
      return `${all}<div id="${key.slice(1)}">${value}</div>`;
    }
    return all;
  }, teleports.body || '');
}
// 初始化vue、render
export async function render(url, manifest) {
  // 拿到实例
  const { app, router, store } = createApp();
  try {
    // 路由跳转,在服务端渲染对应组件模板
    await router.push(url);
    // 确保初始化之后执行
    await router.isReady();
    const ctx = {};
    // 渲染模板
    const html = await renderToString(app, ctx);
    // 处理css模板等内容 内容
    const preloadLinks = renderPreloadLinks(ctx.modules, manifest);
    const teleports = renderTeleports(ctx.teleports);
    //拿到全局数据
    const state = JSON.stringify(store.state.value);
    return [html, state, preloadLinks, teleports];
  } catch (error) {
    console.log(error);
  }
}

``

在客户端实现在初始化一次`vue实例`激活当前`vue`应用

```js
import { createApp } from './main';
import 'uno.css';
import '@/assets/css/index.css';
import 'element-plus/theme-chalk/base.css';
const { app, router, store } = createApp();

router.isReady().then(() => {
  app.mount('#app');
});

实现同构应用

在之前的内容中,我们已讲了什么叫同构应用——也就是一套代码能跑两个端,于是我们就需要迫切的解决两个问题

  • 1、 怎样保证全局状态和路由数据在两端同步
  • 2、 怎样在客户端将页面激活能实现交互

保证全局状态和路由数据在两端同步

我们现在讲第一点,怎样保证全局状态的同步,本质上其实很简单,就是我们在服务端初始化之后,拿到全局状态数据,直接塞到客户端即可

代码如下:

代码语言:javascript
复制
//在服务端
// 在模板中,加入__INITIAL_STATE__ 全局变量
window.__INITIAL_STATE__ = '<pinia-store>'
// 同步state 的值
const state = JSON.stringify(store.state.value)
const html = template.replace(`'<pinia-store>'`, state)
// 在客户端中取出值,直接塞到全局变量中去

if (window.__INITIAL_STATE__) {
  store.state.value = JSON.parse(JSON.stringify(window.__INITIAL_STATE__))
}

而路由的同步,就需要麻烦一点了,因为理论情况下,当我们请求页面的时候,大家都知道,有前端路由也有后端路由

而我们在初始化的过程中,前端路由是不生效的,因为我们需要页面在后端直出,于是我们就需要,在后端获取路由

根据当前的 path 来查找具体的路由,然后根据路由得到具体的组件,然后将组件直出。

代码如下:

代码语言:javascript
复制
// 创建服务匹配所有路由,来拦截初始化所有的路由情况
app.use('*', async (req, res) => {
  // 拿到当前路由路径
  const url = req.originalUrl
  const app = createSSRApp(App)
  // 初始化router
  const router = createRouter()
  app.use(router)
  // 路由跳转,在服务端渲染对应组件模板
  await router.push(url)
  // 确保初始化之后执行
  await router.isReady()
  // 渲染模板
  const html = await renderToString(app, ctx)
})

客户端将页面激活能实现交互

在客户端之所以能实现交互,原理很简单,我们在服务端跑的代码在客户端跑一遍就行了,只是将 dom 挂载这一块不执行即可

原理很简单,但是实现起来却有点麻烦,

首先,我们需要将打包的代码通过模板在客户端运行

然后,为了性能优化,我们只需要拿到当前路由的打包代码以及主流程代码

接着,在打包工具(webpack/vite)的加持下我们只需要更改模板即可

这样一来就能保持客户端和服务端渲染的代码以及路由代码一致

例子:

比如 访问http://localhost/user 链接,他的路由对应的代码应该是

代码语言:javascript
复制
   {
        path: '/user',
        name: 'user',
        component: () => import('@/views/user.vue')
  },

打包后会生成 ssr-manifest 文件,其中包含所有文件打包后的对应的产物

如图:

image.png
image.png

然后再 serve端 初始化中将匹配到的文件塞入模板中

如图:

image.png
image.png

如此一来,就是一个完整的还未激活的ssr流程

而之所以需要ssr-manifest 来进行匹配,就是为了保持两端一致,当已经激活后,路由懒加载的内容,不会被在初始化的时候加载出来,从而在保证性能的同时,有兼顾体验

客户端激活

客户端激活我们之前也说过,其实就是给服务端的代码在跑一遍

代码如下:

代码语言:javascript
复制
// 初始化vue实例
const app = createSSRApp(App)
// 初始化pinia
const store = createPinia()
// 初始化router
const router = createRouter()
app.use(store).use(router)

// 同步state 的值
if (window.__INITIAL_STATE__) {
  store.state.value = JSON.parse(JSON.stringify(window.__INITIAL_STATE__))
}
// router初始化完成 挂载
router.isReady().then(() => {
  app.mount('#app')
})

以上代码中,我们需要注意的是,初始化vue 实例需要createSSRApp 函数,而不是createApp 原因很简单,我已经有dom了,不需要在生成了,只需要根据在已有 dom 上绑定事件即可

我们来简单看一下执行流程

  • 1、 初始化 vue 实例createSSRApp,确定渲染函数hydrate
  • 2、 mount 函数执行挂载进而执行hydrate函数开启激活流程
  • 3、 初始化组件mountComponent
  • 4、 初始化setup(setupComponent)
  • 5、 建立模板的响应式关系setupRenderEffect
  • 6、 执行当前模板编译后的render函数激活页面hydrateSubTree
  • 7、 启动类似patch函数开启事件绑定等流程hydrateNode
  • 8、 hydrateNode 函数递归,直到所有节点绑定完成页面激活成功

最后

ok,一个简单的 vue-ssr项目就这么搭建完成了,如果你觉得不太明白,或者不太理解

没关系,我将所有的源码也传到了git 上, 请细品!!!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2024-01-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 什么是 ssr
    • SSR 优缺点
      • 优点
      • 缺点
    • SSR 与 CSR 的区别
      • CSR(SPA) 优缺点
        • 优点
        • 缺点
      • SSR VS CSR(SPA)
        • SSR + SPA 完美的结合
      • 常规 SSR
        • vue-SSR 原理
          • 同构应用
          • 虚拟 dom
          • vue-server-renderer
          • vue-server-renderer
          • 与浏览器渲染区别
      • vue-ssr 搭建
        • 实现同构应用
          • 保证全局状态和路由数据在两端同步
          • 客户端将页面激活能实现交互
          • 客户端激活
      • 最后
      相关产品与服务
      对象存储
      对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档