✨
blog
  • Blog
  • Element-UI
    • 2019-09-04
  • JS
    • ES6 之 Set 和 Map
    • let 和 const 声明常见概念
    • 元编程
    • ES6之字符串的扩展
    • ES6 之异步流程的前世今生(上)
    • ES6之异步流程的前世今生(下)
    • ES6 之模块你知吗
    • ES6 之解构赋值与箭头函数的妙用
    • 迭代器
    • ES5 之原型(一)
    • ES6之类(二)
    • es7之装饰器
    • es6之数组详解
    • js之this指向
    • 对象
    • vue项目配合使用canvas联动
    • 本文解决痛点:对象里面是否有值
  • MAC
    • vue源码之method
    • Mac的使用技巧
    • 前文
    • Mac常用软件(二)
    • 如何查看 Mac 端口号以及占用情况
  • Node
    • Node之Buffer详解
    • 浏览器与 node 的事件循环(event loop)有何区别
    • Node之多线程
    • node之模块解析(一)
    • 错误捕获与内存告警
  • TS
    • Record
    • 使用方法
    • 工具泛型
    • 类型体操
    • 泛型
  • chrome
    • v8 引擎
    • v8 垃圾回收机制
    • 浏览器的知识
  • flutter
    • 路由
    • 页面布局
  • go
    • index
  • html&css
    • 两栏布局
    • ES5和ES6的区别
    • ES5 和 ES6 的区别
    • HTTP详解
    • TCP 与 UDP 的区别
    • MDN
    • css modules 使用教程
    • css 居中
    • 拖拽
    • flex布局
    • h5 新增特性 html5
    • history 与 hash 路由策略
    • position 定位方式
    • rem布局
    • svg
    • web性能优化
    • 事件循环
    • 从输入网址后发生了什么
    • 前端状态管理
    • 圣杯布局与双飞翼
    • 性能优化 页面的性能统计指标
    • 本地存储的几种对比
    • 浏览器的渲染进程
    • 浏览器缓存策略详解
    • 盒模型
    • 为什么要移动端适配
    • 跨域的 N 种实现方式
  • web3
    • 常见概念
    • vue项目配合使用canvas联动
  • webgl
    • Mac使用技巧(二)
    • Node之模块解析path
  • 代码库
    • documeng的一些常见操作
    • eventBus事件
    • jquery提交
    • jquery的一些常见操作
    • 常见操作
    • 数组polyfill
    • TS代码片段
      • 面试官眼中的test unit
  • 全年安排
    • AfterShip
    • 大企业
  • 函数编程题
    • Promise问题
    • 继承
  • 前端早早聊
    • vue生态
    • 开发一款VScode语言插件
    • 简历回顾和进行复盘
    • 重新认知性能优化及其度量方法
    • 2022-09-17-音视频专场.md
      • 2022-09-17-音视频专场
    • 前端晋升专场
      • 成长的诀窍是靠自己
      • 销销帮
    • 前端监控专场
      • 字节前端监控实践
      • 李港:大前端-从无到有搭建自研前端监控系统
    • 前端跳槽
      • 50个面试官在线招聘
      • 如何识别优秀的前端猎头来跳槽大厂
      • 面试套路
    • 支付宝
      • 面试
    • 管理专场
      • 芋头:管理者眼中的web技术发展前沿
    • 组件专场
      • 基于webCompents的跨技术组件库实践
    • 面试
      • 面试辅导问题
      • 早早聊面试
      • 前端沙箱是什么? 怎么实现沙箱环境?
  • 常见总结
    • 2018年终总结-年底了,你总结了吗?我先来
    • 在逆境中成长
    • 2021年终总结
    • 2024年全年总结
    • 项目
    • Tell2.0 前端复盘
    • 复盘
    • 前端工程师素养
    • 学习方法论
    • 希望与破晓| 2022 年终总结
    • 新起点, 新征途 | 掘金年度征文
    • 稳定| 2023 年终总结
    • 趁着有风快飞翔 | 2019 年终总结
    • AfterShip
      • Emotion:用 JavaScript 编写 CSS 的强大工具
      • 个人中长期目标
      • 事故复盘
      • 时间解析
      • 国内外区别
      • 独立站建设
    • MEIZU
      • NativeApp与H5通信原理
      • SSR 原理
      • SSR的常见问题
      • CLI
      • electron 应用发布流程
      • electron
      • electron 面试
      • 数据结构与算法之美
      • mgc 一期复盘
      • 架构原理
      • 喵币管理
      • 三期复盘总结
      • 异常监控之 sentry 实践
      • 微前端
      • qiankun 原理解析
      • 快游戏一期
      • 游戏中心复盘
    • 个人准则
      • index
    • 编程猫
      • pc 接入 micro bit 方案
      • prompt engineer
      • web work 跨域解析与解决方式
      • web 中的 ai
      • 低版本 node 环境下 ffmpeg 的使用
      • 关于 taobao 源 https 过期
      • 加密 json
      • 安卓 5 和 6 的白屏解决
      • 性能排查与优化实践
      • 探月接入
      • 接入硬件
      • 新生态下的state
      • monorepo 包管理方式
      • 自修复 npm 库
      • 音频的绘制
    • 谨启
      • 音视频
      • 小程序
        • taro 规范
        • 结合 mobx 在跳转前预请求
        • Taro 浅析用法与原理
        • 前文
        • 小程序优化指南
        • 小程序内部实现原理
        • 支付相关
    • tencent
      • TAPD
        • MathJax的食用
        • canvas渲染优化策略
        • 为什么 JavaScript 是单线程的呢?
        • svg 总是不对
        • 前端库
        • 原生端和js端如何通信
        • 在旧项目中复用vue代码
        • 提升自我
        • 批量编辑优化
        • 插入业务对象
        • 编辑器
        • 挂载点
        • 性能优化对比
        • 遇到的问题
        • 项目迁移公告
        • 领导力
      • 行家
        • 实战篇
        • 职业发展、领导力、个人成长
        • 高质量沟通
  • 慕课网
    • react-native原理
    • react-native学习
  • 杂文
    • Dom 节点变动检测并录制的简单实现
    • 错误监控&错误捕获
    • NextJS与NuxtJS
    • 负载均衡的几种常用方式
    • PM2
    • service worker 控制网络请求?
    • SSL 和 TLS 的区别
    • Babel 你太美
    • echart踩坑经验
    • keyup、keydown你都知道有什么区别吗
    • 常见概念
    • 首屏加载优化与性能指标分析
    • preload 和 prefetch 的详解
    • 在项目中配置这几个关系
    • roullp 解析
    • tinymce原理浅析
    • wasm 在前端的应用
    • websocket
    • webworker
    • 项目
    • 从 ajax 到 axios
    • 从postcss 到自己开发一款插件
    • 从输入浏览器到页面展示涉及的缓存机制
    • 代码整洁之道
    • 你知道什么是aop吗
    • 函数式编程
    • 函数式编程指南
    • 前端input框文字最大值
    • 攻坚战
    • 前端书写 sdk
    • 前端文字转语音播放
    • 前端领域的 Docker 和 Kubernetes
    • 前端安全
    • 前端进阶之内存空间
    • 前端音频浅析
    • 十分钟搞定多人协作开发
    • 字符串的比较
    • 尾递归
    • 前文
    • 常见的算法可以分为以下三类
    • 手机调试--mac篇
    • 数组的原生系列
    • COOP 和 COEP - 新的跨域策略
    • 浅谈react组件书写
    • 浏览器与 Node.js 事件循环的区别
    • 由三道题引伸出来的思考
    • 移动端300ms点击延迟
    • 移动端和pc端事件
    • Git 常见疑惑
    • 我们离发 npm 包还有多远
    • 重绘和重排
    • AI 时代下的前端编程范式
    • 音频可视化实战
  • 极客时间
    • Serverless入门课
    • 二分查找
    • 二叉树
    • 全栈工程师
    • 动态规划面试宝典
    • 前端与rust
    • 散列表
    • 前端方面的Docker和Kubernetes
    • 栈
    • 深入浅出区块链
    • 玩转 vue 全家桶
    • 玩转 webpack
    • 程序员的个人财富课
    • 算法
    • 说透元宇宙
    • 跳表
    • 链表
    • 10x 程序员工作法
      • index
    • Node开发实战
      • HTTP服务的性能测试
      • JavaScript语言精髓与编程实战
      • 什么是node。js
      • svg精髓
    • ReactHooks核心原理与实战
      • ReactHooks核心原理与实战
    • Rust
      • Rust编程第一课
      • 前置篇
      • 深度思维
      • 重构
      • 类型体操
      • 基础知识
    • WebAssembly入门课.md
      • 基础篇
      • SSR的注水和脱水
      • jsBriage通信原理
      • 基础知识篇
    • 互联网的英语私教课
      • 互联网人的英语私教课
    • 代码之丑
      • 代码之丑
    • 前端全链路优化实战课
      • 网页指标
    • 图解 Google V8
      • 图解 Google V8
    • 浏览器工作原理与实践
      • 浏览器工作原理与实践
    • 算法面试通关 40 讲
      • 算法面试通关40讲
    • 跟月影学可视化
      • index
    • 软件设计之美
      • 软件设计之美
    • 重学前端
      • js
  • 后续的文件增加都会增加到上面并以编号对应
    • 1029. 两地调度
    • 151.翻转字符串里的单词
    • 2022.3.15
    • 前端数据结构
    • 前端常见算法
    • 前端常见排序
    • 恢复一棵树
  • 设计模式
    • 前端常见设计模式之MVC与MVVM
    • 前端之代理模式
    • 前端常见设计模式之单例模式
    • 前端常见设计模式之发布订阅模式
    • 前端之工厂模式
    • 观察者模式
    • 前端常见设计模式之适配器模式
  • 译文
    • [译] 如何使用CircleCI for GitHub Pages持续部署
    • 您是否优化了 API 的性能
    • [译][官方] Google 正式发布 Flutter 1.2 版本
    • 什么是 Deno ,它将取代 NodeJS ?
  • 读后感
    • JavaScript二十年
    • 1368个单词就够了
    • js编程精解
    • labuladong 的算法小抄
    • lodash常用方法
    • vue的设计与实现
    • 所有的静态资源都是get请求
    • 人生
    • 人生护城河
    • 你不知道的JavaScript
    • 前端核心知识进阶
    • 华为工作法
    • 反脆弱
    • 好好学习
    • 左耳听风
    • 摩托车维修之道
    • 数学之美
    • 深入理解svg
    • 浏览器的ESM到底是啥
    • 经济学原理
    • 编程珠玑
    • 防御式 css 精讲
    • 韭菜的自我修养
  • 雪狼
    • 2022-07-17
    • 基础知识
    • 阶一课程
      • 实战辅导一
      • 实战辅导二
  • 嵌入式
    • 树莓派
      • 排序
  • 源码
    • React
      • 核心知识点
      • errorBoundaries
      • immutable.js 的实现原理
      • React.Suspense
      • react源码分析之Fiber
      • batchedUpdate
      • Component
      • Context
      • react 源码分析之 diff 算法
      • React 中的 key 属性:原理、使用场景与注意事项
      • 使用方式
      • react源码分析之memo
      • react 源码分析之mixin
      • 实战篇
      • react源码分析之react-dom
      • 使用方式
      • scheduleWork
      • useImperativeHandle的使用与原理
      • React 书写小技巧
      • 入口和优化
      • 合成事件和原生事件的区别
      • react 性能优化
      • 构建一个 hooks
      • 浅析 styled-components
      • 生命周期
      • 组合 vs 继承
      • 通信机制
      • 高阶组件
      • 慕课网
        • 应用篇
        • 课程导学
    • ReactHook
      • useCallback
      • useContext
      • useEffect 与 useLayoutEffect
      • useHook
      • useMemo
      • useReducer
      • 原理
      • useState
      • 总结
    • Redux
      • mobx 原理解析
      • redux-saga
      • redux-thunk
      • Mobx 和 Redux 对比
      • 使用方法
      • redux 原理
    • Vite
      • Vite原理
      • Vite配置
      • 热更新原理
      • vite 为什么生产环境用 Rollup
    • Webpack
      • PostCSS
      • Webpack5 核心原理与应用实践-loader
      • Webpack5 核心原理与应用实践-plugin
      • Webpack5 核心原理与应用实践
      • 区分
      • 升级详情
      • treeShaking(树摇Tree Shaking)
      • 编写一个自己的webpack插件plugin
      • 代码分离(code-splitting)
      • webpack 打包优化
      • 基础配置
      • webpack 打包优化
      • webpack 工作原理
      • webpack 按需加载原理
      • webpack 热更新 HMR(Hot Module Replacement)
      • 缓存
      • webpack 自定义 plugin
    • next
      • tailwind
      • 什么是水合
    • sveltejs
      • index
    • tinymce
      • 并发篇
    • 源码手写系列
      • create
      • call
      • bind
      • call
      • es6 单例
      • forEach vs Map
      • instanceOf
      • new
      • reduce
      • 取两个重复数组的交集
      • 函数柯理化
      • 动态规划
      • 基于Generator函数实现async
      • 新建 js 文件
      • 手写一个 slice 方法
      • 手写一个 webpack loader
      • Plugin
      • 手写一个寄生组合式继承
      • 二叉树
      • 链表相关的操作
      • 手动实现发布订阅
      • 数组去重
      • 数组扁平化
      • 数组
      • 构造大顶堆和小顶堆
      • 深浅拷贝 深拷贝
      • 两者对比
    • vue
      • vue2
        • vm.attrs与$listeners
        • vue 和 react 的 diff 算法比较
        • vue 源码分析
        • vue 优化的 diff 策略
        • extends
        • 核心原理篇
        • keep-alive
        • vue 源码分析之 mixins
        • vue 源码分析之 nextTick
        • vue之slot
        • vnode
        • vue 源码分析之 watch
        • 原理
        • vue 源码分析之transition
        • vue 源码分析之异步组件
        • 调用的是 watch
        • 安装
        • react源码分析之portals
        • event 的实现原理(事件的实现原理)
        • 什么是h
        • 分析provide 和 inject
        • vue 源码分析之 use
        • v-model
        • vue源码分析之vuex
        • 响应式原理
        • 初始化的流程
        • 组件更新
        • 编译
        • 父子组件生命周期
        • 原理
        • 多实例
        • Vue 面试
        • 源码研读一
        • 响应式原理
        • 常见问题
        • 数组的劫持
        • vue之自定义指令
        • 运行机制全局概览
      • vue3相比vue2的提升点
        • vue composition api
        • vue3的虚拟dom优化
        • vue3层面的双向数据绑定
        • 预处理优化
  • 重构
    • notification
      • 讲解
  • 面试
    • AfterShip经历
      • JS对URL进行编码和解码
      • ShippingLabelTemplate
      • 接入keycloak详解
      • reCAPTCHA接入
      • yalc与动态解决升级的依赖包
      • RBAC 简介
      • 多语言计划
      • 接入Google登录及其主动弹出快捷登录方式
      • 读书计划
        • 传染
        • 这就是OKR
    • 编程猫经历
      • 2024.1.16
      • 2025.2.20
      • 2025.2.21
      • 2025.2.26
      • 2025.3.28
      • 2025.3.3
      • 2025.3.7
      • 行动轨迹
      • 面试主观题
    • 腾讯经历
      • 2022.02.21
      • 2022.03.30
      • 2022.04.24
      • 2022.04.25
      • 2022.04.27
      • 2022.04.28
      • 2022.04.29
      • 2022.05.05
      • 不同公司的面试关注点不同
      • 2022.05.07
      • 2022.05.09
      • 2022.05.10
      • 2022.05.11
      • 2022.05.12
      • 2022.05.13
      • 2022.05.16
      • 2022.05.17
      • 2022.05.19
      • 2022.05.27
      • 面试
      • 行动轨迹
      • 面试主观题
    • 针对字节
      • 2022.05.14
      • 2022.05.17
      • HR面试准备
      • Promise的相关题目
      • React 进阶实践指南(二)
      • React 面试准备
      • vue 与 react 有什么不同 (react 和 vue 有什么区别)
      • TypeScript 全面进阶指南
      • cookie和session区别
      • express 面试准备 koa 中间件原理
      • next面试准备
      • requestCallBack
      • interface 与 type 异同点
      • 取消 promise
      • 如何设计一个前端项目
      • 进阶篇
      • 早早聊面试准备
      • 自动化部署
      • 挖掘项目的深度
      • 面试
      • 出题指数
    • 魅族经历
      • 2020.09.11
      • 一灯
      • 一灯
      • 一灯
      • 2020.09.20
      • 2020.09.21
      • 网易二面
      • 2020.09.23
      • 头条
      • 360 金融面试题
      • 富途一面
      • 算法
      • 字节
      • 2020.11.04
      • baidu 一面
      • meta 标签的作用
      • 字节
      • 2020.11.22
      • 2020.11.25
      • 微前端接入笔记
      • 面试的基本原则
