SSR 应用与原 CSR 应用变更同步问题实践

news2024/11/16 15:46:39

75034a7bd064540e7a5adb88a60e4bba.gif

在上一篇介绍《天猫汽车商详页的SSR改造实践》一文中提到过,为免影响线上应用,我们的一体化应用(后面简称称 SSR 应用)是在原 CSR 项目基础上另起的应用仓库。

7443077d8775647bd591f87b5d42c444.png

背景

当商详业务有新需求迭代,CSR 仓库发生了变化,SSR 应用也要随之变更,变更流程如下:

5cdfbc51a8e69e38ba381648ec766571.png

即随着需求的不断迭代,技术侧需要同时维护两个代码仓库、两个研发应用,测试、CR、发布的成本都会翻倍

这对于前端和测试来说都有很重的维护成本。为了解决这个问题,最初我们想在代码侧保持 SSR 和 CSR 应用的一致,只在 build.json 里通过开关 ssr 配置:

{
  "targets": [
    "web"
  ],
  "web": {
    "ssr": true, // 通过调整 ssr 配置,让仓库应用发布 mpa 或 ssr 资源
    "mpa": true
  },
}

本地试跑了一下,切换配置后的开发构建都没什么问题。但是在接入阿里集团的研发平台时遇到了阻碍:平台不支持同一个代码仓库接入两个应用。后来发现 SSR 应用在发布时同时会发布相关页面的 CSR 链接,这样就只需要在研发平台维护一个应用,走一次发布。这给我看到了合并仓库与应用的希望,期望合并后的流程如下:

768f61ef5d2383a4e4f1c99c9ee2f288.png

以下是我们的改造过程。

5b0278d39a8e91e0f4dceb47822071e9.png

路由配置

从用户的角度,路由是网页链接的重要组成部分。比如现在有 a、b、c 网页,分别通过 www.taobao.com/a.htmlwww.taobao.com/page/b.htmlwww.taobao.com/page/blog/index/c.html进行访问,域名后标红的部分就是页面的路由。

从开发的角度,路由既与页面源码目录有关,也与工程构建配置有关。

在 SSR 应用里共有四处路由相关:

  1. 页面源码目录:通常放在 src/pages/ 根目录下,较少嵌套情况;

  2. app.json 配置:app.json 里配置各页面的访问路由与对应页面资源;

  3. SSR render 函数目录:SSR 页面的渲染逻辑所在之处,位于 src/apis/render/ 下,可能有嵌套;

  4. render 函数中的 PAGE_NAME:render 函数需要使用的页面资源路径,服务端靠执行这个页面文件生成有内容的文档。

这四处配置互相影响,相关文档又语焉不详。比如 PAGE_NAME,官方文档都没有提及它,只在初始化工程里注释了一句:「页面名称,默认对应 pages 下的目录名」。但从实践来看,这个参数非常重要,它甚至直接决定了服务端能否渲染成功。

下面是我对这几处路由配置的验证与理解。

  源码目录与路由配置

在多页应用中,不同页面的源码都写在 src/pages/下的同级目录:

├── src
│   ├── app.json                    # 路由及页面配置
│   ├── components/                 # 自定义业务组件
│   ├── apis/                       # 服务端代码
│   └── pages                        # 页面源码目录
│       ├── a 页面                   
│       ├── b 页面
|       └── c 页面
├── build.json                      # 工程配置
├── package.json
└── tsconfig.json

在未经过路由配置时,默认通过 域名/a.html访问,即使用页面在 pages 下的目录名(小写)

如需自定义路由,在 app.json 中修改配置,如以下两个页面配置:

{
  "routes": [
    {
      "name": "myhome",
      "source": "pages/Home/index"
    },
    {
      "name": "pages/about",
      "source": "pages/About/index"
    }
  ]
}

source 指定页面的源码位置,name 指定页面路由,这样我们就能够通过 域名/myhome.html域名/pages/about.html访问到页面。

构建结果的存放路径读取的是 name 配置:

└── build
     └── web      # csr 资源的构建结果放在 web 目录下
        ├── myhome.html/js/css                   
        └── pages               
              └── about.html/js/css

  render 函数中的 PAGE_NAME

render 函数就是当用户访问页面时,我们在 nodejs 服务端定义的渲染逻辑。

先来看一下 PAGE_NAME 是怎么用的:

// 页面名称,默认对应 pages 下的目录名
const PAGE_NAME = 'pages/index/index';


export default withController({
  middleware: [
    downgradeOnError(PAGE_NAME), // 降级中间件
  ],
}, async () => {
  const ctx = useContext();
  // nodejs 服务端的业务逻辑
  // ……
  // 生成渲染文档
  const ssrRenderer = await useSSRRenderer(PAGE_NAME);
  await ssrRenderer.renderWithContext(ctx);
});

