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

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

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

相关推荐
2501_918126913 小时前
用html5写一个国际象棋
前端·javascript·css
遇见~未来3 小时前
前端原生能力速查笔记(HTML + 浏览器 API 实战篇)
前端
2401_860319523 小时前
在React Native中开发一个轮播组件(Swipe轮播),通过组件react-native-snap-carousel来实现
javascript·react native·react.js
博客zhu虎康3 小时前
Vue全局挂载Element消息组件技巧
前端·javascript·vue.js
2401_860319523 小时前
在React Native中,开发自定义组件(例如一个`Tag`组件)通常涉及到创建React组件,并且实现一个点击事件处理器
javascript·react native·react.js
尼罗河女娲3 小时前
【测试开发】为什么 UI 自动化总是看起来不稳定?为什么需要引入SessionDirty flag?
开发语言·前端·javascript
JQ_Zhang3 小时前
手把手教你封装一个高性能、多功能的 React 锚点导航组件 (Anchor)
前端
soda_yo3 小时前
隐式类型转换:哈基米 == 猫 ? true :false
前端·javascript·面试
Alair‎3 小时前
200React-Query基础
前端·react.js·前端框架