由 GitBook 提供支持
在本页
  • SSR 的原理
  • SSR 的 Cookies 问题
  • 常见的性能问题
  • 访问特定平台(Platform-Specific) API
  • 自定义指令
  • 避免状态单例
  • 在服务端请求 ssr 首屏数据
  • 注水
  • cookie 透传
  • 同时支持客户端渲染和服务端渲染
  • cookie 注入
  • 处理高并发 和 容易挂的问题
  • SSR 的常见优化
  • 应用代码层面
  • SSR 的注水和脱水

这有帮助吗?

  1. 常见总结
  2. MEIZU

SSR的常见问题

SSR 的原理

vue-server-renderer:应用初始化和应用输出

SSR 的 Cookies 问题

常见的性能问题

vue.mixin、axios 拦截请求使用不当,会内存泄露。 lru-cache 向内存缓存数据, 需要合理混存改动不频繁的资源。

ssr 的局限 服务端压力较大

开发条件受限 在服务端渲染中,created 和 beforeCreated 之外的生命周期钩子不可用,因此项目引用的第三方的库也不用其他生命周期钩子,这对引用库的选择产生了很大的限制。 一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。 你应该避免在 beforeCreate 和 created 生命周期时产生全局副作用的代码,例如在其中使用 setInterval 设置 timer。在纯客户端 (client-side only) 的代码中,我们可以设置一个 timer,然后在 beforeDestroy 或 destroyed 生命周期时将其销毁。为了避免这种情况,请将副作用代码移动到 beforeMount 或 mounted 生命周期中。

