Web 聊天的图片上传与发送

Sep 4·6 min
AI 生成的摘要
商业 IM 软件 Web 端先准备图片预览,再上传文件、发送 XMPP 消息。记录上传进度与发送状态的区别,以及重试时怎样复用文件引用。

前面写了消息确认和附件下载,再补一下商业 IM 软件 Web 端发送图片的过程。

文字可以直接构造消息,图片要先上传文件,拿到引用以后才能发送聊天消息。上传到了 100% 只是前半段结束,不能在这里就把消息改成发送成功。

文件上传和消息发送,各自有结果

当前实现通过文件服务上传内容,拿到远端引用以后,再通过 XMPP 发送文件消息。

消息中包含文件名、访问引用、类型、大小等信息。图片的文件内容和描述这张图片的聊天消息,走的是不同链路。

可以把几个阶段区分开:

阶段客户端掌握的事实
本地预览准备好当前浏览器能展示本地选择的内容
上传接口返回文件引用文件上传链路拿到了结果
消息得到确认客户端按项目规则确认这条聊天消息已发送
接收端取得文件对方设备完成了自己的访问过程

上传进度只描述其中一段。进度回调到达末尾,也还需要等待上传调用返回,才能知道它是否给出了可以使用的文件引用。

本地预览

sendMediaMessage 会准备文件名,按条件生成缩略图,然后创建状态为 SENDING 的本地消息。

新消息的原始 File 会放进内存映射;有缩略图时,会尝试把缩略图以及原始文件写入 Blob 存储。写缓存失败会记录警告,然后继续后续流程。

这些缓存让当前浏览器可以围绕本地内容展示图片,但不能代替远端上传结果。

还有一个实现顺序值得注意:缩略图生成发生在创建本地消息之前,并不位于后面的主 try 块内。所以当前不能笼统描述为“任何文件一选中,消息立刻出现,所有失败都会更新到同一条消息上”。前置处理的耗时和错误,需要单独观察。

这类顺序对体验很直接。用户看到的“点击以后没反应”,可能还停在本地准备阶段,网络上传甚至还没开始。

上传结束后,消息还在发送中

项目按文件 URL 是否为空决定需不需要上传:

snippet
typescript
const shouldUploadFile = !fileInfo.url || fileInfo.url.trim() === ''

需要上传时,进度回调更新本地消息的 uploadProgress。上传结果有 uri 后,会写入 fileUrl,并清掉上传进度:

snippet
typescript
globalStore.set(updateMessageAtom, {
  id: messageId,
  fileUrl: res.uri,
  uploadProgress: undefined,
})

这里并没有直接设置 SENT。客户端接下来还要检查连接、构造文件消息、发送,再执行消息确认。

因此,“不再显示上传百分比,但消息仍在等待确认”是可以解释的中间状态。UI 如果只剩一个含义模糊的 loading,就很难让用户知道现在究竟在等哪一步。

已经上传过,重试时能不能复用?

当前有不重新上传的分支:传入的文件 URL 已存在时,直接使用这个引用继续构造消息。

它提供了复用上传结果的机会。文件已经成功上传,只是后面的连接或确认失败,就没有必要仅因为用户点了重试而机械地再传一遍文件。

但“不上传”与“复用原来的业务消息身份”是两件事。还要检查这次重试使用的消息 ID、原消息更新和确认目标是否一致。

同样,当前判断的是 URL 字符串存在,并没有在这个分支先验证远端对象是否仍然有效。如果文件被清理或者引用已经失效,字符串非空也不够。

URL 失效时要怎样重新获取,还得看文件服务提供的接口。

两条链路失败时,应该保留什么?

比较有价值的失败组合是下面这些:

情况接下来需要决定的事
本地准备失败是否已创建消息,用户从哪里重试
上传失败是否保留原始 File,下一次还能否上传
上传成功,消息未确认是否保留远端引用,怎样核对旧消息结果
消息已确认,接收端下载失败怎样恢复附件访问,而不重复制造聊天消息

当前媒体发送的异常处理还存在条件分支:主 catch 中对失败状态的更新受 shouldUploadFile 控制。已有 URL 的发送路径如果在确认前抛错,不能仅凭看到了 catch,就认为它与新上传路径一定有完全相同的本地收尾行为。

这里更适合逐个检查失败发生在哪一步,以及已经产生了哪些事实。保留本地文件、保留远端引用、更新失败提示,可以是不同决策。

如果上传成功后消息始终没有发出,远端还可能留下没有关联成功消息的文件。服务端如何回收这类文件,需要另外的实现证据,客户端代码本身说明不了。

预览缓存也不是永久存储

当前浏览器可以从本地 File 或 Blob 显示图片,另一台设备则只能通过消息中的远端引用获取内容。

缓存失效以后,这两条访问路径会产生不同表现:当前发送者之前看得到,并不意味着接收端一定可以下载;刷新以后看不到,也不一定说明消息没有发送成功。

缓存 key、远端文件引用、业务消息 ID 各自表达不同身份。它们需要关联,但不应该被当成同一个字段的不同叫法。

排查图片问题时,最好换一台设备打开。发送端可能一直显示的是本地缓存,单看这一端很容易漏掉远端文件访问失败。

最后修改时间: Sep 15
cd ..