博客 / 詳情

返回

如何實現流式輸出?一篇文章手把手教你

在現代Web應用中,流式輸出(Streaming Output)是一種非常重要的技術,它能夠實現實時數據傳輸和漸進式渲染,為用户提供更好的交互體驗。本文將詳細介紹流式輸出的原理和多種實現方式。

什麼是流式輸出?

流式輸出是指數據不是一次性返回給客户端,而是分批次、連續地發送給客户端。這種方式特別適用於:

  • 實時聊天應用
  • 大文件下載
  • AI生成內容展示
  • 日誌實時監控
  • 數據報表逐步加載

流式輸出的優勢

  1. 降低延遲:用户無需等待所有數據準備完成
  2. 節省內存:避免一次性加載大量數據到內存
  3. 提升用户體驗:內容可以逐步顯示,感知更快
  4. 提高性能:減少服務器壓力,提高併發處理能力

前端實現方案

1. 使用 Fetch API + ReadableStream

這是現代瀏覽器中最推薦的方式:

// 基礎流式請求示例
async function streamFetch(url) {
  const response = await fetch(url);
  const reader = response.body.getReader();
  const decoder = new TextDecoder();

  while (true) {
    const { done, value } = await reader.read();
  
    if (done) break;
  
    // 解碼並處理接收到的數據塊
    const chunk = decoder.decode(value, { stream: true });
    console.log('Received chunk:', chunk);
  
    // 更新UI或進行其他處理
    updateUI(chunk);
  }
}

function updateUI(content) {
  const outputElement = document.getElementById('output');
  outputElement.innerHTML += content;
}

2. Vue組件中的流式輸出實現

創建一個支持流式輸出的Vue組件:

<template>
  <div class="stream-output">
    <div class="controls">
      <button @click="startStreaming" :disabled="isStreaming">
        開始流式輸出
      </button>
      <button @click="stopStreaming" :disabled="!isStreaming">
        停止流式輸出
      </button>
    </div>
  
    <div class="output-container">
      <pre ref="outputRef" class="output">{{ streamingContent }}</pre>
    </div>
  
    <div v-if="isLoading" class="loading">正在接收數據...</div>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'

const isStreaming = ref(false)
const streamingContent = ref('')
const isLoading = ref(false)
const abortController = ref(null)
const outputRef = ref(null)

// 模擬API端點
const API_ENDPOINT = '/api/stream-data'