访问特定平台(Platform-Specific) API

自定义指令

大多数自定义指令直接操作 DOM,因此会在服务器端渲染 (SSR) 过程中导致错误。有两种方法可以解决这个问题:

推荐使用组件作为抽象机制,并运行在「虚拟 DOM 层级(Virtual-DOM level)」(例如,使用渲染函数(render function))。

如果你有一个自定义指令,但是不是很容易替换为组件,则可以在创建服务器 renderer 时,使用 directives 选项所提供"服务器端版本(server-side version)"。

学习成本相对较高 除了对 webpack、vue 要熟悉,还需要掌握 node、express 相关技术。

避免状态单例

当编写纯客户端 (client-only) 代码时,我们习惯于每次在新的上下文中对代码进行取值。但是,Node.js 服务器是一个长期运行的进程。当我们的代码进入该进程时,它将进行一次取值并留存在内存中。这意味着如果创建一个单例对象,它将在每个传入的请求之间共享。

每个请求创建一个新的根 Vue 实例.

应用程序的代码分割或惰性加载,有助于减少浏览器在初始渲染中下载的资源体积,可以极大地改善大体积 bundle 的可交互时间(TTI - time-to-interactive)

Simple fix is adding a flag on Vue to make sure you only apply the mixin once.

在服务端请求 ssr 首屏数据

