全棧實戰!用 WebSocket 實現實時消息推送 + 動態進度條可視化
在傳統 Web 應用中,任務狀態查詢或通知推送往往依賴前端定時輪詢接口獲取數據。雖然這種方式實現簡單,但在數據頻繁變化或用戶量激增的場景下,頻繁的 HTTP 請求會引起數據庫壓力增大,響應延遲甚至系統性能下降。
本文將基于 Spring Boot + WebSocket 的技術棧,構建一個服務端主動推送消息的實時提醒系統,并可視化每項任務的進度。前端將通過 WebSocket 進行一次性連接,并實時響應后端推送的最新數據,從而極大提升用戶體驗與系統性能。
系統功能概覽
- 待辦數量實時推送
- 通知紅點自動刷新
- 支持 WebSocket 持久連接
- 動態進度條展示任務完成情況
- 前后端獨立交互,解耦式開發結構
依賴配置(Maven)
添加必要的依賴于 pom.xml
文件中:
<!-- MyBatis Plus & MySQL -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- WebSocket -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
<version>2.1.6.RELEASE</version>
</dependency>
數據庫結構設計
建立兩張表用于模擬待辦任務及其子任務進度:
CREATE TABLE t_todo (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(255) COMMENT '用戶名稱',
name VARCHAR(255) COMMENT '待辦標題'
) COMMENT='待辦任務主表';
CREATE TABLE t_todo_attr (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
todo_id BIGINT COMMENT '主表ID',
status INT COMMENT '完成狀態 1為已完成'
) COMMENT='待辦任務進度子表';
一條 t_todo
記錄表示一個任務,對應若干 t_todo_attr
子任務進度項。
WebSocket 服務端配置
WebSocket 注冊配置
// /src/main/java/com/icoderoad/config/WebSocketConfig.java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
實現主任務通知服務
// /src/main/java/com/icoderoad/ws/WebSocketTodoServer.java
@ServerEndpoint("/ws/todo/{username}")
@Component
public class WebSocketTodoServer {
private static final Map<String, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void open(Session session, @PathParam("username") String username) {
sessions.put(username, session);
int count = SpringContextUtil.getBean(TodoService.class)
.count(new LambdaQueryWrapper<Todo>().eq(Todo::getUserName, username));
send(session, String.valueOf(count));
}
@OnClose
public void close(@PathParam("username") String username) {
sessions.remove(username);
}
@OnMessage
public void message(String msg) {}
@OnError
public void error(Session session, Throwable throwable) {
throwable.printStackTrace();
}
public void sendInfo(String username, String msg) {
Session session = sessions.get(username);
send(session, msg);
}
private void send(Session session, String msg) {
if (session != null) {
synchronized (session) {
try {
session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
后端接口實現
// /src/main/java/com/icoderoad/controller/TodoController.java
@RestController
@RequestMapping("/todo")
public class TodoController {
@Autowired private TodoService todoService;
@Autowired private WebSocketTodoServer wsServer;
@PostMapping("/insert")
public ResponseUtils insert(@RequestParam String todoName, @RequestParam String userName) {
Todo todo = new Todo();
todo.setName(todoName);
todo.setUserName(userName);
todoService.save(todo);
int count = todoService.count(new LambdaQueryWrapper<Todo>().eq(Todo::getUserName, userName));
wsServer.sendInfo(userName, String.valueOf(count));
return ResponseUtils.success(todoName);
}
@GetMapping("/list")
public ResponseUtils list(@RequestParam String userName) {
List<Todo> todos = todoService.list(new LambdaQueryWrapper<Todo>().eq(Todo::getUserName, userName));
return ResponseUtils.success(todos);
}
}
前端頁面展示
<!-- /src/main/resources/static/index.html -->
<div class="message-container" onclick="toggleTodo()">
<div class="bell-icon"></div>
<span class="message-count">0</span>
</div>
<div class="todo-section" id="todoSection" style="display:none;"></div>
<script>
const socket = new WebSocket('ws://localhost:8077/ws/todo/張三');
socket.onmessage = (event) => {
document.querySelector('.message-count').textContent = event.data;
};
async function toggleTodo() {
const section = document.getElementById('todoSection');
section.style.display = section.style.display === 'none' ? 'block' : 'none';
if (section.style.display === 'block') {
const res = await fetch('/todo/list?userName=張三');
const data = await res.json();
section.innerHTML = data.data.map(t => `<div>${t.name}</div>`).join('');
}
}
</script>
子任務進度 WebSocket(進度條)
// /src/main/java/com/icoderoad/ws/WebSocketTodoAttrServer.java
@ServerEndpoint("/ws/todo/attr/{todoId}")
@Component
public class WebSocketTodoAttrServer {
private static final Map<String, Session> attrSessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("todoId") String todoId) {
attrSessions.put(todoId, session);
String progress = SpringContextUtil.getBean(TodoAttrService.class).progress(Long.valueOf(todoId));
send(session, progress);
}
public void sendInfo(String todoId, String msg) {
send(attrSessions.get(todoId), msg);
}
private void send(Session session, String msg) {
try {
if (session != null) session.getBasicRemote().sendText(msg);
} catch (IOException e) {
e.printStackTrace();
}
}
}
任務進度更新接口
// /src/main/java/com/icoderoad/controller/TodoAttrController.java
@PostMapping("/attr/update")
public ResponseUtils updateAttr(@RequestParam Long id) {
todoAttrService.updateById(new TodoAttr(id, 1));
TodoAttr attr = todoAttrService.getById(id);
webSocketTodoAttrServer.sendInfo(String.valueOf(attr.getTodoId()),
todoAttrService.progress(attr.getTodoId()));
return ResponseUtils.success();
}
結語:高性能實時系統構建的利器
借助 WebSocket 實現的實時通信機制,我們有效地解決了輪詢帶來的性能瓶頸和用戶體驗問題。無論是消息推送,待辦提醒,還是任務進度的動態刷新,WebSocket 都提供了更優雅與高效的解決方案。
未來在構建具有實時性要求的系統(如 IM 聊天、實時告警、系統監控等)時,WebSocket 可以作為首選的通信技術基礎,而非傳統的“輪詢 + 回調”。