async function startStreaming() {
  try {
    isStreaming.value = true
    streamingContent.value = ''
    isLoading.value = true
  
    // 創建AbortController用於取消請求
    abortController.value = new AbortController()
  
    const response = await fetch(API_ENDPOINT, {
      signal: abortController.value.signal
    })
  
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`)
    }
  
    const reader = response.body.getReader()
    const decoder = new TextDecoder('utf-8')
  
    // 逐塊讀取數據
    while (true) {
      const { done, value } = await reader.read()
    
      if (done) {
        break
      }
    
      // 解碼數據塊
      const chunk = decoder.decode(value, { stream: true })
    
      // 更新內容
      streamingContent.value += chunk
    
      // 自動滾動到底部
      scrollToBottom()
    }
  
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('流式輸出錯誤:', error)
    }
  } finally {
    isStreaming.value = false
    isLoading.value = false
  }
}

function stopStreaming() {
  if (abortController.value) {
    abortController.value.abort()
  }
  isStreaming.value = false
  isLoading.value = false
}

function scrollToBottom() {
  nextTick(() => {
    if (outputRef.value) {
      outputRef.value.scrollTop = outputRef.value.scrollHeight
    }
  })
}

onUnmounted(() => {
  stopStreaming()
})
</script>

<style scoped>
.stream-output {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.controls {
  margin-bottom: 20px;
}

.controls button {
  margin-right: 10px;
  padding: 8px 16px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}

.output-container {
  border: 1px solid #ddd;
  border-radius: 4px;
  height: 400px;
  overflow-y: auto;
  background-color: #f8f9fa;
}

.output {
  margin: 0;
  padding: 15px;
  font-family: 'Courier New', monospace;
  white-space: pre-wrap;
  word-wrap: break-word;
}

.loading {
  text-align: center;
  color: #666;
  margin-top: 10px;
}
</style>

3. Server-Sent Events (SSE) 實現

SSE是另一種常用的流式通信方式:

// SSE客户端實現
class StreamService {
  constructor() {
    this.eventSource = null
    this.listeners = []
  }

  connect(url) {
    if (this.eventSource) {
      this.disconnect()
    }
  
    this.eventSource = new EventSource(url)
  
    this.eventSource.onmessage = (event) => {
      this.notifyListeners(event.data)
    }
  
    this.eventSource.onerror = (error) => {
      console.error('SSE連接錯誤:', error)
    }
  
    this.eventSource.onopen = () => {
      console.log('SSE連接已建立')
    }
  }

  disconnect() {
    if (this.eventSource) {
      this.eventSource.close()
      this.eventSource = null
    }
  }

  addListener(callback) {
    this.listeners.push(callback)
  }

  removeListener(callback) {
    const index = this.listeners.indexOf(callback)
    if (index > -1) {
      this.listeners.splice(index, 1)
    }
  }

  notifyListeners(data) {
    this.listeners.forEach(callback => callback(data))
  }
}

// 在Vue組件中使用SSE
const streamService = new StreamService()

export default {
  data() {
    return {
      messages: [],
      isConnected: false
    }
  },

  mounted() {
    streamService.addListener(this.handleNewMessage)
  },

  beforeUnmount() {
    streamService.removeListener(this.handleNewMessage)
    streamService.disconnect()
  },

  methods: {
    connectToStream() {
      streamService.connect('/api/events')
      this.isConnected = true
    },
  
    disconnectFromStream() {
      streamService.disconnect()
      this.isConnected = false
    },
  
    handleNewMessage(data) {
      this.messages.push({
        id: Date.now(),
        content: data,
        timestamp: new Date().toLocaleTimeString()
      })
    }
  }
}

4. WebSocket 實現實時雙向通信

對於需要雙向通信的場景:

// WebSocket服務類
class WebSocketStream {
  constructor(url) {
    this.url = url
    this.websocket = null
    this.reconnectAttempts = 0
    this.maxReconnectAttempts = 5
    this.messageListeners = []
    this.statusListeners = []
  }

  connect() {
    this.websocket = new WebSocket(this.url)
  
    this.websocket.onopen = () => {
      console.log('WebSocket連接已建立')
      this.reconnectAttempts = 0
      this.notifyStatus('connected')
    }
  
    this.websocket.onmessage = (event) => {
      const data = JSON.parse(event.data)
      this.notifyMessage(data)
    }
  
    this.websocket.onclose = () => {
      console.log('WebSocket連接已關閉')
      this.notifyStatus('disconnected')
      this.attemptReconnect()
    }
  
    this.websocket.onerror = (error) => {
      console.error('WebSocket錯誤:', error)
      this.notifyStatus('error')
    }
  }

  sendMessage(message) {
    if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
      this.websocket.send(JSON.stringify(message))
    }
  }

  close() {
    if (this.websocket) {
      this.websocket.close()
    }
  }

  attemptReconnect() {
    if (this.reconnectAttempts < this.maxReconnectAttempts) {
      this.reconnectAttempts++
      setTimeout(() => {
        console.log(`嘗試重連 (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
        this.connect()
      }, 1000 * this.reconnectAttempts)
    }
  }

  addMessageListener(callback) {
    this.messageListeners.push(callback)
  }

  addStatusListener(callback) {
    this.statusListeners.push(callback)
  }

  notifyMessage(data) {
    this.messageListeners.forEach(callback => callback(data))
  }

  notifyStatus(status) {
    this.statusListeners.forEach(callback => callback(status))
  }
}

// Vue組件中使用WebSocket
export default {
  data() {
    return {
      wsStream: null,
      messages: [],
      connectionStatus: 'disconnected'
    }
  },

  mounted() {
    this.wsStream = new WebSocketStream('ws://localhost:8080/ws')
    this.wsStream.addMessageListener(this.handleMessage)
    this.wsStream.addStatusListener(this.handleStatusChange)
    this.wsStream.connect()
  },

  beforeUnmount() {
    if (this.wsStream) {
      this.wsStream.close()
    }
  },

  methods: {
    handleMessage(data) {
      this.messages.push({
        ...data,
        receivedAt: new Date().toISOString()
      })
    },
  
    handleStatusChange(status) {
      this.connectionStatus = status
    },
  
    sendUserMessage(content) {
      this.wsStream.sendMessage({
        type: 'user_message',
        content: content,
        sentAt: new Date().toISOString()
      })
    }
  }
}