最合适的方式是通过 Vuex 的 Store, 在 entry-server.js

// 对所有匹配的路由组件调用 `asyncData()`
Promise.all(
  matchedComponents.map((Component) => {
    if (Component.asyncData) {
      return Component.asyncData({
        store,
        route: router.currentRoute,
      });
    }
  })
)
  .then(() => {
    // 在所有预取钩子(preFetch hook) resolve 后,
    // 我们的 store 现在已经填充入渲染应用程序所需的状态。
    // 当我们将状态附加到上下文,
    // 并且 `template` 选项用于 renderer 时,
    // 状态将自动序列化为 `window.__INITIAL_STATE__`,并注入 HTML。
    context.state = store.state;

    resolve(app);
  })
  .catch((err) => {
    console.error(err);
    reject(err);
  });

同时给首屏的第一个路由组件添加 asyncData 方法来请求数据,注意是组件的静态方法,而非在 methods 中定义的方法.

export default {
  name: 'wecircle',
  ...
  asyncData ({ store }) {
    // 触发 action 后,会返回 Promise
    return store.dispatch('setWecircleDataListSSR')
  },
  ...
}

注水

后面的 action 和 mutation 按照正常逻辑写即可, 最后, 当 ssr 数据渲染完成后,会在生成的 html 中添加一个 window.INITIAL_STATE 对象, 修改 entry-client.js 可以将数据直接赋值给客户端渲染.

