发现自己在复制粘贴同一坨流式逻辑,恶心了,停下来抽了个 hook。这篇讲讲我怎么收口的。
重复在哪
三个端不一样的其实只有两层:UI 长什么样、消息存哪。一样的东西多得多------发消息、读 SSE 流、维护"正在生成"状态、错误处理、中断、重发。这部分在三个端里我几乎一字不差地抄了三遍。该抽了。
抽出来的 useChat
我把所有跟 UI 无关的逻辑收进一个 useChat,只暴露状态和方法,长啥样交给各端自己:
javascript
function useChat({ send, store }) {
const messages = ref([])
const isStreaming = ref(false)
async function ask(text) {
messages.value.push({ role: 'user', content: text })
const reply = reactive({ role: 'assistant', content: '' })
messages.value.push(reply)
isStreaming.value = true
try {
await send(messages.value, (delta) => {
reply.content += delta // 流式追加
})
} finally {
isStreaming.value = false
store?.save(messages.value) // 存储交给外部
}
}
return { messages, isStreaming, ask, stop, retry }
}
关键是两个依赖注入:
-
send:怎么发请求、怎么读流,由各端传。Web 传 fetch 版,webview 传走 bridge 的版本。 -
store:存哪,由各端传。H5 传 IndexedDB,后台传调接口的。
hook 本身不关心这俩怎么实现,只管编排流程。三个端共用同一份 ask / stop / retry 逻辑,UI 各画各的。
抽完的收益
-
流式那段最容易出 bug 的代码,从此只有一份,改一处三端都好。
-
新接一个端,只要实现
send和store两个函数,半天接完。 -
测试也省,核心逻辑对着 hook 测一遍就够。
踩到的坑和取舍
坑:响应式系统不统一。 后台是 Vue,有个端是 React。useChat 没法真正一份代码跑两个框架,我最后是抽了一份框架无关的"纯逻辑 class",外面再包一层各自的 hook 适配响应式。多了一层包装,但核心逻辑确实只有一份。
取舍: 依赖注入让 hook 很灵活,但也意味着调用方要懂得多------传错 send 的流式协议照样翻车。灵活和易用这里我偏了灵活,新人接手有点门槛。
模型统一走讯飞 MaaS,三端调同一个现成接口,连后端都不用各搭一套。你们多端复用逻辑都怎么抽?评论区交流。