PAGE_NAME被传参给 useSSRRenderer,以生成 SSR 文档。

useSSRRenderer 的源码中,可以看出 PAGE_NAME 是如何被消费的:

17f90d9b1761463c11fcad0f7c04da52.png

在函数内部,通过 PAGE_NAME 拼接出页面代码构建后的路径,然后从这个路径找出对应文件返回给 ssrRender对象,最后执行生成一份文档。

那么页面代码构建后到底放在哪里呢?看一下 SSR 工程下的构建结果:

└── build
      └── client                              # 客户端资源目录
      |      └── web                         # csr 资源依旧在 web 目录下
      │           ├── myhome.html/js/css                   
      |           └── pages               
      |                 └── about.html/js/css
      | 
      └── node                               # 服务端资源目录
           ├── myhome.js                     # node 端只生成 js 文件
           └── pages               
                └── about.js

node 资源的目录结构依照 app.json 中的 name 配置。

因此 ,PAGE_NAME 的取值并不是所谓「默认对应 pages 下的目录名」,而是对应 page 资源的构建目录,即 app.json 中页面 name 配置。

  render 函数的目录路径

先说结论,render 函数的目录路径直接决定了 SSR 链接的访问路由。

以下面的结构为例:

└── src
      └── apis                                  # 客户端资源目录
            └── render                         # csr 资源依旧在 web 目录下
                 ├── myhome.ts                   
                 └── pages               
                       └── child
                              └── about.ts

项目对应生成两条 SSR 链接:ssr域名/myhomessr域名/pages/child/about。可以看出,render 函数的目录路径就是它的访问路由。由于 SSR 链接访问的是一个服务,而不是一份文档资源,所以链接不是以 .html结尾。

这里是为了便于理解才把 render 文件和 pages 目录的名字保持一致,根据对 PAGE_NAME 的介绍我们知道,服务端渲染使用哪个页面资源与 render 文件名无关。如果业务需要,你把它命名为 abcd 也没有关系。

所以,render 函数的目录路径几乎没什么限制,除了下面一种情况。

在本地启动的时候,应用会默认在浏览器打开生成的第一条链接,这里生成的链接由 app.json 决定。如果你的 render 对应目录路径里没有相应的资源,而浏览器又自动帮你打开了这个链接,服务端就会报错,甚至直接断开。所以 render 函数的最佳实践是与对应页面在 app.json 中的 name 配置保持一致。

  总结

总结,SSR 应用中相关路径与页面路由的关系如下图所示:

70cce8345328873a5e24125f95a6e9b4.png

  1. pages 页面资源路径决定了 app.json 里的 source 配置

  2. app.json 的 name 配置决定了构建后产物位置(CSR/SSR 产物都依赖这个值)、CSR 访问路由

  3. render 函数文件的路径决定了 SSR 访问路由

  4. render 函数中的 PAGE_NAME 决定了该函数使用的页面资源,需要与对应页面的 name 值配置保持一致

b4e89e0115ef7f8e724352a8f866f601.png

技术问题

  云构建问题

在搞清楚上面的路由问题以后,很快地完成了 CSR 应用路由向 SSR 应用的迁移工作,顺利在本地启动了页面,并且成功验证了 SSR 页面和 CSR 页面功能。

但意外还是发生了:在预发布的云构建时,出现了熟悉的 window 错误——环境错误吗。

查了 diff,把所有改造时带 window 的对象全干掉了,再试错误还在,继续删删删,直到把这次改动删得七零八落,错误依然存在。

回忆想起,引了 jsdom 做服务端环境模拟,即使模拟再不给力,window 不至于不存在,更何况本地开发和构建都成功了。

这个错误像是构建时期执行了页面代码,于是把问题提交给了架构组。

很快,架构组同学确认了问题存在并给出了解决方案:一个尚未正式发布的构建器。

  手动模拟代码执行环境

在测试预发 SSR 链接时,新的环境问题出现了。

49fbd6adcacfaf7f254bda7c731246bd.png

定位了一下问题,发现是一个调试插件的锅。梳理一下它的执行逻辑,大概是这样:

window.__mito_result = 'something'; // 定义变量,挂载在 window 上


console.log(__mito_result); // 使用变量时,没有通过 window

变量挂在了 window 下,但没有通过 window 访问,这种直接读取未明确定义变量的方式,运行时会从当前环境的 globalThis 找,node 端的 globalThis 并不是 window,导致这个变量没有找到,报 not define错。

