天猫汽车商详页的SSR改造实践

news2024/12/23 18:10:36

d6d0e8a1bddc776649b99f359db54a7b.gif

由于汽车业务的特殊性,天猫汽车基于 Rax 多页应用自建了商品详情的 H5 页面。自定义商详承载了众多业务能力和投放场景。随着业务的发展和页面承载内容的增多,开始出现白屏时间太长等体验问题。

前端性能优化算是个老生常谈的问题,我们的页面已经做过首屏接口合并、图片懒加载、骨架屏等体验优化,想进一步提升用户体验就要从渲染机制和渲染容器入手了。从容器侧看,淘宝端原生提供的 pha 容器提供的数据预请求、资源离线缓存等功能,可以有效提升手淘内的 H5 页面体验。但是其它渠道的端缺少类似容器能力,需要从渲染机制方向寻找出路。

SSR(Server Side Render)是相对于现有渲染机制CSR(Client Side Render)的一种渲染方案。在用户通过客户端(Client Side)请求页面资源时,CSR 拿到是一份空文档,再通过数据请求、执行渲染脚本后生成文档,最后展示给用户。而 SSR 在请求后拿到的就是服务端(Server Side)经过数据请求脚本渲染以后的完整文档,由于它不强依赖客户端的能力,具有更加稳定的性能和较好的用户体验。

为了提升淘宝外、特别是中低端机用户的浏览体验,我们对自定义商详进行了 SSR 化探索,完成了 Rax 多页应用向 Rax 全栈应用的改造,以下是我们的改造历程。

c269121c2c64772ebb17e0cf4afff56b.png

代码结构

项目现在的 Rax 多页应用体系和目标体系 Rax 全栈应用都基于 Rax 框架,目录结构比较相似,全栈应用在多页应用的基础上增加了服务端渲染函数和FaaS相关 工程配置。Rax 多页应用的目录如下:

├── src
│   ├── app.json                    # 路由及页面配置
│   ├── components/                 # 自定义业务组件
│   ├── pages/                      # 页面
├── build.json                      # 工程配置
├── package.json
└── tsconfig.json

Rax 全栈应用目录如下:

├── src
│   ├── apis                      # 函数源码
│   │   ├── configuration.ts
│   │   ├── lambda/               # 接口
│   │   ├── render                # 渲染函数目录                       
│   │   │   ├── home/             # 渲染函数,需与 pages 里的页面名一一对应       
│   │   │   └── ....../  
│   │   └── typings/              # 数据类型定义
│   ├── pages                     # 页面      
│   │       ├── home/   
│   │       └── ....../   
│   ├── document/                 # 文档结构
│   ├── app.json                  # 路由及页面配置
│   └── typings.d.ts
├── build.json                    # 工程配置
├── f.yml                         # 函数平台配置
├── midway.config.ts.             # midway 配置,主要指定接口和渲染目录
├── package.json
└── tsconfig.json

从目录来看,承载页面渲染的核心业务逻辑如 pages、components 都无须改动。SSR 模式下,服务端返回的不再是空文档,而是经过一次渲染后的文档框架,所以需要保持代码在 Node 环境下可运行。由于汽车商详在 Rax 多页应用开发时没有这种环境约束,因此对技改提出了环境模拟的需求,这点在后面会着重提到。

400167c6c29b6befd1d90f6f5d35c38a.png

数据请求

CSR 模式下,进入页面后拉取主接口数据,执行 js 完成页面渲染。SSR 下,主接口数据需要在服务端获得,完成服务侧的文档渲染。

fdc3b93d58124cb59baf3ccc35d17650.png

客户端得到的只是一个干文档,需要再次执行一遍 js 以激活文档的事件监听、状态传递等,成为可交互的页面,这个过程也有一个形象的名称,叫注水(hydrate):

cfa2e2ba0e50b1ff08ffbd6c372331c8.png

  请求流程的转变

由于请求时机的改变,请求及其前后逻辑需要移至服务端执行。

汽车商详基于自建的一套端到端的渲染方案(XRoot)进行接口设计和页面渲染。接口数据中是组件集,包括各组件名称和对应组件需要的 props。我们封装了一套 XRoot 组件,自动执行接口请求、数据注入、页面渲染等工作。

