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

前言

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

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

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

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

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

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

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

坦率的讲,刚刚接触 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)

一图胜千言

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

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

SSR + SPA 完美的结合

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

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

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

于是 vue + node SRR 就出现了,

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

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

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

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

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

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

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

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

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

常规 SSR

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

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

代码如下:

js 复制代码
/** @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 元素 :::

比如:

html 复制代码
<ul id="list">
  <li class="item">1</li>
  <li class="item">2</li>
  <li class="item">3</li>
</ul>

用虚拟 dom 描述

js 复制代码
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 页面

js 复制代码
/** @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 页面

js 复制代码
/** @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中组件初始化之后生成的虚拟dom转换为字符串,我们简单来看下源码

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

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

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

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

与浏览器渲染区别

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

vue-ssr 搭建

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

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

##目录结构

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

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

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

代码如下

js 复制代码
// 原子组件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、 怎样在客户端将页面激活能实现交互

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

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

代码如下:

js 复制代码
//在服务端
// 在模板中,加入__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 来查找具体的路由,然后根据路由得到具体的组件,然后将组件直出。

代码如下:

js 复制代码
// 创建服务匹配所有路由,来拦截初始化所有的路由情况
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 链接,他的路由对应的代码应该是

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

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

如图:

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

如图:

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

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

客户端激活

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

代码如下:

js 复制代码
// 初始化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 上, 请细品!!!

地址

相关推荐
y先森33 分钟前
CSS3中的伸缩盒模型(弹性盒子、弹性布局)之伸缩容器、伸缩项目、主轴方向、主轴换行方式、复合属性flex-flow
前端·css·css3
前端Hardy33 分钟前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu108301891136 分钟前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
IT女孩儿2 小时前
CSS查缺补漏(补充上一条)
前端·css
吃杠碰小鸡3 小时前
commitlint校验git提交信息
前端
天天进步20153 小时前
Vue+Springboot用Websocket实现协同编辑
vue.js·spring boot·websocket
虾球xz3 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇3 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒3 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员4 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js