最近做了一个需求,让用户本地上传一个最大 300M 的视频文件,下面是前端部分的记录。
需求分析点
- 校验用户上传的文件类型,以及内存大小,还有视频的播放时长检查
- 视频过大需要分片上传,记录本次是第几片数据,重新上传则可以实现接着上次的片段继续上传而非重头上传
- 同个视频需要标记,不可重复上传,减小服务器的内存压力
交互样式
接下来首先如何让用户上传文件,input 标签中type=file
可以让用户上传文件,此处我是在 Vue 项目里。
<input ref="videoInput" type="file" title="选择视频文件" @change="handleUpload" />
此时已经可以让用户在本地上传文件了,但是这里有个问题,在该用户已经有上传过的视频文件时,如果再次上传需要首先展示提示信息,重复上传会覆盖原有视频。但是用户点了该 input 按钮,就已经触发选择文件的弹窗了,并不能拦截,加之原生的 input 样式巨丑还自带选择的文件名,这不是我想要的,跟我们的 ui 样式不符,所以我的想法是,隐藏这个 input, 取而代之的是自己重新写一个按钮,给这个按钮加上点击事件,在该点击事件里处理额外逻辑,然后用 js 代码主动触发该 input 的点击事件。
this.$messageBox.confirm("重新上传视频会替换原有视频,是否继续?").then(() => {
this.$refs.videoInput.click();
});
标记视频
接下来就是如何标记视频,以免重复上传相同的视频。方案是获取视频文件的哈希 md5 值。因为每个文件的 md5 是唯一的,我们在做文件上传的时候,就只要在前端先获取要上传的文件 md5,并把文件 md5 传到服务器,对比之前文件的 md5,如果存在相同的 md5,我们只要把文件的名字传到服务器关联之前的文件即可,并不需要再次去上传相同的文件,再去耗费存储资源、上传的时间、网络带宽。
let file = this.$refs.videoInput.files[0];
console.log(file);
注意这里拿到的file数据,下面会再提到它
此处可以拿到文件 file,它自身可以获取文件的类型type
,以及文件的内存大小size
,单位是字节。同时也有slice
方法用于截取,分片获得子片段数据 file。
但是直接通过它去获取 md5 值发现跟后端(Java)获取不一致。接下来怎么办?
首先要说到的是spark-md5
依赖包
SparkMD5 是 MD5 算法的快速实现。此脚本基于 JKM md5 库,这最适合浏览器使用。
它的原理:(化简后的核心代码)
import SparkMD5 from 'spark-md5';
const fileReader = new FileReader();
fileReader.onload = function (e) {
let spark = new SparkMD5.ArrayBuffer();
spark.append(e.target.result); // Append array buffer
console.info("computed hash", spark.end()); // Compute hash
};
fileReader.readAsArrayBuffer(file);
关于FileReader
请移步🚀 FileReader
使用browser-md5-file
获取文件 md5 值, 它是进一步的封装
import BMF from "browser-md5-file";
const bmf = new BMF();
bmf.md5(
file,
(err, md5) => {
if (err) {
console.log(err);
}
console.log(md5); // md5值
},
(process) => {
//计算进度
}
);
此时对文件的md5值已经跟后端的保持一致了😍!
获取视频的播放时长
这里有两种场景获取视频时长
- 通过File数据
- 通过播放地址
function getVideoDuration(file) {
return new Promise((resolve, reject) => {
var audioElement;
var url;
if (typeof file == 'string') {
audioElement = new Audio();
audioElement.src = file
} else {
url = window.URL.createObjectURL(file);
audioElement = new Audio(url);
}
audioElement.addEventListener("loadedmetadata", function() {
var duration = audioElement.duration;
// console.log("视频时长", duration);
window.URL.revokeObjectURL(url);
resolve(duration)
});
})
}
到这里有必要说一说通过用户上传的文件直接获取到的数据File
到底是什么了?
File
在前端中用于文件操作的二进制对象类,它的父类是Blob
。它们是针对文件的,或者可以说它就是一个文件对象。
关于Blob请移步🚀 你不知道的 Blob
上传文件
上传文件的数据格式{"content-type": "multipart/form-data"}
,html5 提供了 api
FormData 接口提供了一种表示表单数据的键值对 key/value 的构造方式,并且可以轻松的将数据通过 XMLHttpRequest.send() 方法发送出去,本接口和此方法都相当简单直接。如果送出时的编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。
let formData = new FormData();
formData.append("file", item);
axios({
url: "xxx",
method: "post",
headers: { "content-type": "multipart/form-data" },
data: formData,
});
下面是项目中关于上传的代码逻辑,给大家一个参考吧!
/**
* id
* fileList 文件分片后的列表
* hashList 文件分片后对应的md5值列表
* startIndex 在上一次上传的起点 继续上传 默认第一段
* processFn 上传进度
*/
function async startMediaPartUpload(id, fileList, hashList, startIndex = 1, processFn) {
let point = Math.floor(((startIndex - 1) / fileList.length) * 100);
processFn && processFn(point)
// 上传
for (let i = startIndex - 1; i < fileList.length; i++) {
const item = fileList[i];
let formData = new FormData();
formData.append("id", id);
formData.append("option", "slice");
formData.append("hash", hashList[i]);
formData.append("part", i + 1);
formData.append("file", item);
let { code, memo, content } = await mediaPartUpload(formData);
let point = Math.floor(((i + 1) / fileList.length) * 100);
processFn && processFn(point)
if (code == "000000") {
// this.$message.success(`上传ok ${content.part}`);
} else {
this.$message.error(memo);
return Promise.reject("上传失败了");
}
}
}
本文使用 mdnice 排版