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

https://blog.csdn.net/qq_34157798/article/details/119324994

需求:用uniapp开发的微信小程序实现大文件上传
方案:使用文件切割工具分片上传文件
实现:既然需求和方案已经明确了,那么,动手淦吧~
总结:说说大文件上传的难点
断点续传:简要的说明一下
完整代码:↓

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

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

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

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

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

第一步,先实现视频文件的选中,然后读取文件的信息:
选择视频文件我是使用的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()
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)
}
})
}

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)
}
}
}


发表评论

电子邮件地址不会被公开。 必填项已用*标注