
组件功能
这个视频上传组件的功能包括:
- 选择视频文件:通过点击"选择文件"按钮选择要上传的视频文件。
- 显示上传进度:在上传过程中,展示上传进度条以及视频文件的名称、大小和时长信息。
- 使用 simple-uploader.js 实现上传:利用 simple-uploader.js 这个库进行视频文件的上传。
- 计算视频时长:通过使用浏览器的原生 API 获取上传视频文件的时长。
- 计算文件 MD5 哈希值:使用 SparkMD5 库计算视频文件的 MD5 哈希值,用于文件的唯一标识和校验。
这个组件通过以下步骤实现:
- 选择文件:点击"选择文件"按钮,调用
choiceFileHandle
方法,通过uni.chooseVideo
方法选择视频文件并添加到上传队列中。 - 上传进度展示:利用
u-slider
组件展示上传进度。 - 初始化上传器:在
mounted
钩子函数中初始化simple-uploader.js
的实例,并设置相关参数。 - 文件上传事件监听:监听
simple-uploader.js
的文件上传事件,包括文件添加、上传进度和上传成功。 - 计算文件时长和 MD5:通过原生 API 和 SparkMD5 库计算文件时长和 MD5 哈希值。
1.使用依赖
shell
npm install simple-uploader.js
shell
npm install --save spark-md5
2.uploader初始化
- 创建一个
Uploader
实例 - 实例化后添加监听事件
js
this.uploader = new Uploader({
// 单文件上传
singleFile: true,
headers: {
Authorization: "Bearer " + getToken(),
},
//目标上传 URL,默认POST
target: config.baseUrl + "/system/uploader/chunk",
//分块大小(单位:字节)
chunkSize: 1024 * 1024 * 2,
//上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
fileParameterName: "upfile",
//失败后最多自动重试上传次数
maxChunkRetries: 3,
//是否开启服务器分片校验,对应GET类型同名的target URL
testChunks: true,
/*
服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
reponse码是successStatuses码时,才会进入该方法
reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
reponse码是其他状态码,不会进入该方法,正常走标准上传
checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
*/
checkChunkUploadedByResponse: function (chunk, response_msg) {
let objMessage = JSON.parse(response_msg);
if (objMessage.skipUpload) {
return true;
}
return (
(objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0
);
},
})
// 文件添加 单个文件
this.uploader.on('fileAdded', (file, event)=>{
this.computeMD5(file);
})
// 文件在上传中
this.uploader.on('fileProgress', (rootFile, file, chunk) => {
const res = JSON.parse(chunk.processedState?.res ?? '{}');
// console.log(res,chunk);
if (res==200 || !chunk.preprocessState) {
this.uploadProgress = (this.uploader.progress()*100).toFixed(2);
} else {
this.cancel();
}
})
// 文件上传成功
this.uploader.on('fileSuccess', (rootFile, file, message) => {
//refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
file.refProjectId = "123456789";
// 文件合并接口
mergeFile(file).then(res=>{
if (res.code == 200) {
this.$emit("updateValue", res.data.location);
}
})
})
3.使用spark-md5文件切片
js
computeMD5(file) {
file.pause();
console.log("文件大小:" + file.size);
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
//由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
let chunkNumberMD5 = 1;
loadNext();
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunkNumberMD5) {
loadNext();
} else {
let md5 = spark.end();
file.uniqueIdentifier = md5;
file.resume();
console.log(
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
);
}
};
fileReader.onerror = function () {
console.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
currentChunk++;
console.log("计算第" + currentChunk + "块");
}
}
4.文件选择
我这里用的是uni.chooseVideo(OBJECT)
拍摄视频或从手机相册中选视频 this.uploader.addFile(file)
中需传入File类型
js
/**
* 选择文件
*/
choiceFileHandle(){
this.uploader && this.uploader.cancel();
uni.chooseVideo({
success: async (res)=>{
let file = res.tempFile;
this.uploader.addFile(file);
this.showProgress = true;
this.fileData.name = file.name;
this.fileData.size = (file.size/1024/1024).toFixed(2) + 'MB';
this.fileData.time = await this.videoLong(file);
this.$emit("updateTime", this.fileData.time);
}
})
}
完整代码
js
<template>
<view class="upload">
<view class="upload-box" v-if="showProgress">
<view class="upload-box-top">
<image
src="@/static/images/app/mp4.png"
mode="scaleToFill"
/>
<view class="upload-box-top-center">
<p class="name-text">{{ fileData.name }}</p>
<p class="size-text">{{ fileData.size }}</p>
</view>
</view>
<u-slider
v-model="uploadProgress"
activeColor="#3c9cff"
inactiveColor="#c0c4cc"
disabled
min="0"
max="100"
></u-slider>
</view>
<u-button
text="选择文件"
color="#0366F1"
size="small"
style="width: 100px;float: left;"
@click="choiceFileHandle"
></u-button>
</view>
</template>
<script>
import Uploader from 'simple-uploader.js';
import SparkMD5 from "spark-md5";
import { getToken } from '@/utils/auth';
import config from '@/config'
import { mergeFile } from "@/api/promoteLearning.js"
export default {
name: "uploadVideo",
props: {
value: {
type: String,
default: ''
},
time: {
type: Number,
default: ''
}
},
data() {
return {
fileData: {
name: "",
size: "",
time: ""
},
showProgress: false,
uploadProgress: 0,
uploader: null,
};
},
mounted() {
this.initUploader();
},
methods: {
/**
* 选择文件
*/
choiceFileHandle(){
this.cancel();
uni.chooseVideo({
success: async (res)=>{
let file = res.tempFile;
this.uploader.addFile(file);
this.showProgress = true;
this.fileData.name = file.name;
this.fileData.size = (file.size/1024/1024).toFixed(2) + 'MB';
this.fileData.time = await this.videoLong(file);
this.$emit("updateTime", this.fileData.time);
}
})
},
cancel() {
this.fileData = {
name: "",
size: ""
};
this.showProgress = false;
this.uploadProgress = 0;
this.uploader && this.uploader.cancel();
},
videoLong(file){
return new Promise((resolve)=>{
var url = URL.createObjectURL(file);
var audioElement = new Audio(url);
audioElement.addEventListener("loadedmetadata", function() {
resolve(audioElement.duration)
});
})
},
initUploader() {
this.uploader = new Uploader({
// 单文件上传
singleFile: true,
headers: {
Authorization: "Bearer " + getToken(),
},
//目标上传 URL,默认POST
target: config.baseUrl + "/system/uploader/chunk",
//分块大小(单位:字节)
chunkSize: 1024 * 1024 * 2,
//上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
fileParameterName: "upfile",
//失败后最多自动重试上传次数
maxChunkRetries: 3,
//是否开启服务器分片校验,对应GET类型同名的target URL
testChunks: true,
/*
服务器分片校验函数,判断秒传及断点续传,传入的参数是Uploader.Chunk实例以及请求响应信息
reponse码是successStatuses码时,才会进入该方法
reponse码如果返回的是permanentErrors 中的状态码,不会进入该方法,直接进入onFileError函数 ,并显示上传失败
reponse码是其他状态码,不会进入该方法,正常走标准上传
checkChunkUploadedByResponse函数直接return true的话,不再调用上传接口
*/
checkChunkUploadedByResponse: function (chunk, response_msg) {
let objMessage = JSON.parse(response_msg);
if (objMessage.skipUpload) {
return true;
}
return (
(objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0
);
},
})
// 文件添加 单个文件
this.uploader.on('fileAdded', (file, event)=>{
this.computeMD5(file);
})
this.uploader.on('fileProgress', (rootFile, file, chunk) => {
const res = JSON.parse(chunk.processedState?.res ?? '{}');
// console.log(res,chunk);
if (res==200 || !chunk.preprocessState) {
this.uploadProgress = (this.uploader.progress()*100).toFixed(2);
} else {
this.cancel();
}
})
this.uploader.on('fileSuccess', (rootFile, file, message) => {
console.log('fileSuccess',rootFile, file, message);
file.refProjectId = "123456789";
mergeFile(file).then(res=>{
if (res.code == 200) {
this.$emit("updateValue", res.data.location);
}
})
})
},
computeMD5(file) {
file.pause();
console.log("文件大小:" + file.size);
let fileReader = new FileReader();
let time = new Date().getTime();
let blobSlice =
File.prototype.slice ||
File.prototype.mozSlice ||
File.prototype.webkitSlice;
let currentChunk = 0;
const chunkSize = 10 * 1024 * 1000;
let chunks = Math.ceil(file.size / chunkSize);
let spark = new SparkMD5.ArrayBuffer();
//由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
let chunkNumberMD5 = 1;
loadNext();
fileReader.onload = (e) => {
spark.append(e.target.result);
if (currentChunk < chunkNumberMD5) {
loadNext();
} else {
let md5 = spark.end();
file.uniqueIdentifier = md5;
file.resume();
console.log(
`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
file.size
} 用时:${new Date().getTime() - time} ms`
);
}
};
fileReader.onerror = function () {
console.error(`文件${file.name}读取出错,请检查该文件`);
file.cancel();
};
function loadNext() {
let start = currentChunk * chunkSize;
let end =
start + chunkSize >= file.size ? file.size : start + chunkSize;
fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
currentChunk++;
console.log("计算第" + currentChunk + "块");
}
}
},
};
</script>
<style lang="scss" scoped>
.upload {
width: 100%;
}
.upload-box {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #e4e7ed;
box-shadow: 0px 0px 12px rgba(0, 0, 0, .12);
margin-bottom: 10px;
&-top {
display: flex;
image {
width: 40px;
height: 40px;
margin-right: 10px;
}
&-center {
display: flex;
flex-direction: column;
justify-content: space-around;
.name-text {
font-size: 14px;
color: #4F4F4F;
font-weight: 700;
}
.size-text {
font-size: 13px;
color: #828282;
}
}
}
}
::v-deep {
.uni-slider-thumb {
display: none;
}
uni-slider {
margin: 10px 0 0;
}
}
</style>