一种纯前端的H5灰度方案

什么是灰度发布

在互联网领域,灰度发布是产品质量保障的重要一环,它可以让某次更新的产品,以一种平滑,逐步扩大的方式呈现给用户,在此过程中,产品和技术团队可以对功能进行验证,收集用户反馈,不断优化,从而减少线上问题的影响范围,完善产品功能。

在前端领域,APP和小程序天生就具有灰度的能力,一般基于发布平台来控制。但 H5 却缺少这种天生能力,而且 H5 一旦发布就会影响所有用户,更加需要一套灰度系统,来保证产品的稳定性。

灰度发布的本质

既然要让部分用户先使用新功能,就需要做好两件事情,这也是灰度的本质:

  1. 版本控制 同一个项目需要在线上同时发布至少两套页面,一套针对全量用户,一套针对灰度用户
  2. 分流控制 需要有一套规则,把用户按某种特征划分为不同的群体,可以是用户ID,门店、城市,也可以是年龄,亦或是随机。命中的用户访问灰度页面,未命中的访问全量页面。

那么想要实现灰度发布有哪些方案呢?

可选的灰度方案

Nginx+lua+redis

通过使用 Nginx 的反向代理特性,我们可以根据请求的特定属性(例如ip、请求头、cookie)等有选择性的将请求路由到全量或灰度版本。

同时在 Nginx 中嵌入 Lua 脚本,负责根据预定义的灰度发布策略处理请求,Lua 脚本可以从 Redis 中获取灰度配置。从而确定哪些用户可以访问新版本,那些用户应该可以访问旧版本。

Redis 用于存储灰度发布的配置数据。

通过这种方式可以实现基于 Ngnix 的灰度发布,但这种方式并不适合我们,为什么呢?

因为我们的C端H5页面连同HTML文件都是直接投放在 CDN 上,这就意味着我们没有中转服务层,无法使用第一套 Nginx 的方案,而且使用 Nginx 也会响应降低页面加载速度,虽然可能很轻微,但却是对所有用户都会有影响。

采用 Nginx 进行中转:

不采用 Nginx 中转:

如上两张图,可以很明显的看到,如果采用 Nginx 来作为中转并进行分流控制,将导致我们的 CDN 优势失效,所有的流量都可能回到上海的机房,再流转到上海的 CDN,这显然不是我们想看到的。

这也是我们放弃 Nginx+lua+redis 方案的原因。

基于 SSR 做灰度

如果我们的前端页面是通过服务端来进行渲染,可以把灰度控制继承在服务端渲染中,基于不同的用户放回不同的HTML,这样也就可以做到灰度发布。

不过这需要有一套完善的 SSR系统,对于访问量大的产品,维持系统稳定性的难度远大于实现 SSR 本身的技术难度。由于我们是前后端分离,并且没有基于 Node 高可用的运维团队和经验,所以这个方案也就放弃了。

APP拦截灰度

基于APP的方案,是在用户点击H5资源位,创建webview时,拉取灰度配置,如果当前页面有灰度,则拉取灰度配置,判断是否命中灰度,如果命中,替换H5链接即可。

看过我其他文章的朋友,应该有了解到我们针对H5秒开有一套配置下发到APP,那么灰度配置,也可以集成到原有配置中,一并下发给APP,这套方案相对而言也比较简单,但是却有如下问题。

  1. 只能支持APP,APP外和小程序内打开的场景无法支持
  2. 依赖APP,公司其他业务线的APP,如果要使用也需要开发,工作量较大。

所以最后该方案也被排除。

纯前端方案

方案概览

基于如上的一些原因,于是我们采用了一套纯前端的方案,来解决灰度发布问题,虽然这套方案也有一点缺点。前面我们提到灰度发布的本质,其实包含两个方面,一是版本控制,二是分流控制。

版本控制比较好做,我们把全量的HTML代码发布到 index.html 文件,把灰度的HTML代码发布到 gray.html 文件,这样就做到了版本控制。

