一种纯前端的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,亦或是不同的容器,都无依赖,各个业务线都可以平滑使用。

相关推荐
杨荧15 分钟前
【开源免费】基于Vue和SpringBoot的实习管理系统(附论文)
java·前端·javascript·vue.js·spring boot·spring cloud·java-ee
软件小伟17 分钟前
Element UI如何实现按需导入--Vue3篇
前端·javascript·ui·elementui·vue
明川1 小时前
Android 性能优化:内存优化(理论篇)
android·前端·性能优化
liulanba1 小时前
Kotlin的data class
前端·微信·kotlin
战族狼魂2 小时前
淘宝客结合C#使用WebApi和css绘制商品图片
前端·css·c#
活宝小娜2 小时前
vue项目使用element-ui中的radio,切换radio时报错: Blocked aria-hidden
前端·vue.js·ui
PasteSpider2 小时前
贴代码框架PasteForm特性介绍之datetime,daterange
前端·html·.netcore·crud
跑得动2 小时前
uni-ui自动化导入
前端·ui·自动化
JackieDYH2 小时前
element-plus如何修改内部样式而不影响vue其他组件的样式
前端·javascript·vue.js
Micheal_Wayne2 小时前
“无关紧要”的小知识点:“xx Packages Are Looking for Funding”——npm fund命令及运行机制
前端·npm·node.js