entry-client.js: 客户端 entry 只需创建应用程序, 并且将其挂载到 DOM, 然后将 Store 状态同步给客户端 bundle:

const { app, router, store } = createApp();

// 同步store

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__);
}

router.onReady(() => {
  router.beforeResolve((to, from, next) => {
    const matched = router.getMatchedComponents(to);
    const prevMatched = router.getMatchedComponents(from);
    let diffed = false;
    const activated = matched.filter((c, i) => {
      return diffed || (diffed = prevMatched[i] !== c);
    });

    const asyncDataHooks = activated.map((c) => c.asyncData).filter((_) => _);
    if (!asyncDataHooks.length) {
      return next();
    }

    Promise.all(asyncDataHooks.map((hook) => hook({ store, route: to })))
      .then(() => {
        console.log("client entry asyncData function emit");
        next();
      })
      .catch(next);
  });

  app.$mount("#app");
});

服务端

entry-server.js: 服务端入口需要处理路由, 并触发数据预取逻辑

import { createApp } from '../main';

export default context => {
  return new Promise((resolve, reject) => {
    let { app, router, store } = createApp();

    router.push(context.url);

	  router.onReady(() => {
      const matchedComponents = router.getMatchedComponents();

      // 对所有匹配的路由组件调用 `asyncData()`
      Promise.all(
        matchedComponents.map(Component => {
          if (Component.asyncData) {
            return Component.asyncData({ store, route: router.currentRoute });
          }
        })
      ).then(() => {
          context.state = store.state;
          resolve(app);
        })
        .catch(reject);
    }, reject);
	})
}

cookie 透传

当在 ssr 端请求数据时, 需要带上浏览器的 cookie, 在客户端到 ssr 服务器的请求中, 客户端是携带有 cookie 数据的,但是在 ssr 服务器请求后端接口的过程中, 相应的 cookie 数据的, 在 ssr 服务器进行接口请求的时候,我们需要手动那倒客户端的 cookie 传给后端服务器.

我们有个场景就是 需要在请求数据时, 带上 immei 进行登录, 而客户端到 ssr 服务器的请求中, 客户端是携带有 cookie 数据的. 但是在 ssr 服务器 请求后端接口的过程中, 却是没有相应的 immei 数据的, 因此在 ssr 服务器进行接口请求的时候, 我们需要手动拿到客户端的 immei 传给后端服务器.

