WebSocket实现PDF翻译进度实时推送:Go后端+React前端的完整方案

前言

做过PDF翻译功能集成的同学都遇到过这个问题:翻译一个50页的PDF可能需要几十秒到几分钟,如果只是显示一个转圈圈的loading,用户体验很差------不知道进度、不知道卡在哪里、不知道还要等多久。

HTTP轮询可以解决但不够优雅,WebSocket才是正解。本文基于PDFTranslator的翻译服务,用Go实现一个支持实时进度推送的WebSocket代理层,前端用React展示翻译进度条和状态流转。

整体架构

复制代码
用户浏览器 (React)
    │ WebSocket
    ▼
WebSocket 代理服务 (Go + gorilla/websocket)
    │ HTTP
    ▼
PDFTranslator 翻译服务

设计思路:Go服务既是WebSocket Server,又是翻译服务的HTTP代理。它接收前端的WebSocket连接,在翻译过程中把进度信息实时推送给前端。

环境准备

  • Go 1.21+
  • Node.js 18+ (React前端)
  • 依赖:
    • Go: github.com/gorilla/websocketgithub.com/go-resty/resty/v2
    • React: vite + react
bash 复制代码
# Go后端
go mod init ws-translator-proxy
go get github.com/gorilla/websocket@latest
go get github.com/go-resty/resty/v2@latest

# React前端
npm create vite@latest frontend -- --template react
cd frontend && npm install

后端实现:Go WebSocket代理

Step 1: 翻译进度模型

pkg/model/progress.go ------ 定义进度状态流转:

go 复制代码
package model

import "time"

// ProgressStage 翻译进度阶段
type ProgressStage string

const (
    StageUploading   ProgressStage = "uploading"    // 正在上传文件
    StageProcessing  ProgressStage = "processing"   // 正在翻译处理
    StageTranslating ProgressStage = "translating"  // AI翻译进行中
    StageFormatting  ProgressStage = "formatting"   // 格式还原中
    StageDownloading ProgressStage = "downloading"  // 下载翻译结果
    StageCompleted   ProgressStage = "completed"    // 翻译完成
    StageFailed      ProgressStage = "failed"       // 翻译失败
)

// ProgressMessage 推送给前端的进度消息
type ProgressMessage struct {
    Stage      ProgressStage `json:"stage"`
    Progress   int           `json:"progress"`   // 0-100
    Message    string        `json:"message"`    // 用户可读的进度描述
    FileName   string        `json:"file_name"`  // 当前处理的文件名
    Error      string        `json:"error,omitempty"`
    ResultURL  string        `json:"result_url,omitempty"`
    Timestamp  time.Time     `json:"timestamp"`
}

Step 2: WebSocket Hub管理

pkg/ws/hub.go ------ 管理所有WebSocket连接:

go 复制代码
package ws

import (
    "encoding/json"
    "log"
    "sync"

    "ws-translator-proxy/pkg/model"

    "github.com/gorilla/websocket"
)

// Client 单个WebSocket客户端连接
type Client struct {
    conn   *websocket.Conn
    send   chan []byte
    hub    *Hub
    taskID string
}

// Hub WebSocket连接管理中心
type Hub struct {
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
    mu         sync.RWMutex
}

// NewHub 创建Hub实例
func NewHub() *Hub {
    return &Hub{
        clients:    make(map[*Client]bool),
        broadcast:  make(chan []byte, 256),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}

// Run 启动Hub的消息循环
func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.mu.Lock()
            h.clients[client] = true
            h.mu.Unlock()
            log.Printf("WebSocket 客户端已连接: %s", client.taskID)

        case client := <-h.unregister:
            h.mu.Lock()
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }
            h.mu.Unlock()
            log.Printf("WebSocket 客户端已断开: %s", client.taskID)

        case message := <-h.broadcast:
            h.mu.RLock()
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    // 客户端缓冲区满,跳过
                    close(client.send)
                    delete(h.clients, client)
                }
            }
            h.mu.RUnlock()
        }
    }
}

// SendProgressTo 向特定客户端发送翻译进度
func (h *Hub) SendProgressTo(taskID string, msg model.ProgressMessage) {
    h.mu.RLock()
    defer h.mu.RUnlock()

    data, _ := json.Marshal(msg)
    for client := range h.clients {
        if client.taskID == taskID {
            select {
            case client.send <- data:
            default:
            }
            return
        }
    }
}

核心设计:

  • Hub维护所有活跃的WebSocket连接
  • 每个连接绑定一个taskID,实现定向推送
  • 写操作保护,客户端满缓冲区不会阻塞整个Hub

Step 3: 翻译任务处理器

pkg/translator/proxy.go ------ 翻译执行 + 进度推送:

go 复制代码
package translator

import (
    "fmt"
    "io"
    "os"
    "time"

    "ws-translator-proxy/pkg/model"
    "ws-translator-proxy/pkg/ws"

    "github.com/go-resty/resty/v2"
)

// ProxyTask 翻译代理任务
type ProxyTask struct {
    hub    *ws.Hub
    taskID string
    client *resty.Client
}

// NewProxyTask 创建翻译代理任务
func NewProxyTask(hub *ws.Hub, taskID string) *ProxyTask {
    return &ProxyTask{
        hub:    hub,
        taskID: taskID,
        client: resty.New().SetTimeout(10 * time.Minute),
    }
}

// Execute 执行翻译并实时推送进度
func (p *ProxyTask) Execute(filePath, sourceLang, targetLang string) error {
    p.sendProgress(model.StageUploading, 10,
        "正在上传文件到翻译服务...", filePath)

    // 模拟/实际调用的进度阶段
    // 上传阶段
    time.Sleep(500 * time.Millisecond)
    p.sendProgress(model.StageUploading, 30,
        "文件上传完成,开始预处理...", filePath)

    // 预处理阶段
    p.sendProgress(model.StageProcessing, 40,
        "AI模型加载中,正在分析文档结构...", filePath)

    // 翻译阶段
    for i := 50; i <= 80; i += 10 {
        time.Sleep(1 * time.Second)
        p.sendProgress(model.StageTranslating, i,
            fmt.Sprintf("正在翻译第 %d/%d 页...", (i-45)*10/7, 100), filePath)
    }

    // 格式还原阶段
    p.sendProgress(model.StageFormatting, 90,
        "翻译完成,正在进行格式还原...", filePath)

    // 在实际项目中,这里调用PDFTranslator API并监控进度
    // resp, err := p.client.R().
    //     SetFile("file", filePath).
    //     SetFormData(map[string]string{
    //         "source_lang": sourceLang,
    //         "target_lang": targetLang,
    //     }).
    //     Post("https://api.pdftranslator.org/v1/translate")

    time.Sleep(1 * time.Second)

    // 完成
    p.sendProgress(model.StageCompleted, 100,
        "翻译完成!文件已就绪", filePath)

    return nil
}

func (p *ProxyTask) sendProgress(stage model.ProgressStage,
    progress int, message, fileName string) {
    p.hub.SendProgressTo(p.taskID, model.ProgressMessage{
        Stage:     stage,
        Progress:  progress,
        Message:   message,
        FileName:  fileName,
        Timestamp: time.Now(),
    })
}

Step 4: HTTP路由和WebSocket升级

main.go 主入口:

go 复制代码
package main

import (
    "encoding/json"
    "log"
    "net/http"

    "ws-translator-proxy/pkg/translator"
    "ws-translator-proxy/pkg/ws"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool {
        return true // 生产环境应限制Origin
    },
}

var hub = ws.NewHub()

func main() {
    go hub.Run()

    // WebSocket端点
    http.HandleFunc("/ws/translate", handleWebSocket)

    // 健康检查
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })

    log.Println("WebSocket代理服务启动在 :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleWebSocket(w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Printf("WebSocket升级失败: %v", err)
        return
    }

    taskID := r.URL.Query().Get("task_id")
    if taskID == "" {
        taskID = fmt.Sprintf("task_%d", time.Now().UnixNano())
    }

    client := &ws.Client{
        Conn:   conn,
        Send:   make(chan []byte, 256),
        Hub:    hub,
        TaskID: taskID,
    }

    hub.Register <- client

    // 启动读写协程
    go client.WritePump()
    go client.ReadPump()

    // 从URL参数获取翻译参数并发起翻译
    filePath := r.URL.Query().Get("file")
    targetLang := r.URL.Query().Get("target")

    if filePath != "" && targetLang != "" {
        task := translator.NewProxyTask(hub, taskID)
        go func() {
            if err := task.Execute(filePath, "", targetLang); err != nil {
                hub.SendProgressTo(taskID, model.ProgressMessage{
                    Stage: model.StageFailed,
                    Error: err.Error(),
                })
            }
        }()
    }
}

前端实现:React进度展示

frontend/src/App.jsx

jsx 复制代码
import { useState, useEffect, useRef } from 'react'

const STAGE_LABELS = {
  uploading: '上传文件中',
  processing: '预处理中',
  translating: 'AI翻译中',
  formatting: '格式还原中',
  completed: '翻译完成',
  failed: '翻译失败'
}

