You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Go WASM异步文件上传时实时显示上传进度

问题原因与解决方法

核心问题分析

  1. 进度更新时机延迟:Go WASM中goroutine调度依赖JS事件循环,你在report()里启动的goroutine会被延迟到POST请求完成后才执行——因为http.Client.Do()会阻塞当前goroutine,直到请求结束,期间没有机会调度其他goroutine。
  2. 进度更新次数不足:你把整个multipart数据写入bytes.Buffer后再读取,bytes.Buffer.Read()会一次性返回所有数据,导致Read()方法仅被调用一次,进度直接跳转到100%。

解决方案

1. 实时触发UI更新(解决延迟问题)

修改report()方法,使用JS的setTimeout将进度更新操作加入JS事件队列,绕过Go goroutine调度的延迟:

func (pr *progressReporter) report() {
    // 创建临时回调函数,执行emit后释放资源
    cb := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        progress := pr.sent * 100 / pr.fileSizeEncrypted
        js.Global().Get("dropzoneObject").Call("emit", "uploadprogress", pr.file, progress, pr.sent)
        cb.Release() // 释放JS函数,避免内存泄漏
        return nil
    })
    // 用setTimeout将回调放入JS事件循环,立即触发
    js.Global().Call("setTimeout", cb, 0)
}

2. 分块读取数据(解决更新次数不足问题)

不要提前将所有数据写入bytes.Buffer,改用io.Pipe实现边写边读,让Read()方法被多次调用,从而频繁更新进度:

func UploadChunk(this js.Value, args []js.Value) interface{} {
    // ... 原有代码 ...
    handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
        resolve := args[0]
        reject := args[1]

        go func() {
            // ... 原有变量定义(data、fileSize、offset、jsFile等)...
            
            // 创建管道,一边写multipart数据,一边读
            r, w := io.Pipe()
            go func() {
                defer w.Close()
                writer := multipart.NewWriter(w)
                part, err := writer.CreateFormFile("file", "encrypted.file")
                if err != nil {
                    reject.Invoke(err.Error())
                    return
                }
                
                // 分块写入数据,避免一次性写完
                src := bytes.NewReader(*data)
                buf := make([]byte, 4096) // 4KB分块,可根据需求调整
                for {
                    n, err := src.Read(buf)
                    if err != nil {
                        if err == io.EOF {
                            break
                        }
                        reject.Invoke(err.Error())
                        return
                    }
                    if _, err := part.Write(buf[:n]); err != nil {
                        reject.Invoke(err.Error())
                        return
                    }
                }
                
                if err := writer.Close(); err != nil {
                    reject.Invoke(err.Error())
                    return
                }
            }()
            
            // 包装管道读端为进度追踪器
            pReporter := progressReporter{
                r:                 r,
                fileSizeEncrypted: fileSize,
                sent:              offset,
                file:              jsFile,
            }
            
            rReq, err := http.NewRequest("POST", "./uploadChunk", &pReporter)
            if err != nil {
                reject.Invoke(err.Error())
                return
            }
            rReq.Header.Set("Content-Type", writer.FormDataContentType())
            
            client := &http.Client{}
            resp, err := client.Do(rReq)
            if err != nil {
                reject.Invoke(err.Error())
                return
            }
            defer resp.Body.Close()
            
            // ... 后续处理响应逻辑 ...
            resolve.Invoke(/* 你的返回值 */)
        }()
        return nil
    })
    // 返回Promise
    promise := js.Global().Get("Promise").New(handler)
    handler.Release()
    return promise
}

额外优化

  • 分块大小可根据实际场景调整(比如8KB、16KB),平衡进度更新频率与性能。
  • 确保在错误发生时正确关闭管道并调用reject,避免资源泄漏。

内容的提问来源于stack exchange,提问作者Force

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.23 13:54:22