第6章 构建简单前端界面(HTML + JS 调用 API)
目标概述
本章我们将开发一个简易的网页前端,让用户通过浏览器界面与我们的 ChatBot 交流。前端将包含一个输入框和发送按钮,用于提交问题,以及一个对话显示区域呈现问答历史。核心是在网页中使用 JavaScript 调用我们上章构建的 FastAPI 接口,将用户问题发送给后端并显示返回的答案。通过这个项目,你将掌握基础的 HTML 页面结构、简单 CSS 样式(可选),以及使用原生 JavaScript(或 Fetch API)进行 AJAX 网络请求的流程。完成本章后,你可以在本地打开这个网页文件,体验一个基本的聊天网页应用。
主要知识点
-
HTML 基础结构:HTML 构成网页内容的骨架。我们将创建一个简单的 HTML 文件,包含输入框 (<input>)、发送按钮 (<button>) 和显示聊天内容的区域 (<div>)。了解常见标签语义:比如 <h1> 标题,<p> 段落,<form> 表单等。不过本例可以不使用表单而直接用按钮配合 JS。
-
CSS 简介:CSS 用于美化页面。在本章我们不深究设计,但会写些基本样式让界面更清晰,例如设置输入框和按钮的位置,聊天记录区的滚动条等。可以使用内联 <style> 或者引用简单的 CSS。
-
JavaScript DOM 操作:通过 JavaScript 可以访问和操作 HTML 元素,即 DOM(文档对象模型)。我们需要用 JS 获取用户在输入框中输入的问题值,清空输入框,并创建新的 DOM 元素把 Bot 的回答和用户的问题添加到对话显示区。学会使用 document.getElementById 或 querySelector 定位元素,修改其 .value 或 .innerText,以及用 appendChild 添加新元素节点。
-
Fetch API 调用:现代浏览器提供 fetch() 方法进行HTTP请求。我们将使用 fetch() 对我们的后端 API 发起请求,发送用户问题并等待返回答案JSON[71]。了解基本的 fetch 用法:fetch(url, { method: ..., headers: ..., body: ... }) 返回一个 Promise,需要用 .then() 或 async/await 处理异步结果。以及如何解析返回 JSON:response.json() 也是Promise。[72]
-
跨域和CORS验证:当我们在本地直接打开 HTML 文件(file:// 协议)时,用 JS fetch 请求 http://127.0.0.1:8000 会触发跨域请求。由于上章已在后端允许 "*" 源,所以应该成功返回。如果未设置CORS,浏览器将报错阻止。可以观察浏览器开发者工具的Network或Console信息了解CORS是否成功。
-
用户体验细节:比如发送问题后应禁用按钮防止重复提交,或在等待响应时显示一个“正在思考...”的提示。这些不是核心但是UI友好要素。我们可以通过在调用API前后修改DOM实现:发送前在聊天区插入一条“User: ...”和“Bot 正在输入...”的消息,待API回来后替换或补充Bot回复。
-
前后端分离概念:通过这个练习体会前后端通过HTTP接口通信的模式。前端不需要知道AI如何实现,只通过调用API得到结果。反过来,后端不关心页面如何展示,专注提供数据。这样松耦合便于分别开发和维护。
完整代码
创建一个HTML文件,例如 chat.html,内容如下:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>ChatBot 前端</title>
<style>
body { font-family: sans-serif; margin: 20px; }
h1 { color: #333; }
#chat-box {
width: 100%; max-width: 600px; height: 300px;
border: 1px solid #ccc; padding: 10px; overflow-y: auto;
margin-bottom: 10px;
white-space: pre-wrap; /* 保留换行 */
}
.message { margin: 5px 0; }
.user { font-weight: bold; color: #0066cc; }
.bot { font-weight: bold; color: #00aa00; }
#question { width: 80%; padding: 5px; }
#send-btn { padding: 5px 10px; }
</style>
</head>
<body>
<h1>AI ChatBot 对话</h1>
<div id="chat-box"></div>
<input type="text" id="question" placeholder="在此输入你的提问..." />
<button id="send-btn">发送</button>
<script>
const questionInput = document.getElementById('question');
const sendBtn = document.getElementById('send-btn');
const chatBox = document.getElementById('chat-box');
// 向聊天框添加一条消息
function addMessage(sender, text) {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message');
if (sender === 'user') {
msgDiv.innerHTML = `<span class="user">用户:</span> ${text}`;
} else if (sender === 'bot') {
msgDiv.innerHTML = `<span class="bot">机器人:</span> ${text}`;
} else {
msgDiv.innerHTML = text;
}
chatBox.appendChild(msgDiv);
chatBox.scrollTop = chatBox.scrollHeight; // 滚动到底部
}
// 点击发送按钮事件处理
sendBtn.onclick = async function() {
let question = questionInput.value.trim();
if (question === "") {
return; // 空问题不处理
}
// 显示用户消息并清空输入框
addMessage('user', question);
questionInput.value = "";
questionInput.disabled = true;
sendBtn.disabled = true;
// 显示机器人思考中...
let loadingMsg = document.createElement('div');
loadingMsg.classList.add('message');
loadingMsg.innerHTML = `<span class="bot">机器人:</span> <em>正在思考...</em>`;
chatBox.appendChild(loadingMsg);
chatBox.scrollTop = chatBox.scrollHeight;
try {
// 调用后端 API
const response = await fetch("http://127.0.0.1:8000/chat?question=" + encodeURIComponent(question));
if (!response.ok) {
throw new Error("网络错误,状态码 " + response.status);
}
const data = await response.json();
// 移除 "思考中" 提示
chatBox.removeChild(loadingMsg);
// 显示机器人回复
addMessage('bot', data.answer);
} catch (err) {
chatBox.removeChild(loadingMsg);
addMessage('bot', `<span style="color:red;">[出错] ${err.message}</span>`);
} finally {
questionInput.disabled = false;
sendBtn.disabled = false;
questionInput.focus();
}
};
</script>
</body>
</html>
代码说明:
- HTML 部分包含一个标题、一个用于显示对话的 #chat-box 容器、一个文本输入框 #question、和一个发送按钮 #send-btn。通过简单的 CSS,我们让 chat-box 有固定高度、滚动条,用户和机器人消息用不同颜色[73][9]。这些class(.user, .bot)在JS添加HTML时用到了。
- JS 获取了必要的DOM元素引用。然后定义 addMessage(sender, text) 函数,用来在 chat-box 容器中添加一条消息[74]。我们创建一个 <div>,根据 sender类型拼接不同的内容字符串:如果 sender==='user',就包上 <span class="user">用户:</span> 作为前缀;如果是 'bot' 则 <span class="bot">机器人:</span>。然后将这个div附加到chatBox里,并将 chatBox 的 scrollTop 调整到 scrollHeight 使滚动条总是在底部,从而总是显示最新消息。
- 给发送按钮注册了点击事件处理函数(也可以给输入框绑定回车事件,不过此处简化仅按钮)。用了 async/await 来编写异步逻辑,这样比 .then 链式更直观。
- 点击后:首先读取输入框内容并去掉首尾空白,如果为空则直接 return 不发送[75]。否则,把用户的问题先添加到聊天框显示(调用 addMessage('user', question)),然后清空输入框并暂时禁用输入框和按钮,防止获取结果前用户又点[76]。
- 接着在聊天框添加一条机器人正在思考的提示(斜体“正在思考...”)[77]。这里没有用 addMessage,因为不想让它标记sender,只想创建一个临时的 loadingMsg DOM 元素,以便稍后可以移除。
- 然后使用 fetch 调用 API:await fetch("http://127.0.0.1:8000/chat?question=" + encodeURIComponent(question))[78]。注意:我们用 encodeURIComponent 对问题进行编码,确保即使包含空格、中文等特殊字符,URL也是有效的。fetch 返回一个 Response 对象,我们检查 response.ok(状态码200-299为ok)否则抛异常。[79] 然后调用 response.json() 解析 JSON 数据,同样await等待。
- 拿到 data 后(应该是形如 {"question": "...", "answer": "机器人的回答"}),我们做两件事:移除“思考中”提示节点,调用 addMessage('bot', data.answer) 将机器人答案显示[80]。
- 在 catch 捕获中,可能出现网络错误或服务器返回错误状态(比如500),我们 catch 到err,在界面上移除思考提示并添加一条 bot 消息内容标记错误[81]。这里用了<span style="color:red;">简单让错误消息红色显示。err.message 会包含我们在JS throw或Fetch自带的信息,如 "网络错误,状态码 500" 或别的。
- finally 中,无论成功失败,都重新启用输入框和按钮,并把焦点聚焦回输入框方便用户继续输入[82]。
运行前端:由于我们没有搭建额外的web服务器,这里直接用文件打开。启动后端FastAPI服务器(确保第5章的服务在运行),然后在本地用浏览器打开 chat.html 文件。例如直接双击文件或者浏览器地址栏输入 file:///C:/path/to/chat.html。打开后,应看到一个简洁页面,顶部有标题,下面是空的聊天框、输入框和发送按钮。
测试对话:在输入框输入一句,如“你好”,点击发送。你会看到:自己的这句话出现在聊天框前面带“用户:”,然后紧接着“机器人: 正在思考...”。约一两秒后(取决网络和OpenAI响应),“正在思考...”会被替换为真正的回答文本。此时按钮和输入框会恢复可用状态,你可以继续输入下一句。当聊天记录超出框高度时,会出现滚动条并自动滚到底部显示最新消息。
注意,如果打开页面后没有任何反应,按F12打开浏览器开发者工具Console,查看是否有错误。例如:
-
如果后端没跑,会报 Failed to fetch,我们的catch会捕获,显示 [出错] 网络错误,状态码 ERR_CONNECTION_REFUSED 或类似(不同浏览器词ings不同)。
-
如果CORS没设置好,则Console会报跨域被阻止,这种情况下我们的catch实际上拿不到err,因为浏览器直接拦截。但因为CORS在上章设置了,所以应该不会报。
-
另外也可能遇到Mixed Content问题:如果你的页面用file://或http方式打开,而调用https API才会,反之亦然。我们都在http下所以无问题。
操作截图或流程
-
网页初始状态:截图显示浏览器打开 chat.html 后的界面:顶部标题“AI ChatBot 对话”,中间为空白对话框,下面输入框和发送按钮。UI很朴素但清爽,用户可以点击输入框开始键入。
-
发送提问流程:一系列截图展示用户输入问题并点击发送,然后出现对话记录和回复的过程:
-
用户输入 "OpenAI是做什么的?" 然后点击发送。立即,在聊天框追加一条蓝色的“用户: OpenAI是做什么的?”消息,输入框清空、按钮变灰,下面显示绿色斜体“机器人: 正在思考...”[77]。
-
过了一秒左右,聊天框最后一条消息更新/追加为“机器人: OpenAI是一家人工智能研究实验室...”(假设的回答)。同时输入框和按钮恢复可用状态。整个对话在页面留存。
从截图可以看到,我们的JS逻辑成功地把异步调用结果插入到了页面。
-
连续对话:再来一条,如用户又问 "它什么时候成立的?" 发送。聊天框会继续追加“用户: 它什么时候成立的?”,然后机器人思考/回答。由于我们没有真正实现上下文关联,这里Bot可能不知道“它”指OpenAI,会回答不相关内容。这可以截图观察,有利于引出改进讨论。但此处我们聚焦前端功能。
-
错误情况:如果停止后端服务,再在前端发问,会发生catch错误。聊天框会把思考去掉后显示一条红色“[出错] 网络错误,状态码 500”或类似消息[81]。截图可演示这个错误提示功能,说明前端已考虑异常。
(截图根据实际运行结果截取,可多张展示交互流程。)
常见错误
-
本地文件跨域问题:从 file:// 打开的页面,使用 fetch 请求 http://127.0.0.1:8000,按CORS规则,其实file://算一个源,后端 * 允许所有来源,所以应该没问题。但某些浏览器可能对 file:// 跨域有限制。如果发现 fetch 根本没发出,可以尝试在本地搭建一个简单静态服务器(例如 python -m http.server),在浏览器通过 http://127.0.0.1:8000/chat.html 访问,这样前端Origin是127.0.0.1,与后端host相同只差端口,也受CORS控制但已允许。简单来说,如果file协议导致Issue,就换用localhost静态服。
-
URL错误:确保 fetch URL 与后端实际服务地址匹配。如果FastAPI在8000端口,用127.0.0.1;如果改了端口或者使用不同host,要对应修改。这往往是初学者错误点。例如调用了 http://localhost:8000/chat 但是后台只监听 127.0.0.1(有时localhost解析问题),可尝试统一使用localhost或127.0.0.1都行,一般没区别。但如果遇到 TypeError: Failed to fetch 有可能是URL错或服务没开。
-
未encodeURI:如果不使用 encodeURIComponent,用户输入可能包含空格、问号等特殊字符,直接拼到URL会导致 fetch 报错或请求截断。我们正确用了它,但值得提醒。
-
异步处理问题:如果没有使用 async/await,而用了 fetch(...).then(...),也可以,但要小心作用域,例如要在 then 里面处理dom恢复。async/await 已经帮我们结构清晰避免回调地狱。
-
DOM操作:常见如 document.getElementById('...') 返回 null,引起 JS错误。说明ID对不上。要检查HTML里的id和JS里是否一致拼写。还有一些浏览器Console错误可能是因为变量未定义或语法错,可以据此检查JS拼写错误。
-
消息转义:我们简单地将文本插入innerHTML,这里有XSS风险——不过我们的数据来源是OpenAI,理论上安全但不能100%保证(AI可能返回恶意脚本字符串?概率极低)。严格来说更安全的做法是用 textContent 插入,但那会丢掉我们内嵌的<span>标签样式。可以考虑对 data.answer 做一下简单转义或者更精细的DOM构造。目前这个demo问题不大,但须知生产环境要注意XSS。
-
体验细节:目前bot回答没有包含用户提问上下文,实际对话Bot可能答非所问。用户可能困惑。不过这是后端逻辑局限,不算前端bug。可以在UI层提示用户每个问题最好独立完整。
延伸思考
-
改进样式:你可以用CSS让聊天框更好看,比如为用户消息和机器人消息设计不同背景气泡、对齐方式(如用户靠右机器人靠左)。还可以添加头像、使用Bootstrap等UI库快速美化。本例突出逻辑所以样式简洁朴素。鼓励你尝试改进界面,在不改变功能代码的基础上,锻炼HTML/CSS能力。
-
前端框架:我们用了原生JavaScript。如果项目复杂,可以用前端框架如 React/Vue/Angular 重构。这些框架能帮忙更好地管理状态、组件化UI。本例简单DOM操作用原生即可胜任。思考下使用框架实现会如何?可能将消息列表作为state,每次调用接口后更新state自动重新渲染列表组件。对于小项目未必划算,但了解这些框架的存在和优势(例如React更方便构建复杂交互界面)。
-
持久化聊天记录:目前刷新页面聊天记录就没了,因为存在内存DOM里。如果希望记录对话,可以考虑将聊天内容存在浏览器LocalStorage,每次发送后同步保存,页面加载时读取渲染。或者由后端提供用户session机制保存历史。根据应用需要选择方案。
-
多用户支持:我们的前端仅服务一个用户使用。如果这是个多人访问的网页,前端基本不变,只是每个用户的浏览器独立运行脚本,后端无需区分用户,因为每次对话不关联。但如需实现每个用户自己的一段对话上下文(后端记忆),就需要前端发送某种用户标识,后端用数据库或内存保存上下文。可以通过登录系统或简单给每个浏览器分配一个UUID token来标识用户会话。
通过本章,你已经拥有一个基本可用的ChatBot网页应用!你可以在浏览器上向AI提问并得到回答。这是前后端协作的结果:Cursor 在背后帮我们开发后端API,浏览器展现了界面交互。