在商详首页中,该接口请求的工作并不只是数据拉取与注入,还承担了一系列请求前后的业务逻辑(如设置全局变量、容灾处理等)。对其中逻辑进行抽象归纳,将业务逻辑聚合到 beforeRequest 和 afterResponse 中:

4bed7010b09608af9ddebe71b05a416d.png

SSR 模式下,主接口请求在服务端执行,其执行逻辑为:

647e87e19cc077cf681f46de689c9235.png

afterResponse 逻辑中有一部分是对数据本身的处理,留在服务端实现。另一部分是 UI 相关的,需要在客户端 Hydrate 阶段再次执行。

  中间层网关的模拟实现

在阿里集团内,移动端接口和后端的交互会经过了一层 MTOP 网关。MTOP 网关提供了协议解析、安全防护、稳定性保障等能力。SSR 模式下,服务端的数据请求无法经过这层网关,而是直接访问后端 API。

此时业务依赖到的相关 MTOP 能力需要业务侧在 SSR 下手动补齐,其中直接影响页面渲染的就是 MTOP 对接口中空数据的处理:

40dc44084173ff0997d6d9f15481a4db.png

原始接口数据中值为 null 的属性都被 MTOP 层处理过了,如果 SSR 不做空值属性的删除,前端的默认取值就会出问题:

const { text, tip = '默认提示' } = tagsList[0];
console.log(tip); // 非空处理前:null ;非空处理后:'默认提示'

这里写一个简单的工具函数来做这件事:

export const deleteNullProperties = (obj: Object | Array<any>) => {
  const memory = new Set();
  const fn = (obj) => {
    if (memory.has(obj)) return obj;
    if (['[object Object]', '[object Array]'].includes(Object.prototype.toString.call(obj))) {
      for (const [key, value] of Object.entries(obj)) {
        if (value === null) {
          delete obj[key];
        } else {
          obj[key] = fn(value);
        }
      }
    }
    memory.add(obj);
    return obj;
  }
  return fn(obj);
}


另外,MTOP 网关还提供了用户登陆态的解析与传递,SSR 侧也需要业务自行从 cookie 中读取用户登陆信息。

bbcedefbd2d3ed528e9e1b90380de0dc.png

环境模拟

一般来说,由于服务端和浏览器端环境的差异,在做前后端同构应用的时候,开发者都会自觉注意环境差异,避免在不恰当的时机访问浏览器对象,导致 SSR 侧报错。

但是由于本次是改造项目,历史项目只关注 CSR 场景,不可避免地充斥着预期以外的 DOM/BOM 访问,此时有以下几种解决方案:

f0a7a486986d8fb0ecbcad275cc53e85.png

从改造成本和维护成本来看,借助框架能力和引入社区方案是两种可以低成本探索的思路。

  框架能力

Rax 全栈应用框架本身提供了在服务器端模拟浏览器环境的能力,从而来尽可能保证 SSR 和 CSR 编码的一致性。其环境变量模拟的基本原则是:

  1. 所模拟的信息可由服务端数据推导得出,例如 location、navigator。

  2. 所模拟的信息不会引起代码执行逻辑的错误,例如对 localStorage 的模拟。

从中可以看出,框架模拟能力并不旨在进行环境模拟,相反它还在试图为页面提供一些真实有用的信息。因此框架提供的模拟能力相对有限,一些常用方法也没有空方法占位,如试图调用 window.addEventListener 时会报 undefined 错误。

由于 Hydrate 阶段的存在,我们的目的只是希望代码能够在服务端不报错,客户端侧的二次渲染会提供更加准确的信息,框架能力并不能满足需要。

  jsdom-来自社区的服务端环境库

jsdom 是许多 Web 标准的纯 JavaScript 实现,特别是 WHATWG DOM和HTML标准。该项目并不是为了模拟服务端环境而存在,它的主要目标是模拟足够多的 Web 浏览器子集,使得页面可以在服务端运行,从而测试和抓取真实世界的 Web 应用程序。

eb244cb24d0a26149d1334f2d85480a9.png

它对浏览器环境强大的模拟能力可以帮助我们避免对项目源码的改造。

由于 jsdom 同样追求对功能性的模拟,功能强大的同时,在服务端运行时可能出现预料之外的问题,需要详尽测试。在使用 jsdom 时,也遇到了一些版本兼容问题,可以通过降低库版本和动态加载等方式解决。

