| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- <!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>API Lab · 浏览器调用实验</title>
- <style>
- :root { font-family: system-ui, sans-serif; color: #182c39; background: #f0f3f5; }
- body { max-width: 900px; margin: 40px auto; padding: 0 20px; }
- h1 { font-size: 30px; } h2 { font-size: 20px; margin-top: 0; }
- p { line-height: 1.7; } code { color: #23526c; }
- section { background: white; padding: 24px; border: 1px solid #d5dee3; border-radius: 12px; margin: 20px 0; }
- label { display: block; margin: 14px 0 8px; }
- input { width: min(95%, 500px); padding: 11px; border: 1px solid #a8b8c1; border-radius: 6px; font: inherit; }
- button { padding: 10px 16px; margin: 12px 8px 0 0; background: #194e6e; color: white; border: 0; border-radius: 6px; cursor: pointer; font: inherit; }
- button:disabled { opacity: .45; cursor: default; }
- button.secondary { background: #e5ecef; color: #182c39; }
- pre { background: #edf2f5; padding: 16px; min-height: 48px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
- .hint { font-size: 14px; color: #566b78; }
- :focus-visible { outline: 3px solid #df9a23; outline-offset: 3px; }
- </style>
- </head>
- <body>
- <p class="hint">API 服务开发 / 配套实验</p>
- <h1>让浏览器调用你的 FastAPI</h1>
- <p>先启动 <code>http://127.0.0.1:8000</code> 的 API,再通过 <code>http://localhost:5500/web-client.html</code> 打开此页。在开发者工具的 Network 面板中观察请求。</p>
- <section>
- <h2>01 · POST /notes</h2>
- <p>浏览器发送 JSON 请求体,服务创建笔记并返回 <code>201</code>。</p>
- <label for="note-title">笔记标题</label>
- <input id="note-title" value="我的第一条 API 笔记" maxlength="120">
- <div><button id="create-note" type="button">创建笔记</button></div>
- <pre id="note-output" aria-live="polite">等待请求</pre>
- </section>
- <section>
- <h2>02 · GET /stream · SSE</h2>
- <p>服务连续发送 <code>token</code> 事件,最后发送 <code>done</code>。客户端收到结束事件后主动关闭连接。</p>
- <label for="prompt">输入文本</label>
- <input id="prompt" value="分块返回这段文本,观察连接何时结束。" maxlength="200">
- <div><button id="start-stream" type="button">开始接收</button><button id="stop-stream" class="secondary" type="button" disabled>停止</button></div>
- <p id="stream-status" class="hint" role="status">未连接</p>
- <pre id="stream-output" aria-live="polite">收到的文本会显示在这里</pre>
- </section>
- <section>
- <h2>03 · /ws · WebSocket</h2>
- <p>连接保持期间,客户端可以多次发送消息。这里的文本 <code>ping</code> 是应用约定,不是协议控制帧。</p>
- <div><button id="connect-ws" type="button">连接</button><button id="close-ws" class="secondary" type="button" disabled>断开</button></div>
- <label for="ws-message">消息</label>
- <input id="ws-message" value="你好,FastAPI">
- <div><button id="send-ws" type="button" disabled>发送消息</button><button id="ping-ws" class="secondary" type="button" disabled>发送 ping</button></div>
- <p id="ws-status" class="hint" role="status">未连接</p>
- <pre id="ws-output" aria-live="polite">服务端消息会显示在这里</pre>
- </section>
- <p class="hint">本页只在本机练习。CORS 允许来源为 localhost:5500 和 127.0.0.1:5500;请勿双击 HTML 使用 file:// 打开。</p>
- <script>
- const API = 'http://127.0.0.1:8000';
- const $ = (id) => document.getElementById(id);
- let stream = null;
- let socket = null;
- $('create-note').onclick = async () => {
- $('create-note').disabled = true;
- $('note-output').textContent = '发送中…';
- try {
- const response = await fetch(`${API}/notes`, {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({title: $('note-title').value, content: '', pinned: false}),
- });
- const data = await response.json();
- $('note-output').textContent = `HTTP ${response.status}\n${JSON.stringify(data, null, 2)}`;
- } catch (error) {
- $('note-output').textContent = `请求失败:${error.message}\n检查服务是否启动、页面是否来自 localhost:5500。`;
- } finally {
- $('create-note').disabled = false;
- }
- };
- function stopStream(message = '已停止') {
- if (stream) stream.close();
- stream = null;
- $('stream-status').textContent = message;
- $('stop-stream').disabled = true;
- }
- $('start-stream').onclick = () => {
- stopStream(); // 重复开始时先关闭上一条连接。
- const prompt = $('prompt').value.trim();
- if (!prompt) {
- $('stream-status').textContent = '请先输入文本';
- return;
- }
- $('stream-output').textContent = '';
- $('stream-status').textContent = '连接中…';
- $('stop-stream').disabled = false;
- const current = new EventSource(`${API}/stream?${new URLSearchParams({prompt})}`);
- stream = current;
- current.onopen = () => {
- if (stream === current) $('stream-status').textContent = '正在接收';
- };
- current.addEventListener('token', (event) => {
- if (stream !== current) return;
- try {
- $('stream-output').textContent += JSON.parse(event.data).text;
- } catch {
- stopStream('数据格式错误,连接已关闭');
- }
- });
- current.addEventListener('done', () => {
- if (stream === current) stopStream('已完成,连接已关闭');
- });
- current.onerror = () => {
- if (stream === current) stopStream('连接失败或中断,请检查服务后重新开始');
- };
- };
- $('stop-stream').onclick = () => stopStream();
- function setSocketControls(connected) {
- $('send-ws').disabled = !connected;
- $('ping-ws').disabled = !connected;
- $('close-ws').disabled = !socket;
- $('connect-ws').disabled = Boolean(socket);
- }
- function closeSocket() {
- const previous = socket;
- socket = null;
- if (previous) previous.close(1000, 'Client closed');
- $('ws-status').textContent = '已断开';
- setSocketControls(false);
- }
- $('connect-ws').onclick = () => {
- closeSocket();
- const current = new WebSocket(`${API.replace('http:', 'ws:')}/ws`);
- socket = current;
- $('ws-output').textContent = '';
- $('ws-status').textContent = '连接中…';
- setSocketControls(false);
- current.onopen = () => {
- if (socket !== current) return;
- $('ws-status').textContent = '已连接';
- setSocketControls(true);
- };
- current.onmessage = (event) => {
- if (socket === current) $('ws-output').textContent += `${event.data}\n`;
- };
- current.onerror = () => {
- if (socket === current) $('ws-status').textContent = '连接错误,检查服务是否启动';
- };
- current.onclose = (event) => {
- if (socket !== current) return;
- socket = null;
- $('ws-status').textContent = `连接已关闭(code ${event.code})`;
- setSocketControls(false);
- };
- };
- function sendMessage(text) {
- if (socket?.readyState === WebSocket.OPEN) socket.send(text);
- }
- $('send-ws').onclick = () => sendMessage($('ws-message').value);
- $('ping-ws').onclick = () => sendMessage('ping');
- $('close-ws').onclick = closeSocket;
- window.addEventListener('beforeunload', () => {
- stopStream();
- closeSocket();
- });
- </script>
- </body>
- </html>
|