嘉興企業(yè)網(wǎng)站開發(fā)可以免費(fèi)發(fā)帖的網(wǎng)站
大家好,今天用SpringBoot、vue寫了一個(gè)仿ChatGPT官網(wǎng)聊天的打字機(jī)效果。
所有代碼地址:gitee代碼地址 ,包含前端和后端,可以直接運(yùn)行使用本技術(shù)實(shí)現(xiàn)的項(xiàng)目:aicnn.cn,歡迎大家體驗(yàn)
如果文章知識(shí)點(diǎn)有錯(cuò)誤的地方,請(qǐng)指正!大家一起學(xué)習(xí),一起進(jìn)步。
本文主要應(yīng)用的技術(shù)有:SpringBoot、Vue、Reactive、WebFlux、EventSource等,學(xué)習(xí)和練手的好項(xiàng)目。實(shí)現(xiàn)效果如下:
準(zhǔn)備好了嗎,let’s get it!
采用本文技術(shù)實(shí)現(xiàn)的前后端項(xiàng)目,點(diǎn)擊體驗(yàn)使用:aicnn.cn
文章目錄
- 前言
- 項(xiàng)目運(yùn)行
- SSE技術(shù)概覽
- 什么是Server-Sent Events?
- SSE與WebSockets的區(qū)別
- 為什么選擇SSE?
- SSE的應(yīng)用場景
- 在Spring Boot中實(shí)現(xiàn)SSE
- 設(shè)置SSE
- 處理連接和事件
- 處理異常和斷開連接
- 一些實(shí)用的提示
- Vue前端對(duì)SSE的處理
- 在Vue中接收SSE
- 保證流暢的用戶體驗(yàn)
- 異常處理和重連
- 小結(jié)
- 實(shí)際案例分析:實(shí)時(shí)通知系統(tǒng)
- 場景描述
- 為什么選擇SSE?
- 實(shí)現(xiàn)概覽
- 體驗(yàn)優(yōu)化
- 可能的挑戰(zhàn)
- 結(jié)語
前言
Web開發(fā)的世界永遠(yuǎn)充滿驚喜,不是嗎?每當(dāng)我們認(rèn)為自己掌握了所有的技巧和工具,總會(huì)有新的技術(shù)出現(xiàn),挑戰(zhàn)我們的知識(shí)庫。今天,我們要探討的這項(xiàng)技術(shù)可能對(duì)一些人來說并不陌生,但對(duì)于其他人來說,則像是新發(fā)現(xiàn)的寶藏。沒錯(cuò),我在說的是Server-Sent Events(SSE)。
你可能會(huì)問:“SSE是什么?”簡單來說,SSE是一種讓服務(wù)器實(shí)時(shí)向客戶端發(fā)送更新的技術(shù)。但別誤會(huì),這不是另一個(gè)WebSockets。SSE和WebSockets之間的斗爭,有點(diǎn)像是電影《星球大戰(zhàn)》中的帝國和反抗軍的斗爭——兩者都有其優(yōu)勢和用武之地,但戰(zhàn)場完全不同。SSE是為了解決特定類型的實(shí)時(shí)通訊問題而生,而不是為了取代WebSockets。
在本文中,我們將一探究竟,看看SSE到底是什么魔法,以及如何在Spring Boot應(yīng)用程序和VUE中輕松實(shí)現(xiàn)它。當(dāng)然,我們也不會(huì)忘記前端的小伙伴們——我們將一起探索如何在Vue應(yīng)用中接收和處理這些實(shí)時(shí)數(shù)據(jù),并一起實(shí)現(xiàn)類似ChatGPT官網(wǎng)的聊天打字機(jī)效果。準(zhǔn)備好了嗎?讓我們開始這趟探索之旅吧!
項(xiàng)目運(yùn)行
所有代碼地址:代碼地址 ,包含前端和后端,可以直接運(yùn)行。
springboot安裝依賴,并設(shè)置對(duì)應(yīng)的sd-key即可:
api key 的獲取方式如下:
- 第一步:打開aicnn.cn
- 第二步:進(jìn)入設(shè)置頁面
- 第三步:點(diǎn)擊創(chuàng)建新的秘鑰
- 第四步:復(fù)制密鑰值,替換上面代碼中的
sk-*******
,替換后的代碼如下所示:.header("Authorization", "Bearer sk-1234567890123456789")
前端項(xiàng)目采用vue3實(shí)現(xiàn),
在項(xiàng)目中,使用如下命令運(yùn)行項(xiàng)目,即可運(yùn)行前端:
yarn install
yarn serve
SSE技術(shù)概覽
什么是Server-Sent Events?
在深入探索Server-Sent Events(SSE)之前,讓我們先搞清楚它到底是什么。簡單地說,SSE是一種允許服務(wù)器主動(dòng)向客戶端發(fā)送信息的技術(shù)。與傳統(tǒng)的請(qǐng)求-響應(yīng)模式不同,SSE建立了一個(gè)單向通道,使得服務(wù)器可以實(shí)時(shí)發(fā)送更新。這聽起來有點(diǎn)像WebSockets,但SSE的工作方式和用例卻大有不同。
SSE與WebSockets的區(qū)別
SSE和WebSockets都是實(shí)現(xiàn)實(shí)時(shí)通信的技術(shù),但它們各有千秋。WebSockets提供了一個(gè)全雙工的通信渠道,允許數(shù)據(jù)在客戶端和服務(wù)器之間雙向流動(dòng)。相比之下,SSE是單向的——僅從服務(wù)器到客戶端。這意味著如果你需要從客戶端向服務(wù)器發(fā)送數(shù)據(jù),SSE可能就不是最佳選擇了。SSE的優(yōu)勢在于它的簡單性和輕量級(jí),特別適用于那些僅需要服務(wù)器單向傳送數(shù)據(jù)的場景,比如實(shí)時(shí)新聞更新、股票行情等。
為什么選擇SSE?
現(xiàn)在你可能在想:“為什么我要使用SSE而不是其他技術(shù)?”好問題!首先,SSE在瀏覽器中有很好的支持,這使得它非常容易實(shí)現(xiàn)。其次,由于SSE是基于HTTP的,它可以利用現(xiàn)有的HTTP協(xié)議特性,如緩存、認(rèn)證等。這些特性在WebSockets中可能需要額外的處理。最后,SSE的輕量級(jí)特性使其成為一種高效的實(shí)時(shí)數(shù)據(jù)傳輸方式,尤其是當(dāng)你只需要服務(wù)器到客戶端的單向數(shù)據(jù)流時(shí)。
SSE的應(yīng)用場景
SSE最適合的是那些需要服務(wù)器定期或不定期推送信息到客戶端的場景。例如,如果你正在開發(fā)一個(gè)需要顯示實(shí)時(shí)消息、股票行情或任何形式實(shí)時(shí)數(shù)據(jù)的應(yīng)用,SSE是一個(gè)不錯(cuò)的選擇。它的輕量級(jí)和易用性使其成為這些類型應(yīng)用的理想選擇。
在Spring Boot中實(shí)現(xiàn)SSE
設(shè)置SSE
在Spring Boot中實(shí)現(xiàn)SSE并不復(fù)雜。其核心在于使用Spring框架的webflux
。使用webflux類能夠創(chuàng)建一個(gè)持久的連接,使服務(wù)器能夠向客戶端發(fā)送多個(gè)事件,而無需每次都建立新的連接。
先在pom中引入相關(guān)依賴:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
然后需要在Controller中創(chuàng)建一個(gè)方法來返回Flux
實(shí)例。這個(gè)方法將被映射到特定的URL,客戶端將使用這個(gè)URL來接收事件。
package cn.aicnn.chatssespringboot.controller;import cn.aicnn.chatssespringboot.dto.AIAnswerDTO;
import cn.aicnn.chatssespringboot.service.GptServiceImpl;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;import javax.annotation.Resource;@RestController
public class ChatController {//用于流式請(qǐng)求第三方的實(shí)現(xiàn)類@ResourceGptServiceImpl gptService;//通過stream返回流式數(shù)據(jù)@GetMapping(value = "/completions", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<AIAnswerDTO>> getStream(@RequestParam("messages")String messages) {return gptService.doChatGPTStream(messages)//實(shí)現(xiàn)類發(fā)送消息并獲取返回結(jié)果.map(aiAnswerDTO -> ServerSentEvent.<AIAnswerDTO>builder()//進(jìn)行結(jié)果的封裝,再返回給前端.data(aiAnswerDTO).build()).onErrorResume(e -> Flux.empty());//發(fā)生異常時(shí)發(fā)送空對(duì)象}}
處理連接和事件
一旦有客戶端連接到這個(gè)URL,可以通過調(diào)用GptServiceImpl
實(shí)例的doChatGPTStream
方法來發(fā)送事件。這些事件可以是簡單的字符串消息,也可以是更復(fù)雜的數(shù)據(jù)結(jié)構(gòu),如JSON對(duì)象。記住,SSE的設(shè)計(jì)初衷是輕量級(jí)和簡單,所以你發(fā)送的每個(gè)事件都應(yīng)當(dāng)是獨(dú)立的和自包含的。
GptServiceImpl
的實(shí)現(xiàn)方式如下,也是springboot后端實(shí)現(xiàn)的重點(diǎn)
package cn.aicnn.chatssespringboot.service;import cn.aicnn.chatssespringboot.dto.AIAnswerDTO;
import cn.aicnn.chatssespringboot.dto.ChatRequestDTO;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;import javax.annotation.PostConstruct;
import java.util.*;/*** @author aicnn.cn* @date 2023/2/13* @description: aicnn.cn**/
@Service
public class GptServiceImpl {//webflux的clientprivate WebClient webClient;//用于讀取第三方的返回結(jié)果private ObjectMapper objectMapper = new ObjectMapper();@PostConstructpublic void postConstruct() {this.webClient = WebClient.builder()//創(chuàng)建webflux的client.baseUrl("https://api.aicnn.cn/v1")//填寫對(duì)應(yīng)的api地址.defaultHeader("Content-Type", "application/json")//設(shè)置默認(rèn)請(qǐng)求類型.build();}//請(qǐng)求stream的主題public Flux<AIAnswerDTO> doChatGPTStream(String requestQuestion) {//構(gòu)建請(qǐng)求對(duì)象ChatRequestDTO chatRequestDTO = new ChatRequestDTO();chatRequestDTO.setModel("gpt-3.5-turbo");//設(shè)置模型chatRequestDTO.setStream(true);//設(shè)置流式返回ChatRequestDTO.ReqMessage message = new ChatRequestDTO.ReqMessage();//設(shè)置請(qǐng)求消息,在此可以加入自己的promptmessage.setRole("user");//用戶消息message.setContent(requestQuestion);//用戶請(qǐng)求內(nèi)容ArrayList<ChatRequestDTO.ReqMessage> messages = new ArrayList<>();messages.add(message);chatRequestDTO.setMessages(messages);//設(shè)置請(qǐng)求消息//構(gòu)建請(qǐng)求jsonString paramJson = JSONUtil.toJsonStr(chatRequestDTO);;//使用webClient發(fā)送消息return this.webClient.post().uri("/chat/completions")//請(qǐng)求uri.header("Authorization", "Bearer sk-**************")//設(shè)置成自己的key,獲得key的方式可以在下文查看.header(HttpHeaders.ACCEPT, MediaType.TEXT_EVENT_STREAM_VALUE)//設(shè)置流式響應(yīng).contentType(MediaType.APPLICATION_JSON).body(BodyInserters.fromValue(paramJson)).retrieve().bodyToFlux(String.class).flatMap(result -> handleWebClientResponse(result));//接收到消息的處理方法}private Flux<AIAnswerDTO> handleWebClientResponse(String resp) {if (StrUtil.equals("[DONE]",resp)){//[DONE]是消息結(jié)束標(biāo)識(shí)return Flux.empty();}try {JsonNode jsonNode = objectMapper.readTree(resp);AIAnswerDTO result = objectMapper.treeToValue(jsonNode, AIAnswerDTO.class);//將獲得的結(jié)果轉(zhuǎn)成對(duì)象if (CollUtil.size(result.getChoices()) > 0 && !Objects.isNull(result.getChoices().get(0)) &&!StrUtil.isBlank(result.getChoices().get(0).delta.getError())){//判斷是否有異常throw new RuntimeException(result.getChoices().get(0).delta.getError());}return Flux.just(result);//返回獲得的結(jié)果} catch (Exception e) {throw new RuntimeException(e.getMessage());}}
}
在這里,我們請(qǐng)求的api地址設(shè)置為https://api.aicnn.cn/v1,并且設(shè)置從aicnn.cn獲取的api key即可。
處理異常和斷開連接
在使用SSE時(shí),處理異常和斷開連接也非常重要。確保在客戶端斷開連接或發(fā)生異常時(shí),正確地關(guān)閉webflux
實(shí)例。這有助于避免資源泄露和其他潛在問題。
一些實(shí)用的提示
- 超時(shí)管理:SSE連接可能因?yàn)槌瑫r(shí)而被關(guān)閉。確保妥善處理這種情況。
- 錯(cuò)誤處理:適當(dāng)?shù)靥幚砜赡馨l(fā)生的異常,如網(wǎng)絡(luò)問題或客戶端斷開連接。
- 資源清理:在連接結(jié)束時(shí)清理資源,確保應(yīng)用的健康和性能。
至此,我們就完成了SpringBoot的SSE后端開發(fā)。
Vue前端對(duì)SSE的處理
在Vue中接收SSE
在Vue應(yīng)用中接收SSE消息是相對(duì)直截了當(dāng)?shù)摹P枰龅幕旧暇褪窃赩ue組件中創(chuàng)建一個(gè)新的EventSource
實(shí)例,并指向你的Spring Boot應(yīng)用中設(shè)置的SSE URL,本文使用EventSource作為示例,也可以選擇axios或@microsoft/fetch-event-source發(fā)送post請(qǐng)求的SSE請(qǐng)求,使用另外兩種的好處是可以控制header,攜帶token信息,以便于控制權(quán)限。
this.eventSource = new EventSource('http://127.0.0.1:8080/completions?messages='+this.inputText);
一旦建立了連接,就可以定義各種事件監(jiān)聽器來處理從服務(wù)器接收到的消息。在Vue中,這通常涉及到更新組件的數(shù)據(jù)屬性,這些屬性又通過Vue的響應(yīng)式系統(tǒng)自動(dòng)更新UI。
sendSSEMessage() {// 只有當(dāng)eventSource不存在時(shí)才創(chuàng)建新的EventSource連接if (!this.eventSource) {this.messages.push({text: this.inputText, isMine: true});this.messages.push({text: "", isMine: false});// 創(chuàng)建新的EventSource連接this.eventSource = new EventSource('http://127.0.0.1:8080/completions?messages='+this.inputText);// 設(shè)置消息接收的回調(diào)函數(shù)this.eventSource.onmessage = (event) => {const data = JSON.parse(event.data);this.messages[this.messages.length - 1].text += data.choices[0].delta.content;};// 可選:監(jiān)聽錯(cuò)誤事件,以便在出現(xiàn)問題時(shí)能夠重新連接或處理錯(cuò)誤this.eventSource.onerror = (event) => {console.error("EventSource failed:", event);this.eventSource.close(); // 關(guān)閉出錯(cuò)的連接this.eventSource = null; // 重置eventSource變量,允許重建連接};}}
保證流暢的用戶體驗(yàn)
當(dāng)處理實(shí)時(shí)數(shù)據(jù)時(shí),保證一個(gè)流暢且不中斷的用戶體驗(yàn)至關(guān)重要。在Vue中,這意味著需要確保UI的更新是平滑和高效的。幸運(yùn)的是,Vue的響應(yīng)式系統(tǒng)會(huì)處理大部分重活,但你仍需要注意不要進(jìn)行不必要的大規(guī)模DOM操作或數(shù)據(jù)處理。
異常處理和重連
處理連接中斷或其他異常也是至關(guān)重要的。你可能需要在失去連接時(shí)嘗試重新連接,或者至少提醒用戶當(dāng)前的連接狀態(tài)。這可以通過監(jiān)聽EventSource
的錯(cuò)誤事件并采取適當(dāng)?shù)男袆?dòng)來實(shí)現(xiàn)。
小結(jié)
將SSE與Vue結(jié)合使用,可以為用戶提供一個(gè)富有動(dòng)態(tài)性和實(shí)時(shí)性的界面。無論是實(shí)時(shí)消息、通知,還是實(shí)時(shí)數(shù)據(jù)流,SSE都能讓Vue應(yīng)用更加生動(dòng)和實(shí)用。
當(dāng)然,除了使用SSE進(jìn)行文字聊天的打字機(jī)模式刪除,SSE技術(shù)還有應(yīng)用:
實(shí)際案例分析:實(shí)時(shí)通知系統(tǒng)
場景描述
想象一下,你正在為一個(gè)大型在線零售平臺(tái)工作,該平臺(tái)需要一種方法來實(shí)時(shí)通知其用戶關(guān)于訂單狀態(tài)的更新。在這種情況下,使用SSE來推送通知變得非常有意義。這不僅提高了用戶體驗(yàn),還減少了服務(wù)器的負(fù)載,因?yàn)椴恍枰l繁的輪詢。
為什么選擇SSE?
在這個(gè)場景中,SSE提供了一種高效且輕量的方式來實(shí)時(shí)更新用戶的訂單狀態(tài)。由于訂單狀態(tài)更新不需要來自客戶端的即時(shí)響應(yīng),SSE的單向數(shù)據(jù)流完全符合需求。相比于使用WebSockets,這種方式更加簡單且資源消耗更少。
實(shí)現(xiàn)概覽
在Spring Boot后端,你可以設(shè)置一個(gè)SSE端點(diǎn),以便在訂單狀態(tài)發(fā)生變化時(shí)發(fā)送通知。
在Vue前端,可以創(chuàng)建一個(gè)監(jiān)聽SSE消息的組件。當(dāng)接收到新的訂單狀態(tài)更新時(shí),該組件可以相應(yīng)地更新UI,為用戶提供實(shí)時(shí)反饋。
體驗(yàn)優(yōu)化
為了優(yōu)化用戶體驗(yàn),還可以在前端實(shí)現(xiàn)一些附加功能,比如當(dāng)用戶離開頁面時(shí)暫停事件流,或者在網(wǎng)絡(luò)不穩(wěn)定導(dǎo)致連接斷開時(shí)自動(dòng)重連。
可能的挑戰(zhàn)
當(dāng)然,實(shí)現(xiàn)SSE并非沒有挑戰(zhàn)。例如,你需要確保在高并發(fā)場景下后端能夠有效地處理大量的連接。此外,處理網(wǎng)絡(luò)不穩(wěn)定情況下的重連機(jī)制也是需要考慮的。
結(jié)語
回顧我們今天的探索,我們可以看到Server-Sent Events(SSE)在現(xiàn)代web開發(fā)中的價(jià)值和應(yīng)用潛力。通過在Spring Boot后端輕松實(shí)現(xiàn)SSE,并在Vue前端高效處理這些實(shí)時(shí)事件,我們可以為用戶創(chuàng)造出更加動(dòng)態(tài)和互動(dòng)的體驗(yàn),并使用SSE從0開始實(shí)現(xiàn)了類似ChatGPT聊天的前后端,以及打字機(jī)效果的輸出。
SSE提供了一種簡單、高效的方法來處理單向數(shù)據(jù)流。它是那些需要服務(wù)器向客戶端實(shí)時(shí)推送數(shù)據(jù)的應(yīng)用的理想選擇。無論是實(shí)時(shí)通知、實(shí)時(shí)新聞更新,還是其他任何需要實(shí)時(shí)數(shù)據(jù)的場景,SSE都能夠提供一種輕量級(jí)且易于實(shí)現(xiàn)的解決方案。
當(dāng)然,每種技術(shù)都有其適用場景。SSE并不是萬能的,它最適合于那些不需要客戶端向服務(wù)器發(fā)送數(shù)據(jù)的場景。在選擇技術(shù)方案時(shí),了解你的需求和各種技術(shù)的優(yōu)勢及局限性至關(guān)重要。
最后,鼓勵(lì)所有的開發(fā)者嘗試將SSE集成到自己的項(xiàng)目中。感謝你和我一起探索SSE的奇妙世界?,F(xiàn)在,該是你動(dòng)手實(shí)踐的時(shí)候了!祝你編碼愉快!