上周我写了一篇有关调整 htmx 以显示即时消息的文章。在使用 HTMX 一周之后,我发觉自己需要更多的功能了。我期望能有更好的办法从服务器流式传输 HTML,并且采用 JSX 组件来代替单纯的 HTML 字符串,以此提升代码的可用性。
温馨提示:倘若你觉得这篇文章有价值,请为它点赞哦!你的支持有助于我创作出更多的内容呢。
我所使用的工具如下:
-
HTMX
-
HTMX Websockets 扩展
-
后端的 Hono
-
客户端的 Websockets
我的想法其实非常简单。我的 Conversation 组件被包裹在一个带有 hx-ext="ws" 的 div 之中,当进行渲染时,它就会与我的后端相连接。
export const Conversation = (props: { messages: Message[] }) => ( <div hx-ext="ws" ws-connect="/chatroom-ws"> <div id="conversation"> {props.messages.reverse().map((message) => ( <div> <UserMessage message={message} /> <AssistantMessage message={message} /> </div> ))} </div> <InputMessageForm /> </div> );
接下来关键的部分是 InputMessageForm。只需要在表单中添加 ws-send,它就会发送一条消息,其中键是 textarea 的 ID(messageInput),值为其内容。
export const InputMessageForm = () => ( <form id="query-submit-form" ws-send className="relative"> <textarea id="messageInput" name="userMessage" placeholder="Type your message here..." rows={4} ></textarea> <button type="submit">Send</button> </form> );
Websockets - 服务器
下面是 Hono 服务器的完整代码块。其中有一些用于打开和关闭连接的控制台日志。onMessage 是进行处理的地方。
get( '/chatroom-ws', upgradeWebSocket((c) => { return { onOpen: () => { console.log('WS Connection open'); }, onClose: () => { console.log('WS Connection closed'); }, onMessage: async (event, ws) => { const { userMessage } = JSON.parse(event.data.toString()); console.log('Got user message', userMessage); const inputArea = await c.html( <div id="query-submit-form"> <InputMessageForm /> </div>, ); ws.send(await inputArea.text()); const htmlUser = await c.html( <div id="conversation" hx-swap-oob="beforeend"> <UserMessage message={{ id: v4(), // some random ids used here for placeholder query: userMessage, completion: '', conversationId: v4(), toolsResponse: null, createdAt: new Date(), updatedAt: new Date(), }} /> </div>, ); ws.send(await htmlUser.text()); const response = await talk(userMessage); const htmlAgent = await c.html( <div id="conversation" hx-swap-oob="beforeend"> <AssistantMessage message={response} /> </div>, ); ws.send(await htmlAgent.text()); }, }; }), );
其流程如下:
-
接收查询
-
发送回空数据以保持整洁。由于没有指定 hx-swap-oob,所以默认为 True。这就意味着它会找到 ID 为 query-submit-form 的元素并进行替换。
-
发送带有用户消息的组件。这里 hx-swap-oob 指定为 beforeend,也就意味着它会被添加到现有消息中。
-
talk → 这里是你的逻辑部分。因为我在与 AI 助手对话,所以进行了一些外部 API 调用。
-
发送带有助手回答的组件。与步骤 3 相同,但组件不同。
我发现的问题:
在发送响应回去时存在一些问题,原因是文档不太容易理解。我觉得有个问题被创建出来是为了解决这个问题的,那就是改进 websocket 扩展的文档。这对我帮助很大!
最为重要的是:
你需要发送回一个字符串,该字符串解析后要具有与要替换的元素相同 ID 的 HTML!
问题 1:
我不小心发送了这样的内容:
JSON.stringify('<div id="someid">test 123</div>') // '"<div id=\\"someid\\">test 123</div>"'
这是错误的哦。要注意 ID 和转义字符!不要在这里将字符串字符串化。
问题 2:
你可能认为可以返回某些内容,然后它就会在你想要的位置进行替换。但实际情况不完全是这样。第一个 div 只是 HTMX 关于要做什么的信息。至少我是这么理解的。
我返回的 HTML 如下:
<div id="conversation" hx-swap-oob="beforeend"> <AssistantMessage message={response} /> </div>
它仅仅被追加到客户端的现有 div 中。