web-client.html 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. <!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1">
  6. <title>API Lab · 浏览器调用实验</title>
  7. <style>
  8. :root { font-family: system-ui, sans-serif; color: #182c39; background: #f0f3f5; }
  9. body { max-width: 900px; margin: 40px auto; padding: 0 20px; }
  10. h1 { font-size: 30px; } h2 { font-size: 20px; margin-top: 0; }
  11. p { line-height: 1.7; } code { color: #23526c; }
  12. section { background: white; padding: 24px; border: 1px solid #d5dee3; border-radius: 12px; margin: 20px 0; }
  13. label { display: block; margin: 14px 0 8px; }
  14. input { width: min(95%, 500px); padding: 11px; border: 1px solid #a8b8c1; border-radius: 6px; font: inherit; }
  15. button { padding: 10px 16px; margin: 12px 8px 0 0; background: #194e6e; color: white; border: 0; border-radius: 6px; cursor: pointer; font: inherit; }
  16. button:disabled { opacity: .45; cursor: default; }
  17. button.secondary { background: #e5ecef; color: #182c39; }
  18. pre { background: #edf2f5; padding: 16px; min-height: 48px; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.6; }
  19. .hint { font-size: 14px; color: #566b78; }
  20. :focus-visible { outline: 3px solid #df9a23; outline-offset: 3px; }
  21. </style>
  22. </head>
  23. <body>
  24. <p class="hint">API 服务开发 / 配套实验</p>
  25. <h1>让浏览器调用你的 FastAPI</h1>
  26. <p>先启动 <code>http://127.0.0.1:8000</code> 的 API,再通过 <code>http://localhost:5500/web-client.html</code> 打开此页。在开发者工具的 Network 面板中观察请求。</p>
  27. <section>
  28. <h2>01 · POST /notes</h2>
  29. <p>浏览器发送 JSON 请求体,服务创建笔记并返回 <code>201</code>。</p>
  30. <label for="note-title">笔记标题</label>
  31. <input id="note-title" value="我的第一条 API 笔记" maxlength="120">
  32. <div><button id="create-note" type="button">创建笔记</button></div>
  33. <pre id="note-output" aria-live="polite">等待请求</pre>
  34. </section>
  35. <section>
  36. <h2>02 · GET /stream · SSE</h2>
  37. <p>服务连续发送 <code>token</code> 事件,最后发送 <code>done</code>。客户端收到结束事件后主动关闭连接。</p>
  38. <label for="prompt">输入文本</label>
  39. <input id="prompt" value="分块返回这段文本,观察连接何时结束。" maxlength="200">
  40. <div><button id="start-stream" type="button">开始接收</button><button id="stop-stream" class="secondary" type="button" disabled>停止</button></div>
  41. <p id="stream-status" class="hint" role="status">未连接</p>
  42. <pre id="stream-output" aria-live="polite">收到的文本会显示在这里</pre>
  43. </section>
  44. <section>
  45. <h2>03 · /ws · WebSocket</h2>
  46. <p>连接保持期间,客户端可以多次发送消息。这里的文本 <code>ping</code> 是应用约定,不是协议控制帧。</p>
  47. <div><button id="connect-ws" type="button">连接</button><button id="close-ws" class="secondary" type="button" disabled>断开</button></div>
  48. <label for="ws-message">消息</label>
  49. <input id="ws-message" value="你好,FastAPI">
  50. <div><button id="send-ws" type="button" disabled>发送消息</button><button id="ping-ws" class="secondary" type="button" disabled>发送 ping</button></div>
  51. <p id="ws-status" class="hint" role="status">未连接</p>
  52. <pre id="ws-output" aria-live="polite">服务端消息会显示在这里</pre>
  53. </section>
  54. <p class="hint">本页只在本机练习。CORS 允许来源为 localhost:5500 和 127.0.0.1:5500;请勿双击 HTML 使用 file:// 打开。</p>
  55. <script>
  56. const API = 'http://127.0.0.1:8000';
  57. const $ = (id) => document.getElementById(id);
  58. let stream = null;
  59. let socket = null;
  60. $('create-note').onclick = async () => {
  61. $('create-note').disabled = true;
  62. $('note-output').textContent = '发送中…';
  63. try {
  64. const response = await fetch(`${API}/notes`, {
  65. method: 'POST',
  66. headers: {'Content-Type': 'application/json'},
  67. body: JSON.stringify({title: $('note-title').value, content: '', pinned: false}),
  68. });
  69. const data = await response.json();
  70. $('note-output').textContent = `HTTP ${response.status}\n${JSON.stringify(data, null, 2)}`;
  71. } catch (error) {
  72. $('note-output').textContent = `请求失败:${error.message}\n检查服务是否启动、页面是否来自 localhost:5500。`;
  73. } finally {
  74. $('create-note').disabled = false;
  75. }
  76. };
  77. function stopStream(message = '已停止') {
  78. if (stream) stream.close();
  79. stream = null;
  80. $('stream-status').textContent = message;
  81. $('stop-stream').disabled = true;
  82. }
  83. $('start-stream').onclick = () => {
  84. stopStream(); // 重复开始时先关闭上一条连接。
  85. const prompt = $('prompt').value.trim();
  86. if (!prompt) {
  87. $('stream-status').textContent = '请先输入文本';
  88. return;
  89. }
  90. $('stream-output').textContent = '';
  91. $('stream-status').textContent = '连接中…';
  92. $('stop-stream').disabled = false;
  93. const current = new EventSource(`${API}/stream?${new URLSearchParams({prompt})}`);
  94. stream = current;
  95. current.onopen = () => {
  96. if (stream === current) $('stream-status').textContent = '正在接收';
  97. };
  98. current.addEventListener('token', (event) => {
  99. if (stream !== current) return;
  100. try {
  101. $('stream-output').textContent += JSON.parse(event.data).text;
  102. } catch {
  103. stopStream('数据格式错误,连接已关闭');
  104. }
  105. });
  106. current.addEventListener('done', () => {
  107. if (stream === current) stopStream('已完成,连接已关闭');
  108. });
  109. current.onerror = () => {
  110. if (stream === current) stopStream('连接失败或中断,请检查服务后重新开始');
  111. };
  112. };
  113. $('stop-stream').onclick = () => stopStream();
  114. function setSocketControls(connected) {
  115. $('send-ws').disabled = !connected;
  116. $('ping-ws').disabled = !connected;
  117. $('close-ws').disabled = !socket;
  118. $('connect-ws').disabled = Boolean(socket);
  119. }
  120. function closeSocket() {
  121. const previous = socket;
  122. socket = null;
  123. if (previous) previous.close(1000, 'Client closed');
  124. $('ws-status').textContent = '已断开';
  125. setSocketControls(false);
  126. }
  127. $('connect-ws').onclick = () => {
  128. closeSocket();
  129. const current = new WebSocket(`${API.replace('http:', 'ws:')}/ws`);
  130. socket = current;
  131. $('ws-output').textContent = '';
  132. $('ws-status').textContent = '连接中…';
  133. setSocketControls(false);
  134. current.onopen = () => {
  135. if (socket !== current) return;
  136. $('ws-status').textContent = '已连接';
  137. setSocketControls(true);
  138. };
  139. current.onmessage = (event) => {
  140. if (socket === current) $('ws-output').textContent += `${event.data}\n`;
  141. };
  142. current.onerror = () => {
  143. if (socket === current) $('ws-status').textContent = '连接错误,检查服务是否启动';
  144. };
  145. current.onclose = (event) => {
  146. if (socket !== current) return;
  147. socket = null;
  148. $('ws-status').textContent = `连接已关闭(code ${event.code})`;
  149. setSocketControls(false);
  150. };
  151. };
  152. function sendMessage(text) {
  153. if (socket?.readyState === WebSocket.OPEN) socket.send(text);
  154. }
  155. $('send-ws').onclick = () => sendMessage($('ws-message').value);
  156. $('ping-ws').onclick = () => sendMessage('ping');
  157. $('close-ws').onclick = closeSocket;
  158. window.addEventListener('beforeunload', () => {
  159. stopStream();
  160. closeSocket();
  161. });
  162. </script>
  163. </body>
  164. </html>