全棧實戰(zhàn)!用 WebSocket 實現(xiàn)實時消息推送 + 動態(tài)進度條可視化
在傳統(tǒng) Web 應(yīng)用中,任務(wù)狀態(tài)查詢或通知推送往往依賴前端定時輪詢接口獲取數(shù)據(jù)。雖然這種方式實現(xiàn)簡單,但在數(shù)據(jù)頻繁變化或用戶量激增的場景下,頻繁的 HTTP 請求會引起數(shù)據(jù)庫壓力增大,響應(yīng)延遲甚至系統(tǒng)性能下降。
本文將基于 Spring Boot + WebSocket 的技術(shù)棧,構(gòu)建一個服務(wù)端主動推送消息的實時提醒系統(tǒng),并可視化每項任務(wù)的進度。前端將通過 WebSocket 進行一次性連接,并實時響應(yīng)后端推送的最新數(shù)據(jù),從而極大提升用戶體驗與系統(tǒng)性能。
系統(tǒng)功能概覽
- 待辦數(shù)量實時推送
- 通知紅點自動刷新
- 支持 WebSocket 持久連接
- 動態(tài)進度條展示任務(wù)完成情況
- 前后端獨立交互,解耦式開發(fā)結(jié)構(gòu)
依賴配置(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>
數(shù)據(jù)庫結(jié)構(gòu)設(shè)計
建立兩張表用于模擬待辦任務(wù)及其子任務(wù)進度:
CREATE TABLE t_todo (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(255) COMMENT '用戶名稱',
name VARCHAR(255) COMMENT '待辦標題'
) COMMENT='待辦任務(wù)主表';
CREATE TABLE t_todo_attr (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
todo_id BIGINT COMMENT '主表ID',
status INT COMMENT '完成狀態(tài) 1為已完成'
) COMMENT='待辦任務(wù)進度子表';
一條 t_todo
記錄表示一個任務(wù),對應(yīng)若干 t_todo_attr
子任務(wù)進度項。
WebSocket 服務(wù)端配置
WebSocket 注冊配置
// /src/main/java/com/icoderoad/config/WebSocketConfig.java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
實現(xiàn)主任務(wù)通知服務(wù)
// /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();
}
}
}
}
}
后端接口實現(xiàn)
// /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>
子任務(wù)進度 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();
}
}
}
任務(wù)進度更新接口
// /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();
}
結(jié)語:高性能實時系統(tǒng)構(gòu)建的利器
借助 WebSocket 實現(xiàn)的實時通信機制,我們有效地解決了輪詢帶來的性能瓶頸和用戶體驗問題。無論是消息推送,待辦提醒,還是任務(wù)進度的動態(tài)刷新,WebSocket 都提供了更優(yōu)雅與高效的解決方案。
未來在構(gòu)建具有實時性要求的系統(tǒng)(如 IM 聊天、實時告警、系統(tǒng)監(jiān)控等)時,WebSocket 可以作為首選的通信技術(shù)基礎(chǔ),而非傳統(tǒng)的“輪詢 + 回調(diào)”。