赞
踩
由于工作中 IM 原有设计对于新增聊天场景的支持性不佳、拓展困难等一系列原因,团队计划发起一次 IM 重构,这次重构由我来主导,计划在新的聊天业务中采用新的设计方案来开发,后期再逐步替代现有方案。这次把新方案的工作内容整理了整理,分享给大家,多有疏漏,敬请指正。
本文主要介绍以下几个部分:
下面会介绍当前项目中聊天部分的一些基本工作原理
消息的发送是基于 HTTP 请求,主要包含两种类型:一种是简单文本类型、一种是多媒体类型的消息。
消息的接收是基于 WebSocket 长连接和 HTTP 请求相互配合来实现的。WebSocket 是 App 内用于服务端向客户端发送通知消息的单向通讯服务工具
消息的存储底层采用 sqlite 和第三方 FMDB 开源框架,再此基础之上开发了一套支持 ORM 以及 SQL 语句生成工具的基础库。每个支持数据库保存的 Model 类内部保存了具体表名、不同字段与表字段的映射,进行数据库操作的时候可以根据这些信息生成对应的 SQL 语句来保存到本地数据库。本地消息的保存就是直接通过框架保存到数据库即可,网络部分是请求到数据后,通过前后端约定的 envelop key 确定到具体数据 Model 的类,从而根据 ORM 信息来保存到数据库,大概就介绍到这里吧,不再做过多的介绍了。
重构目标主要是为了减少因产品需求增加而带来维护成本的急剧上升。软件的设计不仅需要遵守软件SOLID 原则,还要结合实际的应用场景,下面会介绍项目中聊天部分的应用场景,以及对应的设计目标。
当前 App 随着产品迭代,聊天的场景逐渐增多。最早只有单聊的场景,到后来新增了群聊,后来较短的时间内又多出了几种类型的临时聊天场景,后来两个月内又新增一种新的群聊场景,并且需要支持主题配置。
这些不同的聊天场景之间,消息发送、接收等逻辑一致,列表中相同类型的消息样式相同。但是,不同业务场景所支持的消息类型、主题样式、业务功能均有不同。
比如,某些聊天场景中根据不同的业务逻辑,发送消息达到一定数量后,就需要本地插入某种特殊类型的提示消息;而其他的业务可能会有在列表的头部展示某种特定样式的卡片;还有一些聊天详情在某些时间没有人发言则需要关闭聊天,并变更 UI。
当前已有的设计起源与单聊,所支持的可扩展方向是支持新增各种消息卡片以及样式,但是对于新增聊天场景难以支持,比如后续的群聊是在单聊的基础上拷贝了大量代码,其通用的部分基本上是复制了一份份代码。随着新的聊天场景越来越多,会存在大量的冗余代码,维护成本几乎指数级别的上升。
所以,这次我们就需要把多种聊天的基础部分抽取出来,来供各种不同的聊条场景来使用。其中聊天列表和聊天通用逻辑,可以方便的迁移到不同的聊天业务场景,使消息列表部分以及发送逻辑与具体业务逻辑进行解耦,实现接入方只关注自身的业务逻辑,不需要关注聊天系统内部通用逻辑。
系统中子模块主要包括四个部分,分别是基础消息列表子模块(CoreMessageList)、聊天消息管理工具(ChatContext)、键盘输入模块(ChatKeyboard),下面做分别介绍在聊天模块中扮演的职责,及其与其他模块的关系,下面做详细介绍:
该模块主要提供消息 UI 的展示、支持配置自定义 UI 样式以及事件回调。该模块依赖于聊天消息管理工具(ChatContext),ChatContext 会通过回调通知 CoreMessageList 渲染页面,与其他模块没有直接关联。
方向 | 介绍 |
---|---|
方便导入 | 对外提供统一头文件 |
功能完备 | 包含通用业务功能的完整实现,初始化后,无需任何其他配置,聊天相关功能不需要业务额外实现,即可实现消息列表相关独立的基础功能 |
基础功能可配 | 固定配置可通过传入初始化参数,动态配置可通过代理实时问询业务是否支持 |
配置信息创建简便 | 通过 builder 生成,支持链式语法,增减字段不需要修改接口,所有配置信息只读,避免误改 |
代理事件信息完整 | 代理方法命名通用化,且需要包含时机以及必要数据 |
主题生成方便 | 传入符合要求配置文件的路径即可生成主题模型 |
功能点 | 介绍 |
---|---|
长按消息菜单 | 通过代理来禁止某默认功能、添加自定义功能项,也可以改变顺序 |
自定义列表数据 | 用户可以在消息列表中插入自定义 UI 卡片(例如个人介绍、配对提示等等) |
自定义消息卡片 UI 样式 | 通过代理方式,传入任意消息类型的自定义消息卡片的样式 |
默认消息卡片 UI 元素配置 | 对只改动消息卡片中部分元素提供支持,比如昵称后增加 Tag、模糊头像、改变字体颜色、消息气泡背景颜色等 |
埋点 | 可根据不同消息,传入埋点参数 |
主题 | 可根据需要传入不同颜色的主题,不传入主题的情况下,支持日夜间模式 |
设计方式 | 介绍 |
---|---|
数据驱动 | 列表中消息的展示、删除、更新等均由数据驱动,列表只根据实际数据变更来渲染页面 |
单向数据流 | 驱动数据流要保持单向,列表内部不会反向修改任何数据源,保持数据逻辑清晰 |
数据源明确 | 数据源包括消息数据与自定义数据,分别由 ChatContext 和业务提供,列表正确展示数据源内容即可 |
内聚呈现逻辑 | 数据组装和驱动逻辑由 CoreMessageListPresenter 呈现器结合 IGListKit 实现,集中将数据呈现到列表 |
简化层级结构 | CoreMessageList 内部代理、列表、工具类等之间交互采用中介者模式,由网状结构改为星型结构,简化事件传递和交互层级 |
隐藏细节 | 通过提供 Adapter 适配器向业务提供标准操作函数,不暴露内部结构,减少滥用风险 |
要点 | 介绍 |
---|---|
代理事件的时机 | 包含 will、did 描述 |
代理事件的控制 | 包含 should,且有 return Bool 类型 |
命名 | 要描述事件而不能描述业务,比如长按头像就应该命名为 longPressAvatar,而不是 atUser |
类聚 | 可分类型的事件不能单独作为一个方法,比如点击了提示消息的「重新编辑」,这属于一种提示类型的事件,需要用提示事件做为枚举,通过一个统一的方法回调处理,防止出现每增加一个提示类型就要增加一个方法 |
内部操作业务可控制 | 消息操作部分:重新发送、撤回、删除,等事件需要通过代理方法由业务控制,事件完成需要回调给业务 |
该负责维护管理消息发送、接收、撤回、删除等等操作,以及消息数据维护,数据变更的事件回调,CoreMessageList 部分会根据该数据变更来驱动列表的 UI 变更。给 ChatKeyboard 模块提供发送接口,给 CoreMessageList 内部的功能菜单提供撤回、删除、重试等功能提供对应的功能接口。
该模块主要分为四个子模块,消息数据检查模块、消息发送模块、消息接收模块、数据管理模块。其中消息发送和接收部分,根据文中前半部分介绍的基本工作原理进行设计开发。数据管理模块,可以根据消息数据变更的特性对算法进行针对性优化,该数据模块与消息 UI 列表中的 Item 数据模型算法保持一致:
特点 | 描述 |
---|---|
功能完备 | 包含聊天功能的环境变量、以及数据逻辑部分的完整实现 |
与 UI 无关 | 只包含数据以及逻辑,与 UI 完全无关,UI 部分可根据数据变更回调来更新页面 |
一对多 | 存在形式是一对多,支持同一聊天对应的 UI 页面存在多个,通过弱引用等方式自管理生命周期 |
接口通用 | 比如,不同的聊天场景请求发送接口会携带业务参数,接口设计需要在增删业务参数时,接口不需要变更 |
简单易用 | 接口参数定义明确,配置信息生成简便可拓展,与 CoreMessageList 配置信息参数设计思路一致 |
针对消息数据的顺序确定性,以及增量新消息数量一般数量比较少等特性。
需要提供的功能主要有添加新的消息、加载历史消息、删除和更新。
在添加新消息前,数据要提前排序,拼接到数据列表中时采用如下图的方式:
负责输入框、键盘以及内部表情菜单等 UI 元素,维护通用功能的管理和配置功能,提供用户事件以及键盘 UI 变动的回调,可支持主题配置。提供 UI 更新事件回调给 CoreMessageList 用以修改消息列表底部的空间、滚动到底部等。点击发送按钮、选择完图片、视频、表情后会调用 ChatContext 对应的发送消息接口。简述如下表:
组成部分 | 输入框、系统键盘、 emoji 表情、大表情、以及视频、语音等工具栏功能 |
依赖模块 | 图片视频选择器、emoji 以及表情包管理工具、音视频录制工具等 |
职责原则 | 数据采集与传递,不做加工与处理 |
提供配置功能 | 主题、支持的表情类型、支持输入的类型 |
对于详细介绍的部分,由于篇幅的原因,这里会详细介绍 CoreMessageList 模块部分,会通过 UML 类图、接口设计以及设计思路来进行详细介绍
设计思路:该部分暴露了消息列表视图控制器的对外功能,保持保持接口功能简捷易用
/// 聊天消息列表视图控制器协议 @protocol XXXCoreMessageListController <NSObject> /// 列表事件代理 @property (nonatomic, weak) id<XXXCoreMessagesListDelegate> listDelegate; /// 长按菜单代理 @property (nonatomic, weak) id<XXXCoreMessageCellMenuDelegate> cellMenuDelegate; /// 埋点信息代理 @property (nonatomic, weak) id<XXXCoreMessageListTrackDelegate> trackDelegate; /// 列表相关功能适配器 @property (nonatomic, readonly) id<XXXCoreMessageListAdaptor> listAdaptor; /// 把列表视图控制的 View 添加到 container,并设置为 container 的子视图控制器 - (void)addViewToContainer:(UIViewController *)container; @end
该代理接口主要是处理长按消息的菜单处理
设计思路:根据展示时机包含 shouldShow、willShow、didShow 三个方法,willShow 时可以设置或者添加自定义数据
/// 长按消息弹窗功能 @protocol XXXCoreMessageCellMenuDelegate <NSObject> @optional /// 长按对应消息气泡,是否尝试展示对应的菜单类型 /// 如果想要禁止某种类型,则 return NO /// @param menuType 菜单类型 /// @param message 长按气泡对应的消息 - (BOOL)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList shouldTryShowMenu:(XXXMessageActionType)menuType message:(XXXMessage *)message; /// 将要弹窗消息菜单 /// 可根据业务需要添加自己的 Action,改变顺序等 /// @param actions 默认的 Action,可通过 type 区分类型 /// return 符合自己业务需求的 Action 数组 - (NSArray<XXXMessageAction *>*)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList willShowActions:(NSArray<XXXMessageAction *>*)actions forMessage:(XXXMessage *)message; /// 已经展示消息菜单 - (void)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList didShowedCellMenuWithActions:(NSArray<XXXMessageAction *>*)actions; @end
XXXCoreMessagesListDelegate 列表相关的总代理,主要包含三个子协议:
/// 列表自定义数据数代理 @protocol XXXCoreMessagesListCustomDataDelegate <NSObject> @optional /// 消息列表顶部添加自定义的 CellModel - (NSArray<XXXCoreMessageListBaseCellModel *> *)customPrependCellModelsForCoreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList; /// 根据自己在列表顶部添加的自定义 CellModel,返回对应的卡片 /// @param cellModel 通过代理传入的自定义 CellModel - (IGListSectionController *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList sectionControllerForCustomPrependCellModel:(XXXCoreMessageListBaseCellModel *)cellModel; /// 列表对于不支持的消息类型,允许业务方自定义文案,待定 - (NSString *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList customTipsForUndefineMessage:(XXXMessage *)message; /// 对于不同消息增加的自定义配置信息 - (XXXCoreMessageListCellConfig *)coreMessageList:(UIViewController <XXXCoreMessageListController>*)coreMessageList cellConfigForMessage:(XXXMessage *)message; @end
/// Cell 相关事件代理 @protocol PUGCoreMessagesListCellDelegate <NSObject> @optional /// 点击头像,return NO 或者不实现默认跳转 profile 页 - (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList tackleTapAvatar:(PUGUser *)avatarUser message:(PUGMessage *)message; /// 长按头像 - (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList didLongPressAvatar:(XXXUser *)avatarUser message:(XXXMessage *)message; /// 点击内容 - (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList didTapContentWithMessage:(XXXMessage *)message; /// 点击富文本链接, 如果可以处理,return YES,否则 return NO - (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList handleTapLink:(NSString *)link messsage:(XXXMessage *)message; /// 点击了提示消息事件 - (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList didTapTipAction:(XXXCoreMessageListCellModelTipAction)action message:(XXXMessage *)message; @end
// 列表滚动事件代理
@protocol PUGCoreMessagesListScrolledDeletgate <NSObject>
@optional
/// 列表滚动
- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewWillBeginDragging:(nonnull UIScrollView *)scrollView;
- (void)coreMessageList:(UIViewController<PUGCoreMessageListController> *)coreMessageList scrollViewDidScroll:(nonnull UIScrollView *)scrollView;
@end
/// 消息列表相关功能代理事件 @protocol XXXCoreMessagesListDelegate <XXXCoreMessagesListScrolledDeletgate, XXXCoreMessagesListCustomDataDelegate, XXXCoreMessagesListCellDelegate> @optional /// 是否重试发送失败的消息 - (BOOL)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList shouldRetrySendMessage:(XXXMessage *)message; /// 成功操作消息 - (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList successOperate:(XXXCoreMessagesListMessageOperate)operate message:(XXXMessage *)message responseObject:(id)responseObject; /// 操作消息失败 - (void)coreMessageList:(UIViewController<XXXCoreMessageListController> *)coreMessageList failToOperate:(XXXCoreMessagesListMessageOperate)operate message:(XXXMessage *)message error:(XXXError *)error; /// 收到新的消息 - (void)coreMessageListDidReceivedNewMessages:(UIViewController <XXXCoreMessageListController>*)coreMessageList isFirstPage:(BOOL)isFirstPage; /// 点击了消息列表页面 - (void)didTapCoreMessageListEmptyArea:(UIViewController<XXXCoreMessageListController> *)coreMessageList; @end
对于这个全新的设计方案,其基础功能目前还不能完全满足现有的业务,基础功能也需要继续完善,为了保证新设计方案不阻塞业务,会优先在新的聊天场景中使用新的聊天框架,在接入新聊天场景的过程中来逐步完善基础功能。后期如果对单聊或者老群聊有较大的改版时,也可以考虑直接接入新的框架去重新实现。
以上所述,是问目前从事的项目聊天模块架构的一个新的设计方案,希望能够给同行提供一定的思路,也希望能够得到大佬们的指导建议。最后祝愿同行的朋友能够身体健康、工作顺心。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。