分流控制,可以被拆分为两部分,一部分只管获取配置、判定是否命中灰度并入在本地,另一部只管读取结果并执行跳转,这样整个系统就解耦了。

方案大体思路是:

  1. 在用户首次方式时,静默激活灰度计算逻辑,通过接口或其他条件判断用户是否命中灰度,把结果存储在 localStorage 中。
  2. 有别于全量版本时使用 index.html,灰度时构建并修改html名称为 gray.html,并发布
  3. 当要灰度发布时,下载 index.html ,注入灰度判断代码到 head 中,注入 GRAY_SWITCH 开关并开启
  4. 当用户再次访问时,执行灰度判断代码,如果命中,重定向到 gray.html 页面
  5. 对获取页面点击的地方,进行封装或拦截,确保灰度用户分享出去的链接,是全量链接

流程图:

时序图如下:

灰度版本控制

对于版本控制,我们通过提供了一个 webpack 插件集成到构建流程中,在构建时生成不同文件名的 html 文件。

通过构建命令参数,来区分各种发布情况

bash 复制代码
npm run build your_project_name -- --gray=open  
# --gray 的值  
# --gray=close 不打开灰度,默认值  
# --gray=open 打开灰度  
# --gray=full 灰度全量  
# --gray=unpublish 撤销灰度

可以分为如下情况:

正式发布

构建时生成:

  • index.html 全量页面
  • index_backup.html 全量备份页面(用来做回归)

灰度发布

构建时生成:

  • gray.html 灰度页面
  • gray_backup.html 灰度备份页(用来在全量后替换 index_backup.html)

同时下载 index.html ,注入灰度重定向控制JS。

重定向控制代码如下:

js 复制代码
// 标记是否打开灰度  
window.__GRAY_SWITCH__ = 1   
let graySwitchName = 'gray_switch_';  
// 获取去除html后的pathname  
const pathname = window.location.pathname.split('/').slice(0, -1).join('/');   
graySwitchName = graySwitchName + pathname  
const graySwitch = localStorage.getItem(graySwitchName)  
if (graySwitch === '1') {  
    const grayUrl = window.location.href.replace('index.html', '_gray.html')  
    if(window.history.replaceState){  
        // 安卓 app 使用 location.replace 无效  
        window.history.replaceState(null, document.title, grayUrl);  
    }else{  
        window.location.replace(grayUrl);  
    }  
} 

修改输出的 HTML 文件名,是通过编写 webpack 的自定义插件来完成。

原理是通过 compiler.hooks.afterEmit.tapAsync 钩子函数,再 "输出" 阶段,对文件名进行修改。

撤销灰度

从云端下载 index_backup.html 重命名为 index.html 放在打包目录,之后再由发布系统上传。

全量发布

从云端下载 gray.htmlgray_backup.html,重命名为 index.htmlindex_backup.html,发布后就会替换原有的全量HTML。

灰度分流控制

分流的重点是如何判断哪些用户能命中灰度。每个项目划分人员的策略都可能不同,比如C端页面更倾向于按useID随机划分。而B端拣货、配送等业务线,更需要按门店来进行划分,这样可以做到同门店员工体验一致,便于管理。所以这块这块必须要足够的灵活性。

我们这里采取了两种方式:

第一种是基于接口来做分流控制:把用户信息传给服务端,接口通过配置的灰度规则,计算是否命中,并返回前端。前端只管把结果存入本地。

第二种是把计算逻辑都放在前端,比较适合C端项目,因为C端项目大部分场景都是随机划分灰度用户。

灰度分流计算的JS代码是在用户每次打开后,静默运行,所以需要引入到业务代码中。

引入的代码如下:

typescript 复制代码
import grayManager from '@cherry/grayManager'  
import { getMemberId } from '../utils/index'  
  