在 Server.js 中获取浏览器 cookie, 并利用 window 对象存储

app.use('*', (req, res) => {
  ...
  window.ssr_cookie = req.cookie
  ...
})
在 axios 中, 添加 header 将 cookie 塞进去

axios.create({
  headers: window.ssr_cookie || {}
})

同时支持客户端渲染和服务端渲染

ssr 服务端渲染挂掉的时候, 需要有容错逻辑保证页面可用, 原先的客户端渲染相关的构建要保留, 即通过直接访问 inde.html 的方式能够正常使用页面, 这里通过 nginx 配置路径转发.

location /index.html {
     return 301 https://$server_name/;
}
 # 客户端渲染服务
  location / {
     # 给静态文件添加缓存
     location ~ .*\.(js|css|png|jpeg)(.*) {
          valid_referers *.nihaoshijie.com.cn;
          if ($invalid_referer) {
            return 404;
          }
          proxy_pass http://localhost:8080;
          expires  3d;# 3天
      }
      proxy_pass http://localhost:8080; # 静态资源走8080端口
  }

  # ssr服务
  location  = /index_ssr {
     proxy_pass http://localhost:8888; # ssr服务使用8888端口
  }

只保留/index_ssr 作为 ssr 渲染的入口, 然后在 server.js 中, 将/index_ssr 处理成首页的路径, 并添加对 ssr 渲染的容错逻辑.

  if (req.originalUrl === '/index_ssr' || req.originalUrl === '/index_ssr/') {
    context.url = '/'
  }
  ...
  renderer(bundle, manifest).renderToString(context, (err, html) => {
    ...
    if (err) {
      // 发现报错,直接走客户端渲染
      res.redirect('/')
      // 记录错误信息 这部分内容可以上传到日志平台 便于统计
      console.error(`error during render : ${req.url}`)
      console.error(err)
    }
    ...
  })

遇坑 1:vue 组件名尽量不要和路由重名,名字一样大小写不一样也不可(例如 组件叫 component,而引用这个组建的路由叫/Component)。如果重名了,会出现路由找不到的情况

遇坑 2: 一定要遵守标签的嵌套规则,尤其是不要单独使用 tag="li"属性,嵌套规则的不一致会造成 client 和 server 两端的 dom 树不一致,导致本地开发没问题而打包上线有问题

cookie 注入

将 Cookies 注入到 global. 在将 cookies 注入到组件的 asyncData 方法.

一套代码两套执行环境

(1)在 beforeCreate,created 生命周期以及全局的执行环境中调用特定的 api 前需要判断执行环境;

(2)使用 adapter 模式,写一套 adapter 兼容不同环境的 api。

 // 在路由组件内
 <template>
  <div>{{ fooCount }}</div>
 </template>
 <script>
 // 在这里导入模块,而不是在 `store/index.js` 中
 import fooStoreModule from '../store/modules/foo'
 export default {
  // 数据预获取生命周期,在服务端运行
  asyncData ({ store }) {
    //惰性注册store模块
    store.registerModule('foo', fooStoreModule)
    //执行foo命名空间下名为inc的action操作
    return store.dispatch('foo/inc')
  },
  // 重要信息:当多次访问路由时,
  // 避免在客户端重复注册模块。
  destroyed () {
    this.$store.unregisterModule('foo')
  },
  computed: {
    fooCount () {
      //获取store数据
      return this.$store.state.foo.count
    }
  }
 }
 </script>

因为 hash 模式的路由提交不到服务器上,因此 ssr 的路由需要采用 history 的方式。

异常处理问题 1.异常来自哪里? (1)服务端数据预获取过程中的异常,如接口请求的各种异常,获取到数据后对数据进行操作的过程中出现的错误异常。

(2)在服务端数据预获取的生命周期结束后的渲染页面过程中出现的异常,包括各种操作数据的语法错误等,如对 undefined 取属性。

2.怎么处理异常

(1)官方处理方法

抛出 500 错误页面,体验不友好,产品不接受。

(2)目前采用的方法

a.服务端数据预获取过程中出现的异常,让页面继续渲染,不抛出 500 异常页面,打错误日志,接入监控。同时,在页面加入标志,让前端页面再次进行一次数据获取页面渲染的尝试。

b.页面渲染过程的异常。由于目前渲染过程是 vue 提供的一个插件进行的,异常不好捕获,同时出现问题的概率不是很大,因此还没有做专门的处理。

