美文网首页
用户体验优化:商品局部刷新策略与实现

用户体验优化:商品局部刷新策略与实现

作者: OrangeEvan | 来源:发表于2017-07-29 15:07 被阅读48次

    一、需求描述

    在快速变动的电商类APP中需要刷新商品信息尤其是数据,以保证商品信息的准确性。在保证数据刷新的同时,因用户的阅览习惯,在刷新的数据时还需要保证浏览位置不变动。因此,我们决定讨论如何能更用户更友好的进行局部刷新。

    让我们分析先来这一类商品的共同点与要求:

    • 商品一般为快速消费: 变动速度快.商品信息进度往往在几分钟内变动数次甚至下架。
    • 商品信息变化大:及时性要求高,需要对用户做出快速反馈
    • 一般收到受到客观限制:商品打折时间,库存等物理或活动上的限制
    • 刷新不能过于频繁,要权衡服务器压力
    淘抢购界面(场景例图)

    因此,我们需要平衡刷新的时效性与流量限制,抉择刷新策略和机制。

    一、刷新机制的策略讨论

    经过一些方案与谈论,我们有了初始的两种策略。
    基于两种游标的刷新策略:


    刷新策略
    1.页标游标刷新 (左)

    使用页标与游标刷新:以当前页面为页标为记录,接入上拉与下拉加载功能。用以保证上拉与下拉后的数据为最新。

    优势:能保证数据始终最新,能获得最新的商品信息。时效性及时。
    劣势:处理逻辑复杂,需要加上拉加载功能并和上拉刷新兼容,工作量巨大。需要处理因数据页码变动产生的商品重复。本屏刷新策略与上下屏不一致,逻辑判断情况多。

    2.唯一标识刷新 (右)

    使用唯一标识刷新:保持已有的原数据与商品列表,通过唯一标识,只抓取商品的最新数据。进行时间,库存的刷新。(新数据的同步和抓取交于另一个定时器,数据刷新只负责取最新数据)

    优势:最小化原则,职能单一,逻辑实现简单。要求简单,单个或多个键值唯一确定一个商品数据即可。最大的优势是用户体验好,感知度较低。既能实时刷新数据,又不会影响使用的直观性。因数据不会变动,位置回滚操作也不需要额外的工作。
    劣势:不能即时的获取新上架的商品信息。如果商品为组合类商品,不容易通过唯一标示查找不能使用。请求次数较零散与频繁。且若该商品已下架需要通过标识告诉用户结果,以免误认为发生bug。

    对比了以上两种方案,我们最终选择了处理逻辑统一,用户使用不太突兀的方案(2),并对初始方案进行了优化,以减少请求量,平衡服务器压力。因刷新场景的常见性,我们做成了SDK的形式希望在能多个项目中使用。

    二、缓存策略

    触发流程:

    • 刷新机制: 用户滑动停止触发
      • 获取刷新id数组与位置(IndexPath)
      • 过滤数据,返回需要刷新的数组id
      • 通过代理返回id数组及位置等信息
    • -> 控制器发送请求
      • -> 刷新数据更新时间

    方案文案如下:

    缓存策略

    三、系统优化

    1.处理用户拖动误触发,进行函数防抖

        // 请求,是否对过滤数据
        private func pullShopItemsData(filter: Bool = false){
            // 0.5秒函数防抖, 合并请求再发送
            self.timer?.invalidate()
            self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(refreshOperaction(timer:)), userInfo: ["refreshRightNow":filter], repeats: false)
        }
    

    2.预测用户浏览轨迹,进行页面上下半屏预刷新

        if (maxRow != -1 && minRow != listData.count+1) {
            // 上下两屏幕
            NSInteger rest = (10 - refreshIdList.count) > 0 ?(10 - refreshIdList.count):0;
            NSInteger beforeNum = floor(rest/2.0);
            // 前半屏
            NSInteger startNum = (minRow - beforeNum>0?minRow - beforeNum:0);
            for (NSInteger i = startNum; i<minRow; i++) {
                NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
                ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
                [refreshIdList addObject:model];
            }
            // 后半屏
            NSInteger afterNum = rest - beforeNum;
            NSInteger endNum = maxRow+afterNum < listData.count-1 ? maxRow+afterNum : listData.count-1;
            
            for (NSInteger i = maxRow+1; i<=endNum; i++) {
                NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:4];
                ShopItemsDataSourceModel *model =  [[ShopItemsDataSourceModel alloc] initWithSourceData:listData[i] indexPath:path];
                [refreshIdList addObject:model];
            }
        
        }
        return refreshIdList;
    

    3.减少请求数方差,使用旧数据填充

    保证及时性,发送请求前进行最小请求量合并,进行节流。

        // 请求数不足,填充老数据
        private func itemsWithFillOldData(originalRefresItems: RefreshItemsInfoStruct, toNum minNum: Int) -> RefreshItemsInfoStruct {
            var filledItems: RefreshItemsInfoStruct = originalRefresItems
            let minRequestNum = minNum - originalRefresItems.count;
            let sortedArray = self.refreshInfoDict
                .filter({return !originalRefresItems.keys.contains($0.key)}) // 除去已确定要刷新元素
                .sorted(by: {$0.value.lastRefreshStamp < $1.value.lastRefreshStamp}) // 根据刷新时间排序
            
            let maxIndex = minRequestNum < (sortedArray.count)
                ? minRequestNum - 1
                : (sortedArray.count) - 1
            
            if ((sortedArray.count) > 0) {
                // 切割数组
                let sortedArray = sortedArray[0...maxIndex]
                for infoTuple in sortedArray {
                    filledItems.updateValue(infoTuple.value, forKey: infoTuple.key)
                }
            }
            return filledItems;
        }
    

    4.定时器周期性刷新

    四、SDK 类图

    刷新机制类图.png

    使用代理模式, 传入想要刷新的cell信息,代理者只负责判断是否数据过期,返回需要刷新的列表,具体刷新操作由使用者去做。

    五、接入说明

    1.初始化代理,遵循 ShopItemsRefreshProxyProtocol 协议
    // 初始化商品刷新代理
    self.proxy = [[ShopItemsRefreshProxy alloc] initWithRefreshMaster:self];
    
    2.实现两个 数据来源 与 请求操作 两个代理方法
    // 数据来源
    - (NSArray<ShopItemsPrepareInfoModel *> *)shopItemsPrepareToRefresh{
        NSMutableArray<ShopItemsPrepareInfoModel *> *refreshIdList = [NSMutableArray array];
        refreshIdList = //视野内你能见到的cell
        return refreshIdList; // 传入所有你想刷新的cell,是否该刷新由代理决定
    }
     
    // 数据操作
    - (void)shopItemsRefreshOperation:(ShopItemsRefreshProxy *)refreshProxy
                       itemsToRefresh:(NSDictionary<NSString *,ShopItemsInfoModel *> *)itemsToRefresh
                      allItemsInfoDic:(NSDictionary<NSString *,ShopItemsInfoModel *> *)allItemsInfoDic {
        __weak typeof(self) weakSelf = self;
         
        [self showHomeLoadView]; // 刷新动画
    // 发送请求服务
        [refreshItemsService refreshHomeCellWithRefreshIdDic:itemsToRefresh
                                              completeHandle:^(NSDictionary<NSString *,id> * _Nullable respond, NSError * _Nullable error) {
            // ...请求成功,刷新数据源
            [refreshProxy updateRefreshTimeWithItems:refreshItems];// 刷新数据的跟新时间
            [self hideHomeLoadView];// 停止刷新动画
        }
    }
    

    3.进行刷新

    // 在拖拽结束方法进行调用
    - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
        if (self.proxy) {
            [self.proxy pullIntelligent]; // 进行刷新
        }
    }
    
    注: 商品刷新SDK 现因仅在我们的应用中使用,暂未集成到cocoapods。如果有类似的使用需求,在评论区@我即可。我会后续集成进去。

    相关文章

      网友评论

          本文标题:用户体验优化:商品局部刷新策略与实现

          本文链接:https://www.haomeiwen.com/subject/jhcclxtx.html