// 伪代码,说明GrayOptions 的类型  
interface GrayOptions {  
    // 灰度比例控制 支持固定值和数组阶梯灰度,配置grayScale 后,grayComputeFn无效  
    grayScale: number | [number]  
    // 自定义灰度方法,在内可以请求接口等  
    grayCompute: () => (() => Promise<boolean>) | boolean  
    // 获取维护标识,比如以 shopId 为灰度标识,该函数就返回当前用户的 shopId  
    getGaryData: () => ()=> Promise<string>,  
    // 配置灰度白名单,白名单内的用户都会命中灰度  
    whiteData: string[]  
}  
  
// 初始化灰度计算逻辑  
grayManagerInit({  
    grayScale: 10,  
    whiteData: ['123', '456']  
})

前端计算分流

随机百分比

多数项目,我们一般使用的策略是随机,比如设置10%的用户命中灰度。

我们可以通过生成随机数来判断是否命中灰度,具体步骤如下:

  1. grayManager.init() 时,随机生成一个 uuid,存在用户本地,不做清除,下次 init 时,先从本地取 uuid,存储 key 命名为 __GRAY_UUID__
  2. 当使用预置灰度计算能力时,取 __GRAY_UUID__ 每位转化为 asci 码并相加,除以100 求余数
  3. 用余数+1 和灰度比例(grayScale)对比,当余数 +1 <= grayScale 时命中灰度

这样可以得到一个近似 10% 比例的灰度用户数。

基于门店和城市分流

如果想基于门店或城市分流,我们只需要配置两个参数, 一是如何获取门店和城市ID

另一个是需要灰度的门店和城市ID

ts 复制代码
import grayManager from '@cherry/grayManager'  
import { getShopId } from '../utils/index'  
  
grayManagerInit({  
    getGaryData: () => {  
        return await getCityId()  
    },  
    whiteData: ['123', '456']  
})

可以通过 grayScale 配置数组来实现,起始时间为打灰度包构建的时间,我们会把构建时间注入到 HTML 中。

其他注意项

开头讲过,这套方案有一点缺点。可能大家也会发现,灰度时用户需要先进入打 HTML,执行 head 中注入的重定向控制JS,对命中灰度的用户再次跳转到 gray.html

这样其实带来了两个问题:一是对灰度用户来说经过了两个HTML,白屏的时间会更长。二是灰度用户访问的URL变化了,如果此时用户把页面分享出去,被分享用户将直接打开灰度页面。

对于第一条,全量用户是不会被影响,只有灰度用户才会白屏更久,我们目前测试白屏的时长还能接受。

对于第二条,我们最初是系统通过 Object.defineProperty 来拦截 对 window.location.pathname 的获取,返回 index.html。但window.location.pathname 是一个只读属性不可拦截。

最后只能提供统一的方法,来获取 pathname

结语

以上就是我们的灰度核心方案,整个方案会比较简单,几乎不依赖外部部门。无论是对于H5还是pcWeb,亦或是不同的容器,都无依赖,各个业务线都可以平滑使用。

相关推荐
Darling02zjh33 分钟前
GUI图形化演示
前端
Channing Lewis35 分钟前
如何判断一个网站后端是用什么语言写的
前端·数据库·python
互联网搬砖老肖1 小时前
Web 架构之状态码全解
前端·架构
showmethetime1 小时前
matlab提取脑电数据的五种频域特征指标数值
前端·人工智能·matlab
码农捻旧1 小时前
解决Mongoose “Cannot overwrite model once compiled“ 错误的完整指南
javascript·数据库·mongodb·node.js·express
淡笑沐白1 小时前
探索Turn.js:打造惊艳的3D翻页效果
javascript·html5·turn.js
sunxunyong2 小时前
yarn任务筛选spark任务,判断内存/CPU使用超过限制任务
javascript·ajax·spark
Ynov2 小时前
详细解释api
javascript·visual studio code
左钦杨2 小时前
IOS CSS3 right transformX 动画卡顿 回弹
前端·ios·css3
NaclarbCSDN2 小时前
Java集合框架
java·开发语言·前端