如何在Go WASM异步文件上传时实时显示上传进度
问题原因与解决方法
核心问题分析
- 进度更新时机延迟:Go WASM中goroutine调度依赖JS事件循环,你在
report()里启动的goroutine会被延迟到POST请求完成后才执行——因为http.Client.Do()会阻塞当前goroutine,直到请求结束,期间没有机会调度其他goroutine。 - 进度更新次数不足:你把整个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
相关产品推荐
相关产品推荐