这个问题有几个解决方式:

  1. 修改插件执行时机

  2. 修改插件注入时机:因为服务端只需要生成文档内容,这个插件是文档无关的,不需要在预发资源里注入

  3. 环境模拟:使用框架能力对特定变量进行模拟

因为这个插件只会注入到预发文件里,不影响正式发布的文件。因此而修改插件或者功能实现,感觉做得有点重。综合下来还是选择了环境模拟方案。

一体化应用在框架侧进行用户自定义环境变量模拟有以下几步:

// 第一步、在 build.json 里配置 mockEnvBrowser 
// build.json
{
  "web": {
    "ssr": {
      "mockBrowserEnv": true
    },
  },
}


// 第二步、在 render 函数中进行传参
// src/apis/render/render-function-path.js
export default withController({
  middleware: [
    downgradeOnError(PAGE_NAME),
    phaIntercept(PAGE_NAME),
  ],
}, async () => {
  // 。。。
  const ssrRenderer = await useSSRRenderer(PAGE_NAME, {
    mockBrowserEnv: true, // 需要再次配置为 true
    globalVariableNameList: [ // 待模拟的变量名列表
      '__mito_data',
      '__mito_result'
    ],
    // 可以不对上面的变量进行实现,这时上面的变量在执行时值为 undefined
    browserEnv: {}, // 待模拟变量的对应实现
  });
  // 。。。
}

这里顺便研究了一下框架侧的环境变量实现,还挺有意思的。在 useSSRRenderer 源码里,如果发现配置了 mockBrowserEnv: true,会走到下面这个逻辑,其中最核心的是字符串构造的函数:

d0395e18a91fb1f7b5b30416f414380e.png

剥离一下它的执行核心逻辑:

// 定义一个执行函数
function mockEnvFn (...globalVariableNameList) { // 定义形参
  execute('page.js');
  // 页面处在 mockEnvFn 函数的上下文,页面逻辑中需要用到 window 的,可以从该函数的传参中取得
}


// 执行该函数
mockEvnFn(globalVariableList); // 传入实参

框架层给页面函数包了一个外层函数,为这个外层函数定义了形参列表,然后执行这个外层函数,这样页面函数就处于形参的上下文里,从而实现了环境模拟。

  媒体/脚本标签注入

在 CSR 的 app.json 中,有 metas 属性和 scripts 属性,可以向文档中插入一些媒体属性和页面代码执行前的相关工具库脚本。但是 SSR 模式下,这两个属性不会生效,需要将其改写到 documents 中。与 metas 标签有一点区别的是,js 执行脚本最好放到 dangerouslySetInnerHTML 属性中,执行位置在 <Script /> 组件之上,<Root /> 标签以下。

在实际构建产物中,CSR 链接取的是 app.json 中的 metas/scripts 配置,SSR 链接取的是 document 中的标签

需要说明的是,当 SSR 链接降级到 CSR 模式时,metas/scripts 取的是 app.json 中的配置。因此,就算 SSR 应用没有投放 CSR 链接的需要,也应当同时在两处维护 metas/scripts,否则 SSR 降级后表现可能会不一致。

728fce95caf4ab11f24852c4ae6b21f8.jpeg

业务问题

在研发平台中,一个仓库只能接入一个应用,应用生成的访问链接又与应用名称紧耦合。使得合仓后发布的 CSR 链接与原应用的 CSR 链接不同,需要测试侧进行功能回归和业务侧进行链接替换。

  由于资源访问特性带来的测试问题

由于 SSR 应用发布时不构建的特性,日常和预发构建的文档会被用作线上,文档中的资源访问地址也都默认打到线上(访问资源的链接都是 g.alicdn.com)。而资源的发布并未变化(预发时资源在 dev.alicdn.com,线上时资源在 g.alicdn.com),这就导致预发文档加载时拿不到线上资源。

SSR 服务端可以能够通过动态设置 webpack_public_path 改变构建文档里的资源链接:

fdb5c881808fa032f6d47b5936d400c9.png

预发链接就能够正常访问到预发 cdn 下的资源。

修改 __webpack_public_path__实质上是 SSR 拿到运行时的配置,在访问时动态替换了资源链接。而 CSR 模式下,文档在构建结束后就生成了,没有办法在运行时动态更改资源链接。

为了测试同学能够正常工作,需要在访问网页时,通过资源代理的方式,对资源路径进行重定向。

  商详链接替换问题

至于业务域如何平稳替换线上链接,这就与每个业务的具体情况相关了。

以汽车商详为例,我们既有搜索、会场、店铺、订单、互动等基础链路域的访问,也有小程序等汽车阵地的导购引流,以及端外留资的商业化线索投放。本就需要根据不同场景,投放不同的链接,需要各处去分别推动。

c52897483edc19209bc3562e961fb1e8.jpeg

总结

SSR 改造做到现在,各种分享做了不少。曾有其他团队的工程师问我,怎么你碰到的这些问题,我一个都没有碰到过?

我自己复盘后,结论是:原本为改造项目,需要带着镣铐跳舞。原项目已经做了一年多,本身复杂度比较高。此外,业务场景的多样性,使我们不得不同时维护 CSR 和 SSR 链接,这也是大多数 SSR 应用不必承担的责任。

这三点原因使我不得不去探索少有人走的道路,虽然曲折,好在走通了,这也要感谢架构团队大佬们的支持。

另外,也有一些别的体会。在推动一项技术在团队内落地的过程中,很大一部分因素不在技术层,而在于需求的定位、资源的协调、业务的落地。这些非技术问题如果能想在前面做在前面,对技术的落地只会大有裨益。

d901d4c13d16bd0edc0366494d185e0e.jpeg

团队介绍

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

¤ 拓展阅读 ¤

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

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

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

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

相关文章

Java基础学习(18)反射、动态代理

Java基础学习 一、反射1.1 什么是反射1.2 获取class对象 二、综合练习2.1 保存信息2.2 文件的动态创建 三、动态代理3.1 什么是动态代理3.2 创建代理 一、反射 1.1 什么是反射 反射允许对封装类的字段&#xff0c;方法和构造函数的信息进行编程访问 个人理解&#xff1a; 就是…

【JVM】5. 本地方法接口和本地方法栈

文章目录 5.1. 什么是本地方法&#xff1f;5.2. 为什么使用Native Method&#xff1f;5.3. 本地方法栈 5.1. 什么是本地方法&#xff1f; 简单地讲&#xff0c;一个Native Method是一个Java调用非Java代码的接囗。一个Native Method是这样一个Java方法&#xff1a;该方法的实现…

【007】C++数据类型之原码、补码、反码

C数据类型之原码、补码、反码 引言一、原码、补码、反码的概述二、为什么要使用补码&#xff1f;三、对数据的存四、对数据的取总结 引言 &#x1f4a1; 作者简介&#xff1a;专注于C/C高性能程序设计和开发&#xff0c;理论与代码实践结合&#xff0c;让世界没有难学的技术。包…

KingbaseES V8R3 集群运维案例--sys_rewind恢复备库节点

​ 案例说明&#xff1a; 在KingbaseES V8R3集群执行failover切换后&#xff0c;原主库被人为误(未配置recovery.conf)启动&#xff1b;或者人为promote备库为主库后。需要将操作节点再重新加入集群&#xff0c;此时节点与主库的timeline将出现分叉&#xff0c;导致节点直接加入…

ChatGLM-6B 本地部署指南!

Datawhale干货 作者&#xff1a;宋志学&#xff0c;Datawhale成员 注意事项-写在最前 显卡需要至少6GB的显存使用GPU部署模型需要自行安装torch和与自己显卡匹配的CUDA、cudnn 下载ChatGLM-6B 在GitHub上下载chatglm-6b的源码&#xff0c;地址如下 https://github.com/THUDM/C…

信息安全-应用安全-SCA技术:SBOM应用实践初探

目录 软件供应链安全治理 供应链安全概述 风险治理重点 何为SBOM SBOM的元素 SBOM的格式 SBOM使用场景 如何使用SBOM 选择SBOM工具 SBOM与风险情报关联 围绕SBOM建立管理流程 关于SBOM的思考 现代软件都是组装的而非纯自研。随着开源组件在数字化应用中的使用比例越…

【C++】运算符重载与赋值运算符重载(显式与默认)的特性与格式以及前置++,后置++重载

文章目录 前言一、运算符重载二、赋值运算符重载1. 赋值运算符重载格式&#xff1a;2. 用户没有显式实现时&#xff0c;编译器会生成一个默认赋值运算符重载&#xff0c;以值的方式逐字节拷贝。3. 赋值运算符只能重载成类的成员函数不能重载成全局函数 三、前置与后置 前言 C为…

【Linux】Linux小程序-进度条

目录 一、\r和\n的理解 二、行缓冲区概念 三、进度条源代码 一、\r和\n的理解 \r&#xff1a;回车&#xff1b; \n&#xff1a;换行&#xff1b; 那么请问这两个有什么区别呢&#xff1f; 比如&#xff1a;我们在编写内容的时候&#xff0c;一行没有写完的情况下&#xff0c;需…

【信息安全案例】——软件解密技术(以OllyDbg为例)

目录 &#x1f552; 1. 软件解密技术&#x1f558; 1.1 概述&#x1f558; 1.2 爆破&#x1f558; 1.3 跟踪注册&#x1f558; 1.4 写出注册 &#x1f552; 2. 破解相关问题&#x1f558; 2.1 破解程度&#x1f558; 2.2 破解线索 &#x1f552; 3. 实验&#xff1a;使用 OllyD…

分布式系统原理

高可用是指系统无中断的执行功能的能力&#xff0c;代表了系统的可用程度&#xff0c;是进行系统设计时必须要遵守的准则之一。 而高可用的实现方案&#xff0c;无外乎就是冗余&#xff0c;就存储的高可用而言&#xff0c;问题不在于如何进行数据备份&#xff0c;而在于如何规避…

MacOS Ventura 13.4 (22F66) 带 OC 引导双分区黑苹果镜像

苹果今日向 Mac 电脑用户推送了 macOS 13.4 更新&#xff08;内部版本号&#xff1a;22F66&#xff09;&#xff0c;本次更新距离上次发布隔了 41 天&#xff0c;主要解决了与Apple Watch自动解锁、蓝牙键盘、屏幕使用时间和VoiceOver相关的问题&#xff0c;推荐大家安装升级。…

Vue 级联组件添加按钮并添加点击事件加传参

我这里采用的是jqvue实现的此功能&#xff0c;首先是要把按钮追加进去&#xff0c;当然头开始写真实dom会导致页面上也追加显示&#xff0c;但是我想实现的是在级联组件上追加所以&#xff0c;选择创建虚拟dom&#xff0c;然后传参这点实在是研究试错了半天&#xff0c;最后选择…

聚观早报|ChatGPT 推出官方 iOS App;大疆称将接收OPPO哲库工程师

今日要闻&#xff1a;ChatGPT 推出官方 iOS App&#xff1b;大疆称将接收OPPO哲库工程师&#xff1b;菜鸟、盒马启动上市计划&#xff1b;苹果公司限制员工使用ChatGPT&#xff1b;张勇&#xff1a;阿里云将分拆上市 ChatGPT 推出官方 iOS App 北京时间 5 月 19 日&#xff0c…

苹果扩充AirTag应用场景,苹果Find My应用更加广泛

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的清单&#xff0c;苹果近日获得了一项技术专利&#xff0c;将追踪器附着人体或者服装上&#xff0c;从而监测健康和活动数据。 AirTag 不仅可以追踪某件事物之外&#xff0c;还可以通过安装在人体的不同位置&#xff0c…

人工智能本来是个很简单的事,咋被人们整的这么神秘?

&#xff08;1&#xff09; 很多人以为大模型的参数量大是因为数据多。 其实大模型的参数量和超参数的配置相关。主要的超参数有下面几个&#xff1a; 词表大小&#xff1a;Vocab_Size最大位置编码大小&#xff1a;Max_Position_embeddings隐层节点数大小&#xff1a;Hidden_Si…

Linux - 第16节 - 网络基础(应用层二)

1.HTTP协议 我们在套接字部分编写的代码和应用层一中编写的网络计算器代码都是在应用层工作的&#xff0c;是应用层代码&#xff0c;因此应用层代码包括&#xff1a; &#xff08;1&#xff09;基本系统socket套接字系列接口的使用。 &#xff08;2&#xff09;定制协议&#x…

一个炎爆术分享给大家~

先来强势围观&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><script src"https://cdnjs.cloudflare.com/ajax/libs/three.js/r120/…

23种设计模式之外观模式(Facade Pattern)

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章将23种设计模式中的外观模式&#xff0c;此篇文章为一天学习一个设计模式系列文章&#xff0c;后面会分享其他模式知识。 如果文章有什么需要改进的地方还请大佬不…

云从科技从容大模型:大模型和AI平台什么关系?为什么造行业大模型?

原创&#xff1a;亲爱的数据 2023年5月18日&#xff0c;坐标广州南沙&#xff0c;来自云从科技的“云从从容大模型”正式亮相。 自此&#xff0c;云从科技从CV四小龙“进阶”成为一家AI大模型公司&#xff0c;同时&#xff0c;别忘记云从还有一个身份常被人提起&#xff1a;首家…

纯虚函数与抽象类

纯虚函数与抽象类 虚析构函数状态转换的引入C中的状态转换C中的状态转换职责链模式 纯虚函数和虚基类使用规则实例接口继承和实现继承 虚析构函数 在上一次博客中写到了这么一段代码&#xff1a; class object { private: int value;public:object(int x 0) : value(x) {}~o…