复杂消息系统最先失控的,往往不是接口,而是消息类型越来越多

6 阅读7分钟

很多人做聊天功能,前期最容易盯着接口和发送流程看,但我这几个月在 Flutter 项目里做消息系统,最深的感受反而是:真正让系统开始失控的,往往不是“能不能把消息发出去”,而是消息类型越来越多以后,列表、渲染、重试、动效和业务扩展开始慢慢挤在一起。

以前我一提到聊天系统,脑子里先想到的通常都是接口。

比如:

  • 拉历史消息
  • 发文本
  • 发图片
  • 收消息

如果这些都通了,心里很容易会有一种感觉:

这套聊天系统大体就算立住了。

但我后来在 Flutter 项目里持续做了一段时间消息模块之后,越来越不这么看了。

因为消息系统真正开始变重,往往不是接口突然变复杂,而是另一件更容易被低估的事:

消息类型开始越来越多。

一开始只有文本和图片时,很多代码还能靠一个页面往下扛。

可一旦你陆续接进来这些东西:

  • 论坛消息
  • 游戏消息
  • 小程序消息
  • 表情包
  • 礼物
  • 失败重发
  • 系统通知

聊天页面的性质就会变。

它不再只是“显示一串消息”,而是在承接一个越来越复杂的业务消息系统。

1. 消息类型一多,最先出问题的往往不是接口,而是页面开始越来越像巨型调度中心

这类问题我现在回头看,特别典型。

因为前期你很容易觉得:

  • 新增一种消息
  • 多写一个消息组件
  • 多补一段类型判断

不是什么大事。

但这种做法只要持续几轮,页面就会慢慢变成一个巨大的消息调度中心。

像项目里的聊天页,真正进入渲染时,已经不是简单判断文本和图片了,而是要分很多层:

if (message.contentType == MessageType.custom) {
  switch (message.customElem?.extension) {
    case OpenImTool.forumCustomMessage:
      return MessageChatForumItem(...);
    case OpenImTool.gameEvaluateCustomMessage:
      return MessageChatGameEvaluateItem(...);
    case OpenImTool.postCustomMessage:
      return MessageChatPostItem(...);
    case OpenImTool.gameCustomMessage:
      return MessageChatGameItem(...);
    case OpenImTool.miniAppCustomMessage:
      return MessageChatMiniAppItem(...);
    case OpenImTool.stickerCustomMessage:
      return MessageChatStickerItem(...);
  }
}

这还只是“能不能把它画出来”这一层。

如果再加上:

  • 礼物动画
  • 失败重发
  • 时间分组
  • 未读状态
  • 不同消息的操作能力

一个聊天页很快就会比很多人预想得重得多。

2. 真正难的不是多一个类型,而是每多一种消息,系统都要跟着长一层责任

我后来越来越确定,消息系统这件事最麻烦的地方,不在于“又来了一种新类型”。

而在于,每新增一种类型,后面常常不是只多一个组件,而是整条链路都要跟着加责任。

比如一个表情包消息,表面上像是:

  • 换个样式显示

但实际上它背后要解决的是:

  • 用户有没有解锁
  • 资源有没有缓存
  • 发送用哪个地址
  • 失败后怎么重发

项目里专门维护了这些缓存:

final Map<int, bool> _stickerOwnedMap = <int, bool>{};
final Map<int, String> _stickerSendUrlMap = <int, String>{};

而礼物消息也不是“展示一张图”就结束了。

你还要考虑:

  • 收到时要不要播动效
  • 多个礼物来了怎么排队
  • 图和 PAG 动效是不是同步
  • 是互动礼物还是普通礼物

这就是为什么消息类型一多以后,系统复杂度会突然上来。

因为你加进去的不是一个外观,而是一层新的系统责任。

3. 如果前面只把消息当显示问题,后面一定会越来越难维护

这件事我在项目里最大的感受,就是:

很多消息系统一开始写乱,不是因为开发者不会拆分,而是因为太容易先把它当成“显示问题”。

前期大家最自然的动作通常是:

  • 后端来了新结构
  • 先把它显示出来
  • 交互有要求再补

这在节奏紧的时候完全可以理解。

但问题是,消息系统和普通列表不一样。

因为它的复杂度不是线性增长的。

普通列表你多一种卡片,页面可能只是多一个组件。

消息系统多一种类型,常常会连带影响:

  • 列表渲染
  • 发送逻辑
  • 失败态
  • 未读态
  • 顶部通知
  • 动效
  • 输入面板
  • 删除和重发

也就是说,如果前期只是“先显示出来”,后面类型一多,页面就会开始越来越重,越来越难改。

4. 这次项目里,真正让我觉得它已经不是“聊天页”了,是这些细节同时出现

如果只看单个功能点,很多改动都不算特别吓人。

但放在一起看,你会明显感觉到这已经不是一个简单聊天页了。

比如:

  • 会话列表要做分页拉取和容错
  • 系统通知要单独成一类会话
  • 顶部横幅要根据未读和路由状态决定是否展示
  • 聊天面板要区分表情面板和更多面板
  • 表情包要支持缓存、长按预览和未解锁购买
  • 礼物消息要支持实时动效和队列播放
  • 失败消息要支持重发

像页面里,失败重发就已经不是简单弹个提示,而是把能力下沉到具体消息组件:

MessageChatStickerItem(
  message: message,
  resendMessage: (Message failedMessage) {
    controller.resendMessage(failedMessage);
  },
)

这件事很说明问题。

因为这代表聊天系统已经不是“一个统一列表”,而是每种消息都在往外长自己的行为。

一旦到了这一步,如果结构没开始主动分层,后面一定会越来越难收。

5. 我后来越来越觉得,聊天系统真正的分水岭,是你有没有把“消息类型扩张”当成核心问题

现在回头看,我觉得很多聊天系统真正的分水岭,不是:

  • 能不能发消息
  • 能不能收消息

而是:

你有没有意识到,消息类型扩张本身就是系统设计问题。

如果意识不到,后面的典型结果通常是:

  • 一段类型判断越来越长
  • 一个聊天页面承担的责任越来越重
  • 一个问题修完又牵出别的问题
  • 新加一种消息,改动范围越来越大

反过来,如果早点承认“消息类型会持续增加”,你做事的方式就会变成:

  • 新类型尽量独立成自己的消息组件
  • 通用能力尽量往上抽
  • 列表负责调度,不负责吃掉所有业务判断
  • 扩展时尽量不让旧逻辑继续变脆

我现在会觉得,这个视角比“聊天页怎么写”重要得多。

因为真正会把系统拖垮的,通常不是某一个 bug,而是:

消息类型一直在长,但结构一直没跟着长。

6. 这件事对我最大的提醒,不是“接口多复杂”,而是“业务消息一多,聊天系统其实已经在往平台走”

以前我会把聊天系统理解得比较窄。

它像是项目里的一个功能模块。

但做久了以后,我越来越觉得,复杂聊天系统更像项目里的一个平台:

  • 业务消息从这里进
  • 用户互动从这里走
  • 状态通知从这里露出
  • 表情、礼物、系统消息都在这里挂载

这时候你如果还把它按“一个普通页面”去管,后面一定会很难受。

所以如果现在让我用一句话总结这几个月的感受,我会说:

复杂消息系统最先失控的,往往不是接口,而是消息类型越来越多。

而真正成熟的做法,也不是等它乱了再补,而是尽早承认:

它迟早会不断长新类型。
你最好一开始就按“它会继续长”这件事来设计结构。