基于 SSE 协议实现任务进度条的实时显示——Vue2 + Spring Boot 实践

750 阅读2分钟

一、引言

在 Web 应用开发中,实时显示任务进度是一个常见的需求。传统的轮询(Polling)和 WebSocket 是实现实时性的两种常用技术。然而,轮询方式存在资源浪费和延迟问题,而 WebSocket 则在某些场景下显得过于重量级。本文将介绍一种基于 Server-Sent Events(SSE)协议的实现方案,并结合 Vue2 和 Spring Boot 技术栈进行实战演示。

二、Server-Sent Events(SSE)简介

官网:www.w3cschool.cn/nwfchn/wpi3…

Server-Sent Events(SSE)是 HTML5 的一部分,允许服务器向客户端推送信息。与 WebSocket 相比,SSE 的优势在于:

  1. 服务器到客户端的单向通信,适用于只需要服务器推送数据的场景。
  2. 基于普通的 HTTP 请求,不需要额外的协议或库。
  3. 支持自动重连和事件流。

三、实战步骤

1、环境准备

本例使用 Vue2 + Spring Boot 技术栈。确保已安装以下环境:

2、实现服务器端(Spring Boot)

在 Spring Boot 项目中创建一个 Controller,用于发送 SSE 事件:

@RestController
public class ProgressController {
 
    // 模拟进度条
    @GetMapping("/progress")
    public SseEmitter getProgress() {
        SseEmitter emitter = new SseEmitter();
        executorService.execute(() -> {
            try {
                for (int i = 0; i <= 100; i++) {
                    Thread.sleep(100); // 模拟任务执行时间
                    // 调用发送方法向浏览器发送 进度信息
                    emitter.send(i);
                }
                emitter.complete();
            } catch (Exception e) {
                emitter.completeWithError(e);
            }
        });
        return emitter;
    }
    
     /**
     * 用于创建连接(将用户注册到server中)
     */
    @GetMapping("/connect")
    public SseEmitter connect() {
        // 这里我的项目自己保存了登录时的用户信息,总之根据项目来看要将 sse链接和用户绑定
        Long userId1 = LoginHelper.getUserId();
        return SseEmitterServer.connect(userId1+"");
    }
    
    /**
     * 实际任务处理场景
     */
    @GetMapping("/taskHandle")
    public String taskHandle() {
        // 根据用户id 获取自己的连接
        Long userId1 = LoginHelper.getUserId();
        SseEmitter sseEmitter  = SseEmitterServer.get(userId1+"");
        // 任务处理代码  处理了 10%
        .....
        sseEmitter.send("10");
        // 任务处理代码  处理了 20%
        .....
        sseEmitter.send("20");
        // 任务处理代码  处理完成
        .....
        sseEmitter.send("100");
        return "successed";
    }
}

在实际业务中可以将 该对象保存在一个Map中、在业务中使用 用户的userid为key、从map中获取该sse对象再进行发送数据

public class SseEmitterServer {

    /**
     * 使用map对象,便于根据userId来获取对应的SseEmitter
     */
    private static Map<String, SseEmitter> sseEmitterMap = new ConcurrentHashMap<>();


    public static SseEmitter connect(String userId) {
        // 设置超时时间,0表示不过期。默认30秒,超过时间未完成会抛出异常:AsyncRequestTimeoutException
        SseEmitter sseEmitter = new SseEmitter(0L);
        // 注册回调方法
        sseEmitter.onCompletion(completionCallBack(userId));
        sseEmitter.onError(errorCallBack(userId));
        sseEmitter.onTimeout(timeoutCallBack(userId));
        // 保存在map中
        sseEmitterMap.put(userId, sseEmitter);

        return sseEmitter;
    }
    // 结束链接
    private static Runnable completionCallBack(String employeeCode) {
        return () -> {
            removeUser(employeeCode);
        };
    }
	
    // 超时
    private static Runnable timeoutCallBack(String employeeCode) {
        return () -> {
            removeUser(employeeCode);
        };
    }

    // 发生异常
    private static Consumer<Throwable> errorCallBack(String employeeCode) {
        return throwable -> {
            removeUser(employeeCode);
        };
    }
}

2、实现前端页面使用 vue2

在 Vue 项目中,创建一个组件,用于显示任务进度: 使用的是element-ui中的进度条组件

<!-- Progress.vue -->
<template>
    <div>
        <div>任务进度:{{ progress }}</div>
        <el-progress :percentage="progress"></el-progress>
    </div>
</template>
 
<script>
export default {
    data() {
        return {
            progress: 0,
            sse: null
        };
    },
    mounted() {
        this.initSse();
    },
    methods: {
        initSse() {
    		// 进度条接口、根据后端接口来填写
            this.sse = new EventSource('/progress');
            this.sse.onmessage = event => {
                this.progress = parseInt(event.data);
            };
            this.sse.onerror = error => {
                console.error('SSE 连接异常', error);
                this.sse.close();
            };
        }
    },
    beforeDestroy() {
        if (this.sse) {
            this.sse.close();
        }
    }
};
</script>