需求:用uniapp开发的微信小程序实现大文件上传

公司目前有个需求,就是老师上课的录像需要通过手机端小程序上传到服务器,而手机拍摄的视频一般会很大,虽然微信会自动压缩视频,但是难免的,视频依然会很大~~
微信自带的文件上传工具,虽然能上传大文件,但是。。。难免可能会出现网络波动等问题,导致文件上传失败,而且服务端也做了限制,单个文件不能超过20M,那么~问题来了,录播课程一节课一般都在200-300m左右,如何上传呢??

方案:使用文件切割工具分片上传文件

此时就需要用到大文件切片上传工具啦。我实现的思路很简单:

  1. 文件上传之前的握手:先读取文件信息,例如文件名称、文件大小、文件MD5(用于检验上传完成后的文件完整性,以及作为当前上传的任务key)、文件分片大小、文件总片数等~;
  2. 文件切割:按指定大小将文件切割成独立的文件片,例如2m每片。
  3. 文件合并:将无数个文件片合并成一个完整的文件,然后根据握手时的MD5值校验文件的完整性。
  4. 文件保存:将上传后的文件信息保存到数据库,然后返回文件的保存信息,比如文件路径、文件大小、文件MD5等~
  5. 上传成功:将上传信息返回给前端。

实现:既然需求和方案已经明确了,那么,动手淦吧~

  1. 第一步,先实现视频文件的选中,然后读取文件的信息:
    选择视频文件我是使用的chooseVideo,关于chooseVideo具体的用法,可以参考uniapp的官方文档:点这里传送:chooseVideo
    chooseVideo() {
      uni.chooseVideo({
        success: res => {
          const uploadFile = new BigUpload({
            url: `这是一个文件上传的路径`,
            filePath: res.tempFilePath,
            type: 'video/mp4',
            byteLength: res.size,
            size: 2097152,
            fileName: 'weixin_video.mp4',
            drowSpeed: (p) => {this.percent = p},
            callback: (state) => {
              if (state) {
                this.percent = 100
                this.uploadStatus = '上传完成'
                this.videoMd5 = state.md5
              }
            }
          })
          uploadFile.startUpload()
        }
      })
    }

2.文件选择成功后,读取文件基础信息,组装握手信息:
在chooseVideo选中文件后,tempFilePath就是文件的临时路径,res.size就是文件的大小总长度,剩余的参数就需要我们自行配置,例如type、size(分片大小)、fileName(文件名称,由于这个chooseVideo不能读取文件名,所以这里就自定义一个)等,配置如下:

    url: `这是一个文件上传的路径`,
    filePath: res.tempFilePath,
    type: 'video/mp4',
    byteLength: res.size,
    size: 2097152,
    fileName: 'weixin_video.mp4'

然后获取组装信息:

    startUpload() {
        this.chunkSize = this.Setting.size
        if (!this.Setting.filePath) {
            return
        }
        this.pt_md5 = ''
        this.chunks = Math.ceil(this.Setting.byteLength / this.chunkSize)
        this.currentChunk = 0
    }

上传握手信息:

 handshake(cbk, e) {
        let formData = {}
        let md5 = this.getDataMd5(e)
        this.pt_md5 = md5
        formData.pt_md5 = this.pt_md5
        formData.chunks = this.chunks
        formData.size = this.Setting.byteLength
        formData.type = 'handshake'
        formData.md5 = md5
        formData.fileName = this.Setting.fileName
        formData.contentType = this.Setting.type
        postConsole({
            url: this.Setting.url,
            data: formData
        }).then(res => {
            if (res === 'success') {
                cbk(true)
            } else if (typeof res !== 'number') {
                this.Setting.callback(res)
            } else {
                this.currentChunk = res
                if (this.currentChunk < this.chunks) {
                    this.loadNext()
                } else {
                    this.currentChunk--
                    this.loadNext()
                }
            }
        }).catch(err => {
            console.error(err)
            cbk(false)
        })
    }

3.文件切割上传(最核心的来了):
a.先计算当前上传块的起始位置,以及计算上传进度:

    loadNext() {
        const p = this.currentChunk * 100 / this.chunks
        this.drowSpeed(parseInt(p));
        let start = this.currentChunk * this.chunkSize
        let length = start + this.chunkSize >= this.Setting.byteLength ? this.Setting.byteLength - start : this.chunkSize
        if (this.gowith) {
            this.fileSlice(start, length, file => {
                this.uploadFileBinary(file)
            })
        }
    }