7dea5927e4ed3560d0a72995118c6e61.jpeg

其他问题

  降级策略

Rax 全栈应用框架采取中间件降级策略,SSR 侧有任何报错都会自动将空文档返回给客户端,由客户端发起数据请求和渲染,即降级为 CSR。

e49d650a174592ccbe848112e077f878.png

降级逻辑

123b65473575c2b14009be7e23601fae.png

降级中间件源码

业务侧也可以根据需要使用框架提供的 useCSRRenderer 钩子函数进行主动降级:

d644a7614e98be0d479f64ac8394f6d9.png

  自闭合标签问题

习惯了 react 的 JSX 语法体系,我们通常会写出许多自闭合标签以提高可读性和代码整洁性:

// 自定义组件的自闭合
<MyComponent />


// 原生标签的自闭合
<div />

但事实上 HTML 规范并不支持对 <div /> 的解析,以一个简单的 demo 为例:

92ac95e078dbef4560c4bf55ac1dbcd8.png

在开发者的期待里,b 和 c 同级,d 是 c 的子标签。但实际渲染结果中,c 成了 b 的子级。

对这种情况的一种表象理解是,浏览器把自闭合标签当作正常 <div> 头处理,为其匹配了一个 </div>。到最后闭合标签不够用了,浏览器自动加了两个 </div>,导致文档结构发生了变化:

227c0a3fc47b7df5b7382709f67384d5.png

为什么在 JSX 中写自闭合标签能够得到期待中的结果呢?事实上是 CSR 框架侧帮我们做了这件事,如 react 在其官方文档里就有提到:

react地址:https://react-cn.github.io/react/tips/self-closing-tag.html

3d7d86a9a627261e4f6ca9c1344b712f.png

从实践来看,Rax 框架在 CSR 侧的渲染同样做了类似的处理,但是 Rax 全栈应用的服务端渲染没有,其 SSR 侧生成的文档只是简单将自闭合标签原样返回:

fcec399236dc3c80872ed1ceacd31fda.png

不同阶段下的文档结构

由于在 hydrate 阶段浏览器判断 ssr 文档与客户端预期不一致时,会被丢弃掉重新渲染,所以这种处理没什么功能性问题。只是这样就失去了 SSR 的首屏优化功能。在 Rax 团队完善自闭合标签问题之前,业务侧需要避免写出非自定义标签的自闭合。

  与 CSR 仓库的功能同步

因为我们是对 CSR 项目的改造,为了不影响线上功能,另启了一个应用仓库。随着改造的进行,原 CSR 项目也一直在接新需求不停迭代。

借助 git merge --allow-unrelated-histories 机制,我们在代码同步方面不必投入太多成本。但是为一个业务场景维护两套应用,其中需要付出的沟通、维护、测试等成本还是很高。

  下一步计划

如前所述,两个应用的维护成本较高。因为两者的框架基础是一致的,我们就希望将两个应用合并起来,至少能降低一点代码同步和沟通成本。与技术支持同学沟通了一下,发现没有这个先例,可能会有很多意料之外的问题,所以这部分仍在试错中。

6627d86c8cdaebe2f5ae7cf0bcf15288.jpeg

小结

借着本次服务端渲染(SSR)改造升级的实践,我对前端的渲染机制也有了更深入的理解。虽然 SSR 相较于 CSR 有着许多优势,却未成为 Web 渲染的主流模式,是因为构建 SSR 应用并不轻松:需要熟悉服务端开发、了解服务端和客户端的环境差异、服务部署、服务器运维等,对前端开发提出了更高的要求。

而云原生基础设施的蓬勃发展、Nodejs FaaS 函数服务建设的逐渐完善,都将大大降低 SSR 应用的接入门槛。前端开发者将只需关注业务逻辑就能轻松获得 SSR 能力,用户也更有机会得到更好的交互体验。

1c5a1c5b5e6828fbdfeb35b3d960f7d8.jpeg

团队介绍

我们是阿里巴巴大淘宝技术汽车技术团队,是一支集研发、数据、算法一体的部门,利用互联网+数字化垂直整合汽车行业,打造消费者线上看车、买车、养车的极致体验。在这里,你会接触到新零售核心技术,交易、供应链、结算、阵地运营等。在这里,团队氛围融洽,业务发展迅猛,技术挑战多多,让你有商业思考,又有技术深度。阿里汽车高速发展的路上,期待您的加入!

