最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot+Vue3整合SSE實現(xiàn)實時消息推送功能

 更新時間:2025年11月28日 15:20:43   作者:劉大華  
在日常開發(fā)中,我們經(jīng)常需要實現(xiàn)實時消息推送的功能,這篇文章將基于SpringBoot和Vue3來簡單實現(xiàn)一個入門級的例子,下面小編就和大家詳細介紹一下

前言

在日常開發(fā)中,我們經(jīng)常需要實現(xiàn)實時消息推送的功能。比如新聞應用、聊天系統(tǒng)、監(jiān)控告警等場景。這篇文章基于SpringBootVue3來簡單實現(xiàn)一個入門級的例子。

實現(xiàn)場景:在一個瀏覽器發(fā)送通知,其他所有打開的瀏覽器都能實時收到!

先大概介紹下SSE

SSE(Server-Sent Events)是一種允許服務器向客戶端推送數(shù)據(jù)的技術。與 WebSocket 相比,SSE 更簡單易用,特別適合只需要服務器向客戶端單向通信的場景。

就像你訂閱了某公眾號,有新的文章就會主動推送給你一樣。

下面來看下實際代碼,完整代碼都在這里。

后端實現(xiàn)(SpringBoot)

控制器類 - 核心邏輯都在這里

package com.im.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

@Slf4j
@RestController
@RequestMapping("/api/sse")
public class SseController {

    // 存儲所有連接的客戶端 - 關鍵:這個列表保存了所有瀏覽器的連接
    private final List<SseEmitter> emitters = new CopyOnWriteArrayList<>();

    /**
     * 訂閱SSE事件 - 前端通過這個接口建立連接
     * 當瀏覽器打開頁面時,就會調(diào)用這個接口
     */
    @GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public SseEmitter subscribe() {
        // 創(chuàng)建SSE發(fā)射器,3600000毫秒=1小時超時
        SseEmitter emitter = new SseEmitter(3600_000L);
        log.info("新的SSE客戶端連接成功");
        
        // 將新連接加入到客戶端列表
        emitters.add(emitter);

        // 設置連接完成時的回調(diào)(用戶關閉頁面時會觸發(fā))
        emitter.onCompletion(() -> {
            log.info("SSE客戶端斷開連接(正常完成)");
            emitters.remove(emitter);
        });

        // 設置超時時的回調(diào)
        emitter.onTimeout(() -> {
            log.info("SSE客戶端斷開連接(超時)");
            emitters.remove(emitter);
        });

        // 設置錯誤時的回調(diào)
        emitter.onError(e -> {
            log.error("SSE客戶端連接錯誤", e);
            emitters.remove(emitter);
        });

        // 發(fā)送初始測試消息 - 告訴前端連接成功了
        try {
            News testNews = new News();
            testNews.setTitle("連接成功");
            testNews.setContent("您已成功連接到SSE服務");
            emitter.send(SseEmitter.event()
                    .name("news")        // 事件名稱,前端根據(jù)這個來區(qū)分不同消息
                    .data(testNews));    // 實際發(fā)送的數(shù)據(jù)
        } catch (IOException e) {
            log.error("發(fā)送初始消息失敗", e);
        }

        return emitter;
    }

    /**
     * 發(fā)送新聞通知 - 前端調(diào)用這個接口來發(fā)布新聞
     * 這個方法會把新聞推送給所有連接的瀏覽器
     */
    @PostMapping("/send-news")
    public void sendNews(@RequestBody News news) {
        List<SseEmitter> deadEmitters = new ArrayList<>();
        
        log.info("開始向 {} 個客戶端發(fā)送新聞: {}", emitters.size(), news.getTitle());

        // 向所有客戶端發(fā)送消息 - 關鍵:遍歷所有連接
        emitters.forEach(emitter -> {
            try {
                // 向每個客戶端發(fā)送新聞數(shù)據(jù)
                emitter.send(SseEmitter.event()
                        .name("news") // 事件名稱,前端監(jiān)聽這個事件
                        .data(news)); // 新聞數(shù)據(jù)
                log.info("新聞發(fā)送成功到客戶端");
            } catch (IOException e) {
                // 發(fā)送失敗,說明這個連接可能已經(jīng)斷開
                log.error("發(fā)送新聞到客戶端失敗", e);
                deadEmitters.add(emitter);
            }
        });

        // 移除已經(jīng)斷開的連接,避免內(nèi)存泄漏
        emitters.removeAll(deadEmitters);
        log.info("清理了 {} 個無效連接", deadEmitters.size());
    }