b.切片:

    fileSlice(start, length, cbk) {
        uni.getFileSystemManager().readFile({
            filePath: this.Setting.filePath,
            encoding: 'binary',
            position: start,
            length: length,
            success: res => {
                cbk(res.data)
            },
            fail: err => {
                console.error(err)
                this.callback(false)
            }
        })
    }

c.上传,上传的逻辑是先根据切出来的文件块创建一个临时文件,然后上传这个临时文件,上传成功后就删除这个临时文件${wx.env.USER_DATA_PATH} 这里是用户数据目录,在uniapp中也必须这么写,不然无法识别路径:

	uploadFileBinary(data) {
		//获取文件系统句柄
        const fs = uni.getFileSystemManager()
        //计算数据md5
        const md5 = this.getDataMd5(data)
        //创建临时文件
        const tempPath = `${wx.env.USER_DATA_PATH}/up_temp/${md5}.temp`
        //授权创建
        fs.access({
            path: `${wx.env.USER_DATA_PATH}/up_temp`,
            fail(res) {
                fs.mkdirSync(`${wx.env.USER_DATA_PATH}/up_temp`, false)
            }
        })
        //写入文件系统
        fs.writeFile({
            filePath: tempPath,
            encoding: 'binary',
            data: data,
            success: res => {
                let formData = {}
                formData.currentChunk = this.currentChunk + 1
                formData.pt_md5 = this.pt_md5
                formData.type = 'file'
                formData.md5 = md5
                //上传文件片
                uni.uploadFile({
                    url: this.Setting.url,
                    filePath: tempPath,
                    name: 'file',
                    formData: formData,
                    success: res2 => {
                        fs.unlinkSync(tempPath)
                        if (res2.statusCode === 200) {
                            const data = JSON.parse(res2.data)
                            if (data.code === '0') {
                                this.currentChunk++
                                //判断是否所有篇都上传了
                                if (this.currentChunk < this.chunks) {
                                	//继续上传下一片
                                    this.loadNext()
                                } else {
                                    this.callback(data.data)
                                }
                                return true
                            } 
                        }
                        //上传错误
                        this.callback(false)
                    },
                    fail: err => {
                        console.log(err)
                        this.callback(false)
                    }
                })
            },
            fail: err => {
                console.log(err)
                this.callback(false)
            }
        })
    }

4.文件合并:文件合并的操作主要在后端实现,实现逻辑也很简单,就是按照顺序将所有的文件块拼接起来就可以了。
5.上传成功:回显文件上传信息,比如路径、MD5等信息;

          const uploadFile = new BigUpload({
            url: `一个路径`,
            filePath: res.tempFilePath,
            type: 'video/mp4',
            byteLength: res.size,
            size: 2097152,
            fileName: 'weixin_video.mp4',
            drowSpeed: (p) => {this.percent = p},
            callback: (state) => {
              if (state) {
                this.percent = 100
                this.uploadStatus = '上传完成'
                this.videoMd5 = state.md5
              }
            }
          })

当callback失败时,返回false,当上传成功时,返回文件的信息。drowSpeed为绘制上传进度百分比。

总结:说说大文件上传的难点

大文件切片上传,最复杂的莫过于切片和上传这一块,之前研究uniapp文档时,上面写得很不详细,然后跑去微信官方文档上去查,微信文档上描述的比较清楚,我把地址贴出来戳这里FileSystemManager,有兴趣的可以看看.

断点续传:简要的说明一下

后端以md5值为key,将进度存入redis,所以就算上传到一半有一个片失败了,那么下次重新上传时,会根据MD5值查询上次的上传进度,然后续传。当然也支持其他客户端上传,比如在上机上上传了10%,那么剩下的90%可以在电脑上继续上传,暂时不支持多客户端并行上传同一个文件。

完整代码:↓

upload.js

import SparkMD5 from 'spark-md5'

export const postConsole = (options) => {
    let header = {...options.header}
    return new Promise((resolve, reject) => {
        uni.request({
            url: options.url + '/console',
            method: options.method || 'POST',
            data: options.data || {},
            dataType: 'json',
            header,
            success: (res) => {
                if (res.data) {
                    if (res.data.code === '0') {
                        resolve(res.data.data)
                    } else {
                        reject(res.data.msg)
                    }
                }
            },
            fail: (err) => {
                reject(err)
            }
        })
    })
}
export default class BigUpload {
    constructor(Setting) {
        this.Setting = Setting
    }