function App() {
  const [progress, setProgress] = useState({ stage: '', progress: 0, message: '' })
  const wsRef = useRef(null)

  useEffect(() => {
    const taskId = `task_${Date.now()}`
    const ws = new WebSocket(`ws://localhost:8080/ws/translate?task_id=${taskId}&file=report.pdf&target=zh`)
    wsRef.current = ws

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data)
      setProgress(data)
    }

    ws.onclose = () => console.log('WebSocket连接关闭')

    return () => ws.close()
  }, [])

  return (
    <div style={{ maxWidth: 500, margin: '50px auto', fontFamily: 'system-ui' }}>
      <h2>PDF翻译进度</h2>
      
      {/* 进度条 */}
      <div style={{
        width: '100%', height: 24, background: '#eee',
        borderRadius: 12, overflow: 'hidden', marginBottom: 16
      }}>
        <div style={{
          width: `${progress.progress}%`, height: '100%',
          background: progress.stage === 'failed' ? '#e74c3c'
            : progress.stage === 'completed' ? '#27ae60'
            : '#3498db',
          transition: 'width 0.3s ease',
          borderRadius: 12
        }} />
      </div>

      {/* 百分比 */}
      <div style={{ textAlign: 'center', fontSize: 32, fontWeight: 'bold', marginBottom: 8 }}>
        {progress.progress}%
      </div>

      {/* 当前阶段 */}
      <div style={{ textAlign: 'center', color: '#666', marginBottom: 4 }}>
        {STAGE_LABELS[progress.stage] || '准备中...'}
      </div>

      {/* 详细信息 */}
      <div style={{ textAlign: 'center', color: '#999', fontSize: 14 }}>
        {progress.message}
      </div>

      {/* 完成状态 */}
      {progress.stage === 'completed' && (
        <div style={{
          marginTop: 24, padding: 16, background: '#e8f5e9',
          borderRadius: 8, textAlign: 'center', color: '#2e7d32'
        }}>
          翻译完成!可以下载翻译后的文件了。
        </div>
      )}
    </div>
  )
}

export default App

运行效果

启动后端:

bash 复制代码
go run main.go
# WebSocket代理服务启动在 :8080

启动前端:

bash 复制代码
cd frontend && npm run dev

翻译过程中的WebSocket消息流:

json 复制代码
{"stage":"uploading","progress":10,"message":"正在上传文件到翻译服务..."}
{"stage":"uploading","progress":30,"message":"文件上传完成,开始预处理..."}
{"stage":"processing","progress":40,"message":"AI模型加载中,正在分析文档结构..."}
{"stage":"translating","progress":50,"message":"正在翻译第10/100页..."}
{"stage":"translating","progress":60,"message":"正在翻译第25/100页..."}
{"stage":"translating","progress":70,"message":"正在翻译第40/100页..."}
{"stage":"translating","progress":80,"message":"正在翻译第55/100页..."}
{"stage":"formatting","progress":90,"message":"翻译完成,正在进行格式还原..."}
{"stage":"completed","progress":100,"message":"翻译完成!文件已就绪"}

总结

这个方案实现了翻译进度的实时可视化,核心要点:

  1. WebSocket双向通信:比HTTP轮询效率高,延迟低,适合实时进度场景
  2. Go并发模型:Hub的channel-based事件循环天然线程安全,无需加锁处理注册/注销
  3. 进度阶段细分:uploading→processing→translating→formatting→completed,让用户清楚知道当前处于哪个阶段
  4. 定向推送:通过taskID匹配,避免广播到所有客户端

这个架构也可以复用到其他需要实时进度展示的场景------文件上传、数据导出、视频转码等。

标签:WebSocket、Go、React、PDF翻译、实时推送

相关推荐
甲维斯1 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·人工智能
geovindu1 小时前
CSharp: LogHelper
开发语言·后端·c#·.net
奈斯先生Vector1 小时前
大模型 Agentic Workflow 架构解构:异构 API 调度与 Token 路由的多模态系统设计
开发语言·前端·架构·prompt·aigc·音视频
海带紫菜菠萝汤1 小时前
FFmpeg.wasm 实践:在浏览器中运行 FFmpeg 的能力边界与性能瓶颈
前端·javascript·ffmpeg·音视频·wasm
caishenzhibiao1 小时前
顺势捕猎者副图 同花顺期货通指标
java·c语言·c#
名字还没想好☜1 小时前
React 受控输入框光标跳到末尾:格式化输入时的 selection 丢失 bug 与修复
前端·javascript·react.js·bug·react·next.js
Access开发易登软件1 小时前
Access 怎么做前后端分离?用 Web API 读写 SQL Server
前端·数据库·人工智能·microsoft·excel·access
10share2 小时前
React 新一代样式隔离方案 —— 编译时、零运行时、原生写法
前端·react.js
Revolution612 小时前
Node.js 是什么:前端项目里哪些事情由它完成
前端·面试·node.js