    // 新聞數(shù)據(jù)模型 - 簡單的Java類,用來存儲新聞數(shù)據(jù)
    public static class News {
        private String title;    // 新聞標題
        private String content;  // 新聞內(nèi)容

        // getters and setters
        public String getTitle() {
            return title;
        }

        public void setTitle(String title) {
            this.title = title;
        }

        public String getContent() {
            return content;
        }

        public void setContent(String content) {
            this.content = content;
        }
    }
}

后端代碼就這么多,代碼非常的簡單。

后端核心思路:

  • 用一個列表 emitters 保存所有瀏覽器的連接
  • 當有新聞發(fā)布時,遍歷這個列表,向每個連接發(fā)送消息
  • 及時清理斷開的連接,保持列表的清潔

前端實現(xiàn)(Vue3)

1. 數(shù)據(jù)類型定義

// src/types/news.ts

// 定義新聞數(shù)據(jù)的類型
export interface News {
  title: string     // 新聞標題
  content: string   // 新聞內(nèi)容
}

// 定義錯誤信息的類型
export interface SseError {
  message: string
}

2. SSE服務類 - 封裝連接邏輯

// src/utils/sseService.ts
import type { News } from '@/types/news'

// 定義回調(diào)函數(shù)的類型
type MessageCallback = (data: News) => void  // 收到消息時的回調(diào)
type ErrorCallback = (error: Event) => void  // 發(fā)生錯誤時的回調(diào)

class SseService {
  private eventSource: EventSource | null = null  // SSE連接對象
  private retryCount = 0                          // 重試次數(shù)
  private maxRetries = 3                          // 最大重試次數(shù)
  private retryDelay = 3000                       // 重試延遲(3秒)
  private onMessageCallback: MessageCallback | null = null  // 消息回調(diào)
  private onErrorCallback: ErrorCallback | null = null      // 錯誤回調(diào)

  /**
   * 訂閱SSE服務 - 建立連接并設置回調(diào)函數(shù)
   * @param onMessage 收到消息時的處理函數(shù)
   * @param onError 發(fā)生錯誤時的處理函數(shù)
   */
  public subscribe(onMessage: MessageCallback, onError: ErrorCallback): void {
    this.onMessageCallback = onMessage
    this.onErrorCallback = onError
    this.connect()  // 開始連接
  }

  /**
   * 建立SSE連接
   */
  private connect(): void {
    // 如果已有連接,先斷開
    if (this.eventSource) {
      this.disconnect()
    }

    // 創(chuàng)建新的SSE連接,連接到后端的/subscribe接口
    this.eventSource = new EventSource('/api/sse/subscribe')

    // 連接成功時的處理
    this.eventSource.addEventListener('open', () => {
      console.log('SSE連接建立成功')
      this.retryCount = 0 // 連接成功后重置重試計數(shù)
    })

    // 監(jiān)聽新聞事件 - 當后端發(fā)送name為"news"的消息時會觸發(fā)
    this.eventSource.addEventListener('news', (event: MessageEvent) => {
      try {
        // 解析后端發(fā)送的JSON數(shù)據(jù)
        const data: News = JSON.parse(event.data)
        console.log('收到新聞消息:', data)
        // 調(diào)用消息回調(diào)函數(shù),把新聞數(shù)據(jù)傳遞給組件
        this.onMessageCallback?.(data)
      } catch (error) {
        console.error('解析SSE消息失敗:', error)
      }
    })

    // 錯誤處理
    this.eventSource.onerror = (error: Event) => {
      console.error('SSE連接錯誤:', error)
      this.onErrorCallback?.(error)  // 調(diào)用錯誤回調(diào)
      this.disconnect()              // 斷開連接

      // 自動重連邏輯 - 網(wǎng)絡不穩(wěn)定時的自我恢復
      if (this.retryCount < this.maxRetries) {
        this.retryCount++
        console.log(`嘗試重新連接 (${this.retryCount}/${this.maxRetries})...`)
        setTimeout(() => this.connect(), this.retryDelay)
      } else {
        console.error('已達到最大重連次數(shù),停止重連')
      }
    }
  }