    startUpload() {
        this.chunkSize = this.Setting.size
        if (!this.Setting.filePath) {
            return
        }
        this.pt_md5 = ''
        this.chunks = Math.ceil(this.Setting.byteLength / this.chunkSize)
        this.currentChunk = 0
        this.gowith = true
        this.fileSlice(0, this.Setting.byteLength, file => {
            this.handshake(flag => {
                if (flag) {
                    this.loadNext()
                } else {
                    this.Setting.callback(false)
                }
            }, file)
        })
    }

    handshake(cbk, e) {
        let formData = {}
        let md5 = this.getDataMd5(e)
        this.pt_md5 = md5
        formData.pt_md5 = this.pt_md5
        formData.chunks = this.chunks
        formData.size = this.Setting.byteLength
        formData.type = 'handshake'
        formData.md5 = md5
        formData.fileName = this.Setting.fileName
        formData.contentType = this.Setting.type
        postConsole({
            url: this.Setting.url,
            data: formData
        }).then(res => {
            if (res === 'success') {
                cbk(true)
            } else if (typeof res !== 'number') {
                this.Setting.callback(res)
            } else {
                this.currentChunk = res
                if (this.currentChunk < this.chunks) {
                    this.loadNext()
                } else {
                    this.currentChunk--
                    this.loadNext()
                }
            }
        }).catch(err => {
            console.error(err)
            cbk(false)
        })
    }

    loadNext() {
        const p = this.currentChunk * 100 / this.chunks
        this.drowSpeed(parseInt(p));
        let start = this.currentChunk * this.chunkSize
        let length = start + this.chunkSize >= this.Setting.byteLength ? this.Setting.byteLength - start : this.chunkSize
        if (this.gowith) {
            this.fileSlice(start, length, file => {
                this.uploadFileBinary(file)
            })
        }
    }

    uploadFileBinary(data) {
        const fs = uni.getFileSystemManager()
        const md5 = this.getDataMd5(data)
        const tempPath = `${wx.env.USER_DATA_PATH}/up_temp/${md5}.temp`
        fs.access({
            path: `${wx.env.USER_DATA_PATH}/up_temp`,
            fail(res) {
                fs.mkdirSync(`${wx.env.USER_DATA_PATH}/up_temp`, false)
            }
        })
        fs.writeFile({
            filePath: tempPath,
            encoding: 'binary',
            data: data,
            success: res => {
                let formData = {}
                formData.currentChunk = this.currentChunk + 1
                formData.pt_md5 = this.pt_md5
                formData.type = 'file'
                formData.md5 = md5
                uni.uploadFile({
                    url: this.Setting.url,
                    filePath: tempPath,
                    name: 'file',
                    formData: formData,
                    success: res2 => {
                        fs.unlinkSync(tempPath)
                        if (res2.statusCode === 200) {
                            const data = JSON.parse(res2.data)
                            if (data.code === '0') {
                                this.currentChunk++
                                if (this.currentChunk < this.chunks) {
                                    this.loadNext()
                                } else {
                                    this.callback(data.data)
                                }
                            } else {
                                this.callback(false)
                            }
                        } else {
                            this.callback(false)
                        }
                    },
                    fail: err => {
                        console.log(err)
                        this.callback(false)
                    }
                })
            },
            fail: err => {
                console.log(err)
                this.callback(false)
            }
        })
    }

    drowSpeed(p) {
        if (this.Setting.drowSpeed != null && typeof (this.Setting.drowSpeed) === 'function') {
            this.Setting.drowSpeed(p)
        }
    }

    getDataMd5(data) {
        if (data) {
            let trunkSpark = new SparkMD5()
            trunkSpark.appendBinary(data)
            let md5 = trunkSpark.end()
            return md5
        }
    }

    isPlay(cbk) {
        if (this.gowith) {
            this.gowith = false
            if (typeof (cbk) === 'function') cbk(false)
        } else {
            this.gowith = true
            this.loadNext()
            if (typeof (cbk) === 'function') cbk(true)
        }
    }

    fileSlice(start, length, cbk) {
        uni.getFileSystemManager().readFile({
            filePath: this.Setting.filePath,
            encoding: 'binary',
            position: start,
            length: length,
            success: res => {
                cbk(res.data)
            },
            fail: err => {
                console.error(err)
                this.callback(false)
            }
        })
    }

    callback(res) {
        if (typeof (this.Setting.callback) === 'function') {
            this.Setting.callback(res)
        }
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