¤ 拓展阅读 ¤

3DXR技术 | 终端技术 | 音视频技术

服务端技术 | 技术质量 | 数据算法

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/141001.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

DBeaver安装教程

软件安装包官网地址https://dbeaver.io/但一般由于网络问题&#xff0c;我们是很慢才连接上&#xff0c;或者连接不上这里提供压缩包链接&#xff0c;解压可直接用&#xff0c;亲测链接&#xff1a;https://pan.baidu.com/s/14KGY0JakRTOHQVkiRUWc7g?pwdcxda 提取码&#xff1…

2022年第十二届APMCM亚太杯1月增赛E题思路分享

2022年亚洲及太平洋地区建模数学竞赛问题E 有多少颗核弹可以摧毁地球? 题目回顾&#xff1a; 1.基本数据分析 a) 哪些国家曾经拥有过核武器? b) 在过去的20年中&#xff0c;哪个国家的核武器库存减少或增加得最多? c) 在哪五年早&#xff0c;核武器试验发生的次数最多…

【笔记:模拟CMOS集成电路】噪声——分析基础(1)

【笔记&#xff1a;模拟CMOS集成电路】噪声——分析基础&#xff08;1&#xff09;前言1噪声的定义2噪声的描述2.1统计特性&#xff08;1&#xff09;平均功率&#xff08;2&#xff09;功率谱密度&#xff08;PSD&#xff09;2.2噪声相关指标&#xff08;1&#xff09;SNR&…

【C++】vector用法简单模拟实现

文章目录1.vector的介绍及使用1.1 vector基本概念1.2 vector的使用1.2.1 vector的定义 (构造函数)1.2.2 vector的迭代器使用1.2.3 vector 增删查改1.2.4 vector 迭代器失效问题&#xff08;重点&#xff09;1.2.5 vector 空间增长问题2. vector的模拟实现2.1 SGI版vector实现示…

Logback的使用

文章目录概述使用配置文件详解<appender>ConsoleAppenderFileAppenderRollingFileAppender滚动策略<root><encoder>日志级别概述 Logback是一个实现了Slf4j 规范的实现框架&#xff0c;log4j的作者实现了Logback。 下面这是Logback官网的介绍&#xff1a; …

排序算法之插入排序

目录 排序算法介绍 插入排序 算法流程 算法实现 python C 插入排序与冒泡排序 排序算法介绍 《Hello算法》是GitHub上一个开源书籍&#xff0c;对新手友好&#xff0c;有大量的动态图&#xff0c;很适合算法初学者自主学习入门。而我则是正式学习算法&#xff0c;以这本…

云原生丨DBSwitch数据迁移实践

文章目录一、前言一、部署1. 环境准备2. 安装包部署二、使用1.登录2. 使用步骤-gauss迁移到gauss3. 使用步骤-MySQL迁移到gauss三、postgres和 gauss差异说明一、前言 DBSwitch是一个异构数据库迁移工具&#xff0c;能够提供源端数据库向目的端数据库的批量迁移同步功能&#…

Spring的体系结构介绍

Spring框架采用的是分层架构&#xff0c;它一系列的功能要素被分成20个模块&#xff0c;这些模块大体分为Core Container、Data Access/Integration、Web、AOP(Aspect Oriented Programming)、Instrumentation、Messaging和Test。 上图中&#xff0c;包含了Spring框架的所有模…

windows 安装docker使用教程

一、win10上安装docker注册docker账号配置虚拟化启用 bios 虚拟化&#xff0c;是否已启用可以通过任务管理器查看如果没有启动请看&#xff0c;否则可以直接跳到2开启 Hyper-V进行后续操作&#xff1a;irtualization Technology(VT)&#xff0c;中文译为虚拟化技术&#xff0c;…

LabVIEW拓扑

LabVIEW拓扑交换机拓扑是交换机模块上通道和继电器的组织表示形式。拓扑通常显示模块上继电器的默认连接&#xff0c;并标记通道名称。某些交换机模块可以使用多个拓扑或拓扑的变体。某些接线端子或附件可能会强制交换机模块使用给定的拓扑或拓扑集。NI-开关 支持 以下 拓扑&am…

009-Ensp-实验-VLAN间互联