後端實現示例

Node.js Express 實現流式響應

const express = require('express')
const app = express()

// 模擬流式數據生成
app.get('/api/stream-data', (req, res) => {
  // 設置響應頭以支持流式傳輸
  res.setHeader('Content-Type', 'text/plain; charset=utf-8')
  res.setHeader('Transfer-Encoding', 'chunked')

  // 發送初始數據
  res.write('開始流式傳輸...\n')

  let count = 0
  const interval = setInterval(() => {
    count++
    const data = `數據塊 ${count}: ${new Date().toISOString()}\n`
    res.write(data)
  
    // 結束流式傳輸
    if (count >= 10) {
      clearInterval(interval)
      res.write('流式傳輸結束\n')
      res.end()
    }
  }, 1000)

  // 處理客户端斷開連接
  req.on('close', () => {
    clearInterval(interval)
    console.log('客户端斷開了連接')
  })
})

// SSE端點
app.get('/api/events', (req, res) => {
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
    'Connection': 'keep-alive',
    'Access-Control-Allow-Origin': '*'
  })

  // 發送初始事件
  res.write('data: 連接已建立\n\n')

  let count = 0
  const interval = setInterval(() => {
    count++
    const data = `data: 事件 ${count} - ${new Date().toISOString()}\n\n`
    res.write(data)
  }, 2000)

  // 處理客户端斷開連接
  req.on('close', () => {
    clearInterval(interval)
    res.end()
  })
})

app.listen(3000, () => {
  console.log('服務器運行在 http://localhost:3000')
})

性能優化建議

1. 內存管理

// 限制緩存大小
class LimitedBuffer {
  constructor(maxSize = 1000) {
    this.buffer = []
    this.maxSize = maxSize
  }

  add(item) {
    this.buffer.push(item)
    if (this.buffer.length > this.maxSize) {
      this.buffer.shift() // 移除最舊的項
    }
  }

  get() {
    return this.buffer
  }
}

2. 節流更新