  /**
   * 取消訂閱 - 組件銷毀時調(diào)用
   */
  public unsubscribe(): void {
    this.disconnect()
    this.onMessageCallback = null
    this.onErrorCallback = null
  }

  /**
   * 斷開連接
   */
  private disconnect(): void {
    if (this.eventSource) {
      this.eventSource.close()  // 關閉連接
      this.eventSource = null
    }
  }
}

// 導出單例實例,整個應用共用同一個SSE服務
export default new SseService()

3. 新聞通知組件 - 顯示實時新聞

<!-- src/components/NewsNotification.vue -->
<template>
  <div class="news-container">
    <h2>新聞通知</h2>

    <!-- 連接狀態(tài) -->
    <div v-if="loading" class="status loading">連接服務器中...</div>
    <div v-if="error" class="status error">連接錯誤: {{ error }}</div>

    <!-- 新聞列表 -->
    <div class="news-list">
      <div v-for="(news, index) in newsList" :key="index" class="news-item">
        <h3>{{ news.title }}</h3>
        <p>{{ news.content }}</p>
        <div class="time">{{ getCurrentTime() }}</div>
      </div>
    </div>

    <!-- 空狀態(tài) -->
    <div v-if="newsList.length === 0 && !loading" class="no-news">暫無新聞通知</div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue'
import sseService from '@/utils/sseService'
import type { News } from '@/types/news'

export default defineComponent({
  name: 'NewsNotification',
  setup() {
    const newsList = ref<News[]>([]) // 新聞列表
    const loading = ref<boolean>(true) // 加載狀態(tài)
    const error = ref<string | null>(null) // 錯誤信息

    // 處理新消息
    const handleNewMessage = (news: News): void => {
      newsList.value.unshift(news) // 新消息放在最前面
    }

    // 處理錯誤
    const handleError = (err: Event): void => {
      error.value = (err as ErrorEvent)?.message || '連接服務器失敗'
      loading.value = false
    }

    // 獲取當前時間
    const getCurrentTime = (): string => {
      return new Date().toLocaleTimeString()
    }

    // 組件掛載時建立連接
    onMounted(() => {
      sseService.subscribe(handleNewMessage, handleError)
      loading.value = false
    })

    // 組件銷毀時斷開連接
    onUnmounted(() => {
      sseService.unsubscribe()
    })

    return {
      newsList,
      loading,
      error,
      getCurrentTime,
    }
  },
})
</script>

<style scoped>
.news-container {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.status {
  padding: 10px;
  margin: 10px 0;
  border-radius: 4px;
  text-align: center;
}

.loading {
  background-color: #e3f2fd;
  color: #1976d2;
}

.error {
  background-color: #ffebee;
  color: #d32f2f;
}

.news-list {
  margin-top: 20px;
}

.news-item {
  padding: 15px;
  margin-bottom: 10px;
  border: 1px solid #ddd;
  border-radius: 4px;
  background-color: #fafafa;
}

.news-item h3 {
  margin: 0 0 8px 0;
  color: #333;
  font-size: 16px;
}

.news-item p {
  margin: 0 0 8px 0;
  color: #666;
  line-height: 1.4;
}

.time {
  font-size: 12px;
  color: #999;
  text-align: right;
}

.no-news {
  padding: 40px 20px;
  text-align: center;
  color: #999;
  font-style: italic;
}
</style>

4. 新聞發(fā)送組件 - 管理員發(fā)送新聞

<!-- src/components/SendNewsForm.vue -->
<template>
  <div class="send-news-form">
    <h2>發(fā)送新聞通知</h2>
    <form @submit.prevent="sendNews">
      <div class="form-group">
        <label for="title">標題</label>
        <input id="title" v-model="news.title" type="text" required placeholder="輸入新聞標題" />
      </div>

      <div class="form-group">
        <label for="content">內(nèi)容</label>
        <textarea
          id="content"
          v-model="news.content"
          required
          placeholder="輸入新聞內(nèi)容"
          rows="4"
        ></textarea>
      </div>

      <button type="submit" :disabled="isSending">
        {{ isSending ? '發(fā)送中...' : '發(fā)送新聞' }}
      </button>

      <!-- 操作反饋 -->
      <div v-if="message" class="message" :class="messageType">
        {{ message }}
      </div>
    </form>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
import type { News } from '@/types/news'

export default defineComponent({
  name: 'SendNewsForm',
  setup() {
    // 表單數(shù)據(jù)
    const news = ref<News>({
      title: '',
      content: '',
    })

    // 界面狀態(tài)
    const isSending = ref<boolean>(false)
    const message = ref<string>('')
    const isSuccess = ref<boolean>(false)

    // 消息類型樣式
    const messageType = computed(() => {
      return isSuccess.value ? 'success' : 'error'
    })

    // 發(fā)送新聞
    const sendNews = async (): Promise<void> => {
      isSending.value = true
      message.value = ''

      try {
        const response = await fetch('/api/sse/send-news', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(news.value),
        })

        if (response.ok) {
          message.value = '新聞發(fā)送成功!'
          isSuccess.value = true
          // 清空表單
          news.value = { title: '', content: '' }
        } else {
          throw new Error('發(fā)送失敗')
        }
      } catch (err) {
        message.value = '發(fā)送新聞失敗'
        isSuccess.value = false
        console.error(err)
      } finally {
        isSending.value = false
      }
    }

    return {
      news,
      isSending,
      message,
      messageType,
      sendNews,
    }
  },
})
</script>

