如何实现流式输出?一篇文章手把手教你

在现代Web应用中,流式输出(Streaming Output)是一种非常重要的技术,它能够实现实时数据传输和渐进式渲染,为用户提供更好的交互体验。本文将详细介绍流式输出的原理和多种实现方式。

什么是流式输出?

流式输出是指数据不是一次性返回给客户端,而是分批次、连续地发送给客户端。这种方式特别适用于:

  • 实时聊天应用
  • 大文件下载
  • AI生成内容展示
  • 日志实时监控
  • 数据报表逐步加载

流式输出的优势

  1. 降低延迟:用户无需等待所有数据准备完成
  2. 节省内存:避免一次性加载大量数据到内存
  3. 提升用户体验:内容可以逐步显示,感知更快
  4. 提高性能:减少服务器压力,提高并发处理能力

前端实现方案

1. 使用 Fetch API + ReadableStream

这是现代浏览器中最推荐的方式:

javascript 复制代码
// 基础流式请求示例
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组件:

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是另一种常用的流式通信方式:

javascript 复制代码
// 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 实现实时双向通信

对于需要双向通信的场景:

javascript 复制代码
// 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 实现流式响应

javascript 复制代码
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. 内存管理

javascript 复制代码
// 限制缓存大小
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. 节流更新

javascript 复制代码
// 节流函数防止频繁更新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. 错误处理和重试机制

javascript 复制代码
// 带重试机制的流式请求
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)))
    }
  }
}

完整的示例

html 复制代码
<!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. 性能监控:记录传输速度、错误率等指标

通过以上实现方式和最佳实践,你可以轻松在项目中集成流式输出功能,为用户提供更加流畅和实时的交互体验。记住根据具体需求选择最适合的技术方案!

相关推荐
掘了3 分钟前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 分钟前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅28 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法