// 節流函數防止頻繁更新DOM
function throttle(func, limit) {
  let inThrottle
  return function() {
    const args = arguments
    const context = this
    if (!inThrottle) {
      func.apply(context, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 在組件中使用
const throttledUpdate = throttle((content) => {
  streamingContent.value += content
}, 100) // 每100ms最多更新一次

3. 錯誤處理和重試機制

// 帶重試機制的流式請求
async function streamWithRetry(url, maxRetries = 3) {
  for (let i = 0; i <= maxRetries; i++) {
    try {
      await streamFetch(url)
      return // 成功後退出
    } catch (error) {
      console.warn(`流式請求失敗,第${i + 1}次重試`, error)
    
      if (i === maxRetries) {
        throw new Error('達到最大重試次數')
      }
    
      // 等待後重試
      await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)))
    }
  }
}

完整的示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>流式輸出示例</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
      line-height: 1.6;
      color: #333;
      background-color: #f5f5f5;
      padding: 20px;
    }

    .container {
      max-width: 1200px;
      margin: 0 auto;
    }

    header {
      text-align: center;
      margin-bottom: 30px;
      padding: 20px;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      border-radius: 10px;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
    }

    h1 {
      font-size: 2.5em;
      margin-bottom: 10px;
    }

    .subtitle {
      font-size: 1.2em;
      opacity: 0.9;
    }

    .tabs {
      display: flex;
      justify-content: center;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }

    .tab-button {
      padding: 12px 24px;
      margin: 5px;
      background-color: #e0e0e0;
      border: none;
      border-radius: 25px;
      cursor: pointer;
      font-size: 16px;
      font-weight: 500;
      transition: all 0.3s ease;
    }

    .tab-button:hover {
      background-color: #d5d5d5;
    }

    .tab-button.active {
      background-color: #667eea;
      color: white;
    }

    .tab-content {
      display: none;
      background: white;
      border-radius: 10px;
      padding: 25px;
      box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
      margin-bottom: 30px;
    }

    .tab-content.active {
      display: block;
      animation: fadeIn 0.5s ease;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(10px); }
      to { opacity: 1; transform: translateY(0); }
    }

    .controls {
      display: flex;
      gap: 10px;
      margin-bottom: 20px;
      flex-wrap: wrap;
    }

    .btn {
      padding: 10px 20px;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      font-size: 14px;
      font-weight: 500;
      transition: all 0.3s ease;
    }

    .btn-primary {
      background-color: #667eea;
      color: white;
    }

    .btn-secondary {
      background-color: #6c757d;
      color: white;
    }

    .btn-success {
      background-color: #28a745;
      color: white;
    }

    .btn-danger {
      background-color: #dc3545;
      color: white;
    }

    .btn:disabled {
      opacity: 0.6;
      cursor: not-allowed;
    }

    .btn:hover:not(:disabled) {
      transform: translateY(-2px);
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
    }

    .output-container {
      border: 2px solid #e9ecef;
      border-radius: 8px;
      height: 300px;
      overflow-y: auto;
      background-color: #f8f9fa;
      padding: 15px;
      margin-bottom: 15px;
      font-family: 'Courier New', monospace;
      white-space: pre-wrap;
      word-wrap: break-word;
    }

    .status-bar {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 10px;
      background-color: #e9ecef;
      border-radius: 5px;
      margin-top: 10px;
    }

    .status-indicator {
      display: inline-block;
      width: 12px;
      height: 12px;
      border-radius: 50%;
      margin-right: 8px;
    }

    .status-connected {
      background-color: #28a745;
    }

    .status-disconnected {
      background-color: #dc3545;
    }

    .status-loading {
      background-color: #ffc107;
    }

    .progress-bar {
      width: 100%;
      height: 8px;
      background-color: #e9ecef;
      border-radius: 4px;
      overflow: hidden;
      margin-top: 10px;
    }

    .progress-fill {
      height: 100%;
      background: linear-gradient(90deg, #667eea, #764ba2);
      border-radius: 4px;
      transition: width 0.3s ease;
    }

    .chat-messages {
      height: 350px;
      overflow-y: auto;
      border: 1px solid #ddd;
      border-radius: 8px;
      padding: 15px;
      margin-bottom: 15px;
      background-color: white;
    }

    .message {
      margin-bottom: 15px;
      padding: 10px;
      border-radius: 8px;
      max-width: 80%;
    }

    .message-user {
      background-color: #667eea;
      color: white;
      margin-left: auto;
      text-align: right;
    }

    .message-bot {
      background-color: #f1f3f4;
      color: #333;
    }

    .input-group {
      display: flex;
      gap: 10px;
    }

    .input-group input {
      flex: 1;
      padding: 12px;
      border: 1px solid #ddd;
      border-radius: 5px;
      font-size: 14px;
    }

    .features {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
      gap: 20px;
      margin-top: 30px;
    }

    .feature-card {
      background: white;
      border-radius: 10px;
      padding: 20px;
      box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      transition: transform 0.3s ease;
    }

    .feature-card:hover {
      transform: translateY(-5px);
    }

    .feature-card h3 {
      color: #667eea;
      margin-bottom: 10px;
    }

    footer {
      text-align: center;
      margin-top: 40px;
      padding: 20px;
      color: #6c757d;
      font-size: 0.9em;
    }

    @media (max-width: 768px) {
      .container {
        padding: 10px;
      }
      
      h1 {
        font-size: 2em;
      }
      
      .controls {
        flex-direction: column;
      }
      
      .btn {
        width: 100%;
      }
    }
  </style>
</head>
<body>
  <div class="container">
    <header>
      <h1>流式輸出技術演示</h1>
      <p class="subtitle">Fetch API + ReadableStream | Server-Sent Events | WebSocket</p>
    </header>

    <div class="tabs">
      <button class="tab-button active" onclick="switchTab('fetch')">Fetch Stream</button>
      <button class="tab-button" onclick="switchTab('sse')">Server-Sent Events</button>
      <button class="tab-button" onclick="switchTab('websocket')">WebSocket Chat</button>
    </div>

    <!-- Fetch Stream Tab -->
    <div id="fetch" class="tab-content active">
      <h2>Fetch API 流式輸出</h2>
      <p>使用現代瀏覽器的 Fetch API 和 ReadableStream 實現流式數據傳輸</p>
      
      <div class="controls">
        <button id="startFetchBtn" class="btn btn-primary" onclick="startFetchStream()">
          開始流式輸出
        </button>
        <button id="stopFetchBtn" class="btn btn-danger" onclick="stopFetchStream()" disabled>
          停止流式輸出
        </button>
        <button class="btn btn-secondary" onclick="clearFetchOutput()">
          清空輸出
        </button>
      </div>
      
      <div id="fetchOutput" class="output-container"></div>
      
      <div class="status-bar">
        <div>
          <span class="status-indicator" id="fetchStatusIndicator"></span>
          <span id="fetchStatusText">未開始</span>
        </div>
        <div>接收字節: <span id="fetchByteCount">0</span></div>
      </div>
      
      <div class="progress-bar">
        <div class="progress-fill" id="fetchProgress" style="width: 0%"></div>
      </div>
    </div>

    <!-- SSE Tab -->
    <div id="sse" class="tab-content">
      <h2>Server-Sent Events (SSE)</h2>
      <p>使用 SSE 實現服務器推送的實時數據流</p>
      
      <div class="controls">
        <button id="connectSSEBtn" class="btn btn-success" onclick="connectSSE()">
          連接SSE
        </button>
        <button id="disconnectSSEBtn" class="btn btn-danger" onclick="disconnectSSE()" disabled>
          斷開連接
        </button>
        <button class="btn btn-secondary" onclick="clearSSEOutput()">
          清空輸出
        </button>
      </div>
      
      <div id="sseOutput" class="output-container"></div>
      
      <div class="status-bar">
        <div>
          <span class="status-indicator" id="sseStatusIndicator"></span>
          <span id="sseStatusText">未連接</span>
        </div>
        <div>接收事件: <span id="sseEventCount">0</span></div>
      </div>
    </div>

    <!-- WebSocket Tab -->
    <div id="websocket" class="tab-content">
      <h2>WebSocket 實時聊天</h2>
      <p>使用 WebSocket 實現雙向實時通信</p>
      
      <div class="chat-messages" id="chatMessages"></div>
      
      <div class="input-group">
        <input type="text" id="messageInput" placeholder="輸入消息..." onkeypress="handleKeyPress(event)">
        <button id="sendBtn" class="btn btn-primary" onclick="sendMessage()" disabled>
          發送
        </button>
        <button id="connectWSBtn" class="btn btn-success" onclick="connectWebSocket()">
          連接
        </button>
        <button id="disconnectWSBtn" class="btn btn-danger" onclick="disconnectWebSocket()" disabled>
          斷開
        </button>
      </div>
      
      <div class="status-bar">
        <div>
          <span class="status-indicator" id="wsStatusIndicator"></span>
          <span id="wsStatusText">未連接</span>
        </div>
        <div>消息數量: <span id="messageCount">0</span></div>
      </div>
    </div>

    <div class="features">
      <div class="feature-card">
        <h3>🚀 高性能</h3>
        <p>流式輸出減少等待時間,提升用户體驗,避免長時間白屏。</p>
      </div>
      <div class="feature-card">
        <h3>💾 內存友好</h3>
        <p>逐塊處理數據,避免一次性加載大量數據到內存中。</p>
      </div>
      <div class="feature-card">
        <h3>🔄 實時性強</h3>
        <p>數據即時傳輸,適用於聊天、通知、實時監控等場景。</p>
      </div>
    </div>

    <footer>
      <p>流式輸出技術演示 | 基於現代Web標準實現</p>
    </footer>
  </div>

  <script>
    // 全局變量
    let fetchController = null;
    let sseConnection = null;
    let wsConnection = null;
    let fetchByteCount = 0;
    let sseEventCount = 0;
    let messageCount = 0;

    // 標籤頁切換
    function switchTab(tabId) {
      // 隱藏所有標籤內容
      document.querySelectorAll('.tab-content').forEach(tab => {
        tab.classList.remove('active');
      });
      
      // 移除所有激活按鈕樣式
      document.querySelectorAll('.tab-button').forEach(btn => {
        btn.classList.remove('active');
      });
      
      // 顯示選中的標籤內容
      document.getElementById(tabId).classList.add('active');
      
      // 激活對應的按鈕
      event.target.classList.add('active');
      
      // 停止所有正在進行的操作
      stopFetchStream();
      disconnectSSE();
      disconnectWebSocket();
    }

    // ==================== Fetch Stream Implementation ====================
    
    async function startFetchStream() {
      const output = document.getElementById('fetchOutput');
      const startBtn = document.getElementById('startFetchBtn');
      const stopBtn = document.getElementById('stopFetchBtn');
      const statusIndicator = document.getElementById('fetchStatusIndicator');
      const statusText = document.getElementById('fetchStatusText');
      const byteCount = document.getElementById('fetchByteCount');
      const progressBar = document.getElementById('fetchProgress');
      
      // 重置狀態
      output.innerHTML = '';
      fetchByteCount = 0;
      byteCount.textContent = '0';
      progressBar.style.width = '0%';
      
      // 更新UI狀態
      startBtn.disabled = true;
      stopBtn.disabled = false;
      statusIndicator.className = 'status-indicator status-loading';
      statusText.textContent = '流式傳輸中...';
      
      try {
        // 創建AbortController用於取消請求
        fetchController = new AbortController();
        
        // 模擬流式響應 - 在實際應用中這會是一個真實的API端點
        const response = await simulateFetchStream(fetchController.signal);
        
        const reader = response.body.getReader();
        const decoder = new TextDecoder('utf-8');
        
        let progress = 0;
        const totalChunks = 20; // 模擬總塊數
        
        while (true) {
          const { done, value } = await reader.read();
          
          if (done) {
            break;
          }
          
          // 解碼數據塊
          const chunk = decoder.decode(value, { stream: true });
          
          // 更新輸出
          output.innerHTML += chunk;
          output.scrollTop = output.scrollHeight;
          
          // 更新統計信息
          fetchByteCount += value.byteLength;
          byteCount.textContent = fetchByteCount;
          
          // 更新進度條
          progress = Math.min(progress + 1, totalChunks);
          const percentage = (progress / totalChunks) * 100;
          progressBar.style.width = percentage + '%';
        }
        
        // 完成後更新狀態
        statusIndicator.className = 'status-indicator status-connected';
        statusText.textContent = '傳輸完成';
        
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Fetch流式錯誤:', error);
          statusIndicator.className = 'status-indicator status-disconnected';
          statusText.textContent = '傳輸錯誤: ' + error.message;
        } else {
          statusText.textContent = '傳輸已停止';
        }
      } finally {
        startBtn.disabled = false;
        stopBtn.disabled = true;
        if (progressBar.style.width !== '100%') {
          progressBar.style.width = '100%';
        }
      }
    }
    
    function stopFetchStream() {
      if (fetchController) {
        fetchController.abort();
        fetchController = null;
      }
      
      const startBtn = document.getElementById('startFetchBtn');
      const stopBtn = document.getElementById('stopFetchBtn');
      const statusIndicator = document.getElementById('fetchStatusIndicator');
      const statusText = document.getElementById('fetchStatusText');
      
      startBtn.disabled = false;
      stopBtn.disabled = true;
      statusIndicator.className = 'status-indicator status-disconnected';
      statusText.textContent = '傳輸已停止';
    }
    
    function clearFetchOutput() {
      document.getElementById('fetchOutput').innerHTML = '';
      document.getElementById('fetchByteCount').textContent = '0';
      document.getElementById('fetchProgress').style.width = '0%';
    }
    
    // 模擬Fetch流式響應
    function simulateFetchStream(signal) {
      return new Promise((resolve) => {
        // 創建一個ReadableStream來模擬服務器響應
        const stream = new ReadableStream({
          start(controller) {
            let count = 0;
            const maxChunks = 20;
            
            const sendChunk = () => {
              if (count >= maxChunks || signal.aborted) {
                controller.close();
                return;
              }
              
              count++;
              const chunkData = `數據塊 ${count}: ${new Date().toLocaleTimeString()}\n` +
                               `隨機內容: ${Math.random().toString(36).substring(7)}\n` +
                               `${'='.repeat(50)}\n`;
              
              controller.enqueue(new TextEncoder().encode(chunkData));
              
              // 隨機間隔發送下一個塊
              setTimeout(sendChunk, Math.random() * 800 + 200);
            };
            
            sendChunk();
          }
        });
        
        // 模擬響應對象
        resolve({
          body: stream
        });
      });
    }

    // ==================== SSE Implementation ====================
    
    function connectSSE() {
      const output = document.getElementById('sseOutput');
      const connectBtn = document.getElementById('connectSSEBtn');
      const disconnectBtn = document.getElementById('disconnectSSEBtn');
      const statusIndicator = document.getElementById('sseStatusIndicator');
      const statusText = document.getElementById('sseStatusText');
      const eventCount = document.getElementById('sseEventCount');
      
      // 重置狀態
      output.innerHTML = '';
      sseEventCount = 0;
      eventCount.textContent = '0';
      
      // 更新UI狀態
      connectBtn.disabled = true;
      disconnectBtn.disabled = false;
      statusIndicator.className = 'status-indicator status-loading';
      statusText.textContent = '連接中...';
      
      // 模擬SSE連接
      simulateSSEConnection();
    }
    
    function disconnectSSE() {
      if (sseConnection) {
        clearInterval(sseConnection);
        sseConnection = null;
      }
      
      const connectBtn = document.getElementById('connectSSEBtn');
      const disconnectBtn = document.getElementById('disconnectSSEBtn');
      const statusIndicator = document.getElementById('sseStatusIndicator');
      const statusText = document.getElementById('sseStatusText');
      
      connectBtn.disabled = false;
      disconnectBtn.disabled = true;
      statusIndicator.className = 'status-indicator status-disconnected';
      statusText.textContent = '連接已斷開';
    }
    
    function clearSSEOutput() {
      document.getElementById('sseOutput').innerHTML = '';
      document.getElementById('sseEventCount').textContent = '0';
    }
    
    // 模擬SSE連接
    function simulateSSEConnection() {
      const output = document.getElementById('sseOutput');
      const statusIndicator = document.getElementById('sseStatusIndicator');
      const statusText = document.getElementById('sseStatusText');
      const eventCount = document.getElementById('sseEventCount');
      
      statusIndicator.className = 'status-indicator status-connected';
      statusText.textContent = '已連接';
      
      let count = 0;
      sseConnection = setInterval(() => {
        count++;
        sseEventCount++;
        eventCount.textContent = sseEventCount;
        
        const eventData = `[${new Date().toLocaleTimeString()}] 服務器事件 #${count}\n` +
                         `事件類型: 系統通知\n` +
                         `內容: 這是第${count}個模擬事件\n` +
                         `${'-'.repeat(40)}\n`;
        
        output.innerHTML += eventData;
        output.scrollTop = output.scrollHeight;
        
        // 模擬連接斷開
        if (count === 15) {
          clearInterval(sseConnection);
          sseConnection = null;
          const statusIndicator = document.getElementById('sseStatusIndicator');
          const statusText = document.getElementById('sseStatusText');
          statusIndicator.className = 'status-indicator status-disconnected';
          statusText.textContent = '連接已斷開';
          document.getElementById('connectSSEBtn').disabled = false;
          document.getElementById('disconnectSSEBtn').disabled = true;
        }
      }, 1000);
    }

    // ==================== WebSocket Implementation ====================
    
    function connectWebSocket() {
      const connectBtn = document.getElementById('connectWSBtn');
      const disconnectBtn = document.getElementById('disconnectWSBtn');
      const sendBtn = document.getElementById('sendBtn');
      const statusIndicator = document.getElementById('wsStatusIndicator');
      const statusText = document.getElementById('wsStatusText');
      const chatMessages = document.getElementById('chatMessages');
      
      // 重置狀態
      chatMessages.innerHTML = '';
      messageCount = 0;
      document.getElementById('messageCount').textContent = '0';
      
      // 更新UI狀態
      connectBtn.disabled = true;
      disconnectBtn.disabled = false;
      sendBtn.disabled = false;
      statusIndicator.className = 'status-indicator status-loading';
      statusText.textContent = '連接中...';
      
      // 模擬WebSocket連接
      simulateWebSocketConnection();
    }
    
    function disconnectWebSocket() {
      if (wsConnection) {
        clearInterval(wsConnection);
        wsConnection = null;
      }
      
      const connectBtn = document.getElementById('connectWSBtn');
      const disconnectBtn = document.getElementById('disconnectWSBtn');
      const sendBtn = document.getElementById('sendBtn');
      const statusIndicator = document.getElementById('wsStatusIndicator');
      const statusText = document.getElementById('wsStatusText');
      
      connectBtn.disabled = false;
      disconnectBtn.disabled = true;
      sendBtn.disabled = true;
      statusIndicator.className = 'status-indicator status-disconnected';
      statusText.textContent = '連接已斷開';
    }
    
    function sendMessage() {
      const input = document.getElementById('messageInput');
      const message = input.value.trim();
      
      if (message) {
        addMessage(message, 'user');
        input.value = '';
        
        // 模擬機器人回覆
        setTimeout(() => {
          const replies = [
            '你好!我收到了你的消息。',
            '這是一個很好的問題!',
            '讓我想想如何回答...',
            '感謝你的分享!',
            '我理解你的觀點。',
            '這很有趣!告訴我更多。'
          ];
          const randomReply = replies[Math.floor(Math.random() * replies.length)];
          addMessage(randomReply, 'bot');
        }, 1000 + Math.random() * 2000);
      }
    }
    
    function handleKeyPress(event) {
      if (event.key === 'Enter') {
        sendMessage();
      }
    }
    
    function addMessage(content, sender) {
      const chatMessages = document.getElementById('chatMessages');
      const messageCountEl = document.getElementById('messageCount');
      
      const messageDiv = document.createElement('div');
      messageDiv.className = `message message-${sender}`;
      
      const timeString = new Date().toLocaleTimeString();
      messageDiv.innerHTML = `
        <div>${content}</div>
        <small style="opacity: 0.7; font-size: 0.8em;">${timeString}</small>
      `;
      
      chatMessages.appendChild(messageDiv);
      chatMessages.scrollTop = chatMessages.scrollHeight;
      
      messageCount++;
      messageCountEl.textContent = messageCount;
    }
    
    // 模擬WebSocket連接
    function simulateWebSocketConnection() {
      const statusIndicator = document.getElementById('wsStatusIndicator');
      const statusText = document.getElementById('wsStatusText');
      
      statusIndicator.className = 'status-indicator status-connected';
      statusText.textContent = '已連接';
      
      // 模擬系統消息
      setTimeout(() => {
        addMessage('歡迎來到實時聊天室!', 'bot');
      }, 500);
      
      // 模擬定期系統通知
      let notificationCount = 0;
      wsConnection = setInterval(() => {
        notificationCount++;
        if (notificationCount <= 5) {
          addMessage(`系統通知: 用户在線數 ${Math.floor(Math.random() * 100)}`, 'bot');
        }
      }, 5000);
    }

    // 初始化
    document.addEventListener('DOMContentLoaded', function() {
      // 設置初始狀態指示器
      document.getElementById('fetchStatusIndicator').className = 'status-indicator status-disconnected';
      document.getElementById('sseStatusIndicator').className = 'status-indicator status-disconnected';
      document.getElementById('wsStatusIndicator').className = 'status-indicator status-disconnected';
    });
  </script>
</body>
</html>

最佳實踐總結

  1. 選擇合適的傳輸協議

    • 單向流式輸出:Fetch + ReadableStream 或 SSE
    • 雙向實時通信:WebSocket
  2. 合理設置緩衝區大小:避免內存溢出
  3. 實現優雅降級:當流式不支持時提供備選方案
  4. 添加適當的錯誤處理:網絡中斷、解析錯誤等
  5. 考慮用户體驗:加載狀態提示、自動滾動等
  6. 性能監控:記錄傳輸速度、錯誤率等指標

通過以上實現方式和最佳實踐,你可以輕鬆在項目中集成流式輸出功能,為用户提供更加流暢和實時的交互體驗。記住根據具體需求選擇最適合的技術方案!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.