<style scoped>
.send-news-form {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.form-group {
  margin-bottom: 15px;
}

label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
  color: #333;
}

input,
textarea {
  width: 100%;
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  box-sizing: border-box;
  font-size: 14px;
}

input:focus,
textarea:focus {
  outline: none;
  border-color: #1976d2;
}

textarea {
  resize: vertical;
  min-height: 80px;
}

button {
  width: 100%;
  background-color: #1976d2;
  color: white;
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  font-size: 14px;
  cursor: pointer;
}

button:hover:not(:disabled) {
  background-color: #1565c0;
}

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

.message {
  margin-top: 15px;
  padding: 10px;
  border-radius: 4px;
  text-align: center;
  font-size: 14px;
}

.success {
  background-color: #e8f5e8;
  color: #2e7d32;
  border: 1px solid #c8e6c9;
}

.error {
  background-color: #ffebee;
  color: #c62828;
  border: 1px solid #ffcdd2;
}
</style>

5. 主頁面引用組件

<!-- src/Home.vue -->
<template>
  <div class="welcome">
    <SendNewsForm />
    <NewsNotification />
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import SendNewsForm from './components/SendNewsForm.vue'
import NewsNotification from './components/NewsNotification.vue'
</script>

全部代碼和注釋都在這里,可以直接啟動項目測試了。

測試

1. 測試多瀏覽器接收

  • 打開第一個瀏覽器(比如 Chrome)訪問應用
  • 打開第二個瀏覽器(比如 Firefox)訪問應用
  • 在任意瀏覽器中使用發(fā)送表單發(fā)布新聞
  • 觀察兩個瀏覽器是否都實時收到了新聞通知

2. 預期效果

  • 第一個瀏覽器打開:顯示"連接成功"
  • 第二個瀏覽器打開:顯示"連接成功"
  • 在任意瀏覽器發(fā)送新聞:兩個瀏覽器都立即顯示新新聞
  • 關閉一個瀏覽器:不影響另一個瀏覽器的正常使用

總結

通過這個小案例的源碼,我們學會了SSE的簡單使用:

  • SSE 的基本原理和使用方法
  • SpringBoot 如何維護多個客戶端連接
  • Vue3 組合式 API 的使用
  • 前后端分離架構的實時通信實現(xiàn)

這個方案非常適合新聞推送、系統(tǒng)通知、實時數(shù)據(jù)展示等場景。代碼簡單易懂,擴展性強,你也可以基于這個基礎添加更多功能。

到此這篇關于SpringBoot+Vue3整合SSE實現(xiàn)實時消息推送功能的文章就介紹到這了,更多相關SpringBoot實時消息推送內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!

相關文章

最新評論

贵港市| 交口县| 临城县| 金昌市| 灵武市| 广灵县| 宁远县| 满城县| 洛宁县| 临汾市| 垫江县| 石泉县| 牡丹江市| 周至县| 鸡东县| 墨脱县| 阿拉善右旗| 兴业县| 嘉黎县| 常山县| 义马市| 惠水县| 台南县| 尉氏县| 德钦县| 碌曲县| 临猗县| 隆化县| 铁岭县| 砀山县| 江都市| 陈巴尔虎旗| 周宁县| 新野县| 庆元县| 邵东县| 金华市| 梅河口市| 达孜县| 高州市| 巴东县|