草庐IT

uniapp 微信小程序 分片 断点续传 大文件上传

陌生的L君 2023-04-20 原文

uniapp 微信小程序实现大文件分片上传

需求:用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)
        }
    }
}

有关uniapp 微信小程序 分片 断点续传 大文件上传的更多相关文章

  1. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - 在 Rails 中将文件大小字符串转换为等效千字节 - 2

    我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby - 如何使用 Ruby aws/s3 Gem 生成安全 URL 以从 s3 下载文件 - 2

    我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A

  10. ruby - rspec 需要 .rspec 文件中的 spec_helper - 2

    我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只

随机推荐