Implementing a Typing Effect with Spring WebFlux and Server‑Sent Events
This article explains how to use Spring WebFlux's reactive, non‑blocking model to create a server‑sent events (SSE) endpoint that streams text data, and demonstrates a front‑end Vue implementation that splits the stream into characters to produce a realistic typing animation.
1. Introduction
The author introduces the goal of reproducing a typing effect commonly seen in online chat widgets by leveraging Spring WebFlux on the back‑end and a Vue front‑end that consumes Server‑Sent Events (SSE).
2. Spring WebFlux Overview
Spring WebFlux is a reactive, asynchronous, non‑blocking web framework built on the Reactive Streams standard, enabling high‑concurrency request handling. In Spring Boot 2.x it is provided via the spring‑boot‑starter‑webflux dependency and supports both functional and annotation‑based routing.
3. Differences Between Spring WebFlux and Spring MVC
Difference
Spring WebFlux
Spring MVC
Programming Model
Asynchronous, non‑blocking, based on Reactive Streams.
Synchronous, blocking, each request occupies a dedicated thread.
Concurrency Handling
Non‑blocking I/O with a small thread pool.
Blocking I/O via the Servlet API.
Typical Use Cases
High‑throughput, real‑time communication scenarios.
Traditional, latency‑tolerant applications.
4. Core Reactive Concepts
Flux : Represents an asynchronous sequence of 0..N elements. Flux<Object> obj = Flux.just(1,2,3);
Mono : Represents an asynchronous sequence of 0..1 element. Mono<Object> obj = Mono.just("Spring WebFlux");
Scheduler : Controls the thread on which reactive operations run. Scheduler scheduler = Schedulers.parallel();
Operators : Transformations applied to Flux or Mono . Flux<Integer> val = obj.map(n -> n+1);
5. Integrating Spring WebFlux into a Spring Boot Project
5.1 Add Dependency
<!--Spring WebFlux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>3.2.5</version>
</dependency>5.2 Utility Class for Reactive Responses
/**
* @author: jiangjs
*/
@Component
public class ChatMsgFluxUnit<T,R> {
public Mono<R> getMonoChatMsg(T t, Function<T,R> function){
return Mono.just(t).map(function).onErrorResume(e->Mono.empty());
}
public Flux<R> getMoreChatMsg(T t, Function<T,R> function){
return Flux.just(t).map(function).onErrorResume(e->Flux.empty());
}
}5.3 Controller for SSE Endpoint
/**
* @author: jiangjs
*/
@RestController
@RequestMapping("/chat")
public class ChatMsgController {
@Resource
private ChatMsgFluxUnit<String,Map<String,String>> chatMsgFluxUnit;
@GetMapping(value = "/flux", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@CrossOrigin(origins = "*")
public Flux<Map<String,String>> flux(@RequestParam("content") String content){
return chatMsgFluxUnit.getMoreChatMsg("你要问啥?请详细描述一下你的问题......", s -> {
Map<String, String> map = new HashMap<>(1);
map.put("msg", s);
return map;
});
}
}The endpoint streams a map containing a msg field; the client receives each chunk as an SSE event.
6. Front‑End Integration and Typing Effect
The Vue component creates an EventSource pointing to the SSE endpoint, splits the received string into characters, and appends each character to the displayed message with a timed delay to simulate typing.
6.1 Creating the EventSource
this.eventSource = new EventSource('http://127.0.0.1:8008/word/chat/main?content='+this.inputText);6.2 Typing Logic
const strings = data.msg.split("");
strings.forEach((obj,index) => {
setTimeout(() => {
this.messages[this.messages.length - 1].text += obj;
if (index > 0 && index % 20 === 0) {
this.messages[this.messages.length - 1].text += "\n";
}
},index*80);
});6.3 Complete Vue Component
<template>
<div class="bg-gray-100 h-screen flex flex-col max-w-lg mx-auto">
... (template markup omitted for brevity) ...
</div>
</template>
<script>
export default {
name: 'Home',
data() {
return {
inputText: null,
messages: [
{ text: "你好", isMine: true },
{ text: "你好,我是咨询小哥,有什么我能帮助你的吗?", isMine: false },
],
eventSource: null,
};
},
beforeUnmount() {
if (this.eventSource) this.eventSource.close();
},
methods:{
sendSSEMessage() {
if (!this.eventSource) {
this.messages.push({text: this.inputText, isMine: true});
this.messages.push({text: "", isMine: false});
this.eventSource = new EventSource('http://127.0.0.1:8008/word/chat/main?content='+this.inputText);
this.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
const strings = data.msg.split("");
strings.forEach((obj,index) => {
setTimeout(() => {
this.messages[this.messages.length - 1].text += obj;
if (index > 0 && index % 20 === 0) {
this.messages[this.messages.length - 1].text += "\n";
}
},index*80);
});
};
this.eventSource.onerror = (event) => {
console.error("EventSource failed:", event);
this.eventSource.close();
this.eventSource = null;
};
}
}
}
}
</script>
<style scoped>
</style>7. Conclusion
The article demonstrates a full stack solution: a reactive Spring WebFlux SSE endpoint that streams text and a Vue front‑end that consumes the stream to render a smooth typing animation, providing a practical example of combining backend reactive programming with modern front‑end techniques.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.