entry-server.js 服务端部分:

 Promise.all(matchedComponents.map(component => {
    //代码略,参见官方文档
 })).then(() => {
    //代码略,参见官方文档
 }).catch(err => {
    //官方代码在这里直接抛出异常,从而走500错误页面
    //我们做如下处理,首先打印错误日志,将日志加入监控报警,监控异常
    console.log('rendererror','entry-server',err);
    // 其次,增加服务端预渲染错误标识,前端拿到标志后重新渲染
    context.serverError = true;
    //最后,将服务端vue实例正常返回,避免抛500
    resolve(app)
 })

性能

(1)页面级别的缓存 将渲染完成的页面缓存到内存中,同时设置最大缓存数量和缓存时间。 优势:大幅度提高页面的访问速度 代价:增加服务器内存的使用

const LRU = require("lru-cache"); //删除最近最少使用条目的缓存对象
// 实例化配置缓存对象
const microCache = LRU({
  max: 100, //最大存储100条
  maxAge: 1000, // 存储在 1 秒后过期
});
//http请求处理
server.get("*", (req, res) => {
  //根据url获取缓存页面
  const hit = microCache.get(req.url);
  //如果有缓存则直接返回缓存数据
  if (hit) {
    return res.end(hit);
  }
  renderer.renderToString((err, html) => {
    res.end(html);
    //将页面缓存到缓存对象中
    microCache.set(req.url, html);
  });
});

处理高并发 和 容易挂的问题

SSR 的常见优化

常见的拆解方式是用户从浏览器发起的请求阶段、服务端渲染阶段和响应阶段

  • 请求已经到到达服务还未执行渲染

  • 开始渲染计算,直到渲染完成

  • 服务器处理响应

渲染前: 缓存: 数据、组件、页面 请求:http、keep-alive 降级: 客户端渲染

在不考虑自研 CDN 的情况下,开启 CDN 缓存的步骤非常简单:

域名接入 CDN 服务,同时针对路径启用缓存 在源站设置 Cache-Control 响应头,为了更灵活地控制缓存规则,但并不是必须

什么服务可以开启 CDN 缓存

  • 无用户状态

  • 低时效性

从下面几个方面

  1. 缓存时间 提高 Cache-Control 的时间是最有效的措施,缓存持续时间越久,缓存失效的机会越少。Cache-Control 只能告知 CDN 该缓存的时间上限,并不影响它被 CDN 提早淘汰。流量过低的资源,很快会被清理掉,CDN 用逐级沉淀的缓存机制保护自己的资源不被浪费。

  2. 忽略 url 参数 页面的参数明显不符合预期, 例如微信等渠道分享后,末尾被挂上各种渠道自身设置的统计参数。 平均到单个资源的访问量就会大大降低,进而降低了缓存效果。

  3. 主动缓存 化被动为主动,才有可能实现 100% 的缓存命中率。常用的主动缓存是资源预热,更适合 URL 路径明确的静态文件,动态路由无法交给 CDN 智能预热,除非依次推送具体的地址。

应用代码层面

  1. 掌控缓存

app.use((ctx, next) => {
  if (["/foo", "/foo/"].includes(ctx.patch)) {
    ctx.set("Cache-Control", "max-age=300");
  }
});

客户端初始化: 获取 immei 拿到相关的逻辑

SSR 的注水和脱水

在服务端渲染(SSR,Server-Side Rendering)中,“注水”(Hydration)和“脱水”(Dehydration)是两个核心概念,用于协调服务端和客户端的渲染过程,确保页面既有快速的首屏呈现,又能支持客户端的交互性。以下我会详细讲解这两个概念的原理、作用和实现方式,并结合 Next.js 等框架中的具体应用,帮助你深入理解 SSR 的“注水”和“脱水”。

  1. 基本概念

(1) 脱水(Dehydration) 定义:服务端将渲染后的 HTML 和初始数据“脱水”成静态内容和序列化的状态,发送给客户端。 时机:发生在服务端渲染过程中。 目的:将服务端计算的状态(如 API 数据、组件状态)序列化为客户端可用的格式(如 JSON),减少客户端重复请求。

(2) 注水(Hydration) 定义:客户端接收服务端发送的 HTML 和状态数据,通过 JavaScript “注水”激活页面,使其变为可交互的动态应用。 时机:发生在客户端加载后。 目的:将静态 HTML 与前端框架(如 React、Vue)的运行时绑定,恢复事件监听和动态行为。

形象比喻

脱水:服务端把“湿漉漉”的动态页面烘干成静态 HTML 和数据“干货”。 注水:客户端把“干货”重新加水,变成“活”的应用。

  1. 工作原理 服务端(脱水)

  • 渲染 HTML: 服务端执行组件树,生成初始 HTML。 例如,React 使用 renderToString 将组件转为 HTML 字符串。

  • 序列化状态: 将动态数据(如 API 返回的 JSON)序列化为字符串。 通常嵌入到 HTML 中的 标签。

  • 发送给客户端: 返回完整的 HTML 和状态数据。 客户端(注水)

  • 加载 HTML: 浏览器直接显示服务端渲染的静态页面(首屏可见)。

  • 加载 JS: 客户端加载前端框架的 JS 脚本。

  • 绑定状态: 使用服务端提供的数据初始化组件状态。 React 调用 hydrate(而不是 render),复用现有 DOM 并添加事件监听。

  1. Next.js 中的实现 Next.js 是 SSR 的典型框架,以下以它为例说明“注水”和“脱水”。 脱水(服务端) 在 getServerSideProps 或 getStaticProps 中:

