提问背景 Question:
难道不该用类似数组,然后在模板中循环,模拟之前的聊天过程?
如果没有聊天记录,就显示默认的内容。为什么要用json?
Go模板循环与JS渲染对比
💡 这是一个非常专业且切中要害的问题!
您说得完全对,最符合直觉、也是最传统的做法,就是在 Go 的 HTML 模板中使用 {{range .History}} 循环直接输出 HTML。
然而,在您的特定场景下,不直接使用 Go 模板循环输出,而是推荐注入 JSON 并用 JS 渲染,核心原因在于:客户端 Markdown 解析与动态交互绑定(如复制代码块按钮)。
我们可以对比以下两种方案的实现细节,您可以根据偏好自由选择:
方案一:使用 Go 模板直接循环(传统的后端渲染)
如果在 Go 后端进行 {{range}} 循环,您的代码会像这样:
<div id="chat-window">
{{if .History}}
{{range .History}}
<div class="message {{.Sender}}">
<!-- 🚨 问题:这里的内容是原始 Markdown,前端无法直接正常显示 -->
{{.Text}}
</div>
{{end}}
{{else}}
<!-- 默认的欢迎气泡 -->
<div class="message ai" style="border-left: 4px solid #38bdf8;">
<!-- 欢迎词 Markdown -->
</div>
{{end}}
<div id="bottom-spacer"></div>
</div>
⚠️ 采用此方案必须解决的两大痛点:
- Markdown 解析失效:Go 输出的纯文本含有
###、**或代码块,浏览器无法自动解析。您必须在页面DOMContentLoaded时,用 JS 遍历所有.message,提取里面的文本,通过marked.parse解析后再写回innerHTML。 - 代码复制按钮等交互失效:您在
appendMessage()里为每个代码块动态创建了包裹层<div class="code-block-wrapper">和[复制]按钮,并绑定了点击事件。如果直接用 Go 循环生成 HTML,这些动态生成的复制按钮和通道标识将统统丢失,除非您把这套复杂的 DOM 遍历与绑定逻辑在前端重写一遍。
方案二:后端注入 JSON,前端调用 JS 渲染(推荐方案)
由于您已经把“渲染 Markdown”、“配置代码块包装器”、“添加复制按钮”、“添加通道标识”这些极其繁琐的工作完美整合在前端的 appendMessage() 函数里,那么把历史记录作为 JSON 数组一次性喂给 JS,由 JS 来调用 appendMessage,能最完美地复用现有成果。
🛠️ Go 后端准备数据:
// 1. 定义消息结构体
type ChatMessage struct {
Sender string `json:"sender"` // "user" 或 "ai"
Text string `json:"text"` // 消息内容
Provider string `json:"provider"` // 通道标识,没有可为空
}
// 2. 将历史列表序列化为 JSON 字符串
historyBytes, _ := json.Marshal(userHistoryList)
historyJSON := string(historyBytes)
// 3. 传给前端模板的值为 historyJSON(例如: '[{"sender":"user","text":"你好"}]')
🛠️ HTML 模板中完美结合:
在您的 HTML 底部 <script> 中加入如下逻辑,既可以支持在无历史记录时显示默认欢迎内容,又能在有数据时无缝恢复历史会话:
document.addEventListener("DOMContentLoaded", () => {
const chatWindow = document.getElementById("chat-window");
const spacer = document.getElementById("bottom-spacer");
// 1. 接收 Go 注入的 JSON 变量(注意:使用 Go 模板输出 JSON 原生对象,确保安全)
// 若未登录或无数据,后台请赋予空数组 "[]"
const historyData = {{.HistoryJSON}};
if (historyData && historyData.length > 0) {
// 2. 🌟 有历史数据:循环调用您现成的 appendMessage,自动完成 Markdown 解析与复制按钮绑定!
historyData.forEach(item => {
// 根据 item.sender 分发气泡
appendMessage(item.sender, item.text, false, item.provider);
});
// 自动滚动到最新一条历史
chatWindow.scrollTop = chatWindow.scrollHeight;
} else {
// 3. 🌟 无历史数据:显示您的默认欢迎词
const welcomeText = `### 💡 欢迎使用【百问百答】AI 助手!
我是你的智能助手...`;
const welcomeDiv = document.createElement("div");
welcomeDiv.className = "message ai";
welcomeDiv.style.borderLeft = "4px solid #38bdf8";
welcomeDiv.innerHTML = marked.parse(welcomeText);
chatWindow.insertBefore(welcomeDiv, spacer);
}
});
⚖️ 总结:
- 如果您不想让前端 JS 做任何初始化渲染:建议在 Go 后端引入
goldmark库,先在后端将 Markdown 转为 HTML,然后再用 Go 模板的{{range}}输出 HTML,并在前端用 JS 监听并补齐“复制”按钮。 - 如果希望前端样式最统一、代码最精简:使用方案二。直接利用前端封装好的
appendMessage()方法去处理历史记录,能做到“新发的消息”与“历史消息”在视觉、Markdown排版、复制功能上100% 绝对一致。