实验要求 实现两台不同vlan的设备可以互联 实验结构 实验步骤 1. PC1 /PC2 配置ip /gateway 2. LSW4 新增两个VLAN e0/0/2 - e0/0/3 设置为access类型 加入到不同的vlan e0/0/1 设置为trunk类型 允许所有tag经过 3. LSW3 新增两个VLAN g0/0/1 设置为trunk 允许所有tag经过…

Http Https TCP/IP理解

一、 TCP Http Https UDP IP 介绍TCP/IP&#xff08;Transmission Control Protocol/Internet Protocol&#xff0c;传输控制协议/网际协议&#xff09;是指能够在多个不同网络间实现信息传输的协议簇。TCP/IP协议不仅仅指的是TCP 和IP两个协议&#xff0c;而是指一个由FTP、SM…

[数据结构基础]树和二叉树的概念、结构及性质

目录 一. 树 1.1 树的概念及结构 1.2 树和树的节点的相关概念 1.3 定义树的四种方式 1.3.1 说明了树的度为N 1.3.2 采用顺序表的方式存储子节点 1.3.3 采用结构体数组进行存储 1.3.4 左孩子右兄弟表示法&#xff08;最优&#xff09; 二. 二叉树 2.1 二叉树的概念及结…

完爆90%的性能毛病,数据库优化八大通用绝招

毫不夸张的说咱们后端工程师&#xff0c;无论在哪家公司&#xff0c;呆在哪个团队&#xff0c;做哪个系统&#xff0c;遇到的第一个让人头疼的问题绝对是数据库性能问题。如果我们有一套成熟的方法论&#xff0c;能让大家快速、准确的去选择出合适的优化方案&#xff0c;我相信…

9.JS-作用域-预解析

1.作用域 代码名字&#xff08;变量&#xff09;在某个范围内起作用和效果&#xff0c;目的是为了提高程序的可靠性&#xff0c;减少命名冲突 2.js作用域的分类&#xff08;es6之前&#xff09; 全局作用域和局部作用域 全局作用域&#xff1a;整个script标签或者是一个单独…

【数据结构与算法——C语言版】2. 数组

前言 本篇文章介绍了数组的基础定义及使用&#xff0c;并针对数组的基本增删改查做了一些代码示例&#xff0c;下篇文章将讲解基于数组的更高效的方法&#xff0c;比如二分查找、插入排序等。 数组基础 数组定义 int nums[10] {0};如上&#xff0c;定义一个int类型的数组&…

【Python百日进阶-数据分析】Day146 - plotly小提琴图:px.violin()/go.violin()

文章目录四、实例4.1 Plotly Express 的小提琴图4.1.1 Plotly Express 的基本小提琴图4.1.2 带框和数据点的小提琴图4.1.3 多个小提琴图4.1.4 叠加的小提琴图4.2 graph_objects的小提琴图4.2.1 基本小提琴图4.2.2 多条小提琴迹线4.2.3 分组小提琴图4.2.4 分裂小提琴图4.2.5 高级…

植物大战僵尸:寻找召唤僵尸关键CALL

通过遍历寻找召唤僵尸的CALL&#xff0c;通过调用CALL出现自定义的僵尸&#xff0c;加速僵尸的出现。我们可以通过僵尸出现在屏幕中的个数来遍历寻找僵尸出现的CALL 僵尸CALL的遍历技巧&#xff1a;首先打开CE->进入游戏开始新的游戏-> 直接搜索未知初始化数据等待出现第…

新手程序员入职新公司,该如何快速上手?

小C今天又来找我了&#xff0c;一脸不开心的样子&#xff0c;我心想不是刚被开除&#xff0c;不会这么快又被开除了吧。 小C对我说&#xff1a;下周马上要入职新公司了。 我&#xff1a;好事啊&#xff0c;怎么一脸不开心的。 小C&#xff1a;马上要入职新公司了&#xff0c…

Altium Designer 20 凡亿教育视频学习-04-2

屏蔽电源线或者我们想要屏蔽的线&#xff0c;只显示我们想要的线 PCB上都是线会妨碍我们布线&#xff0c;因此在分模块的布置器件时&#xff0c;我们只单纯的显示我们需要的模块线&#xff0c;其他的线路就不显示了。白线叫预拉线&#xff0c;就是你真实的线要连的两个地方 第…