// pages/index.js
export async function getServerSideProps() {
  const data = await fetch("https://api.example.com/data").then((res) =>
    res.json()
  );
  return {
    props: { data }, // 脱水数据
  };
}

export default function Home({ data }) {
  return <div>{data.message}</div>;
}

过程: 服务端调用 getServerSideProps,获取 data。 Next.js 将组件渲染为 HTML,同时将 data 序列化为 JSON,嵌入到页面:

<div id="__next">
  <div>Hello from API</div>
</div>
<script id="__NEXT_DATA__" type="application/json">
  {
    "props": { "data": { "message": "Hello from API" } },
    "page": "/",
    "query": {},
    "buildId": "..."
  }
</script>

注水(客户端) 客户端加载时: Next.js 的 _app.js 和 _document.js 自动处理: 读取 NEXT_DATA 中的序列化数据。 调用 ReactDOM.hydrate(现已升级为 hydrateRoot 在 React 18 中):

import { hydrateRoot } from 'react-dom/client';
import App from './App'; const root =
hydrateRoot(document.getElementById('**next'),
<App {...window.**NEXT_DATA\_\_.props} />);

结果:客户端接管 HTML,添加事件监听,页面变为动态。 4. 代码示例(手动实现) 如果不用框架,手动实现 SSR 的注水和脱水: 服务端(Node.js + React)

// server.js
const express = require("express");
const React = require("react");
const { renderToString } = require("react-dom/server");
const App = ({ data }) => <div>{data.message}</div>;
const app = express();
app.get("/", async (req, res) => {
  const data = {
    message: "Hello from SSR",
  }; // 模拟 API 数据
  const html = renderToString(<App data="{data}" />);
  // 脱水:将 HTML 和数据发送给客户端
  res.send(`
<html>
  <body>
    <div id="root">${html}</div>
    <script>
      window.__INITIAL_DATA__ = ${JSON.stringify(data)};
    </script>
    <script src="/client.js"></script>
  </body>
</html>
`);
});
app.listen(3000, () => console.log("Server running on port 3000"));

客户端

// client.js
import React from 'react'; import { hydrate }
from 'react-dom'; import App from './App';
const data = window.**INITIAL_DATA**;
// 从服务端获取脱水数据
hydrate(<App data="{data}" />,
document.getElementById('root'));
  1. 关键点与优化 脱水的注意事项 序列化安全:确保数据可序列化(如避免循环引用)。 数据体积:避免传输过多数据,影响首屏加载。

    安全性:敏感数据需加密或剔除。 注水的注意事项 一致性: 服务端和客户端渲染结果必须一致,否则会导致“hydration mismatch”错误。 示例:服务端用 Date.now() 生成时间,客户端可能不同步。 性能: 减少注水时的重复计算,利用服务端数据。 优化策略 按需注水: 只对交互组件注水,非交互部分保持静态。 Next.js 13 的 app 目录支持部分注水(Partial Hydration)。 延迟加载: 使用动态导入(React.lazy)推迟非关键组件的注水。 缓存: 服务端缓存脱水数据(如 Redis),减少重复计算。 6. 常见问题 Hydration 失败 原因:服务端和客户端渲染不一致(如条件渲染不同)。 解决:检查条件逻辑,确保两端一致,或用 useEffect 调整客户端状态。 性能瓶颈 原因:注水时加载过多 JS。 解决:代码分割,懒加载非首屏组件。 7. 总结 脱水:服务端渲染 HTML 并序列化状态(如 JSON),发送给客户端。 注水:客户端用 JS 激活 HTML,绑定状态和事件。 Next.js:通过 getServerSideProps 和 NEXT_DATA 自动实现。 手动实现:用 renderToString 脱水,hydrate 注水。 这个机制确保 SSR 的首屏速度和客户端交互性兼得。如果你在项目中遇到具体问题(比如 Next.js 的 SSR 优化),告诉我,我可以进一步帮你分析和实现!

上一页SSR 原理下一页CLI

最后更新于2个月前

这有帮助吗?

原先通过 变成 .history 模式的 vue-router 的 path="/"的路由, 对客户端访问和服务端的访问, 分别设置不同的转发

FCP:首次内容绘制时间,TTI:可交互时间

http://xxx.com/index.html
http://xxx.com/