功能描述
这个功能是这样,有一个视频文件的管理页面,可以选择多个视频后,进行上传。问题出在了多个(100以上)视频文件选择的这一步,部分电脑可能会卡顿甚至卡死。
我顺着代码捋了一下,大概代码如下:
- 视频文件选择使用的组件是 element-plus 的 Upload 组件,可以通过 on-change 传入在文件发生变化后执行的方法。
- 在on-change中,通过 getFileMd5 获取文件的 MD5 信息;
- 在on-change中,通过 getVideoInfo 获取视频文件的时长、宽、高信息;
我现在自己的电脑上,通过浏览器的任务管理器看了一下, getFileMd5 和 getVideoInfo 都会造成浏览器的占用的CPU和内存过大的问题。
看下这两个方法是如何实现的:
js
import SparkMD5 from 'spark-md5'
// 获取文件的 MD5
export const getFileMd5 = file => {
return new Promise((resolve, reject) => {
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
reader.onload = function (e) {
// 使用 SparkMD5 计算 md5
spark.append(e.target.result)
const md5 = spark.end() // 完成计算
resolve(md5)
}
reader.onerror = function (error) {
reject(error)
}
// 读取文件为 ArrayBuffer
reader.readAsArrayBuffer(file)
})
}
js
// 获取视频的时长、宽度和高度
export const getVideoInfo = file => {
return new Promise((resolve, reject) => {
const url = URL.createObjectURL(file) // 创建一个指向该视频文件的 url
const video = document.createElement('video')
video.src = url
video.onloadedmetadata = () => {
resolve({
duration: video.duration, // 获取时长
width: video.videoWidth, // 获取宽度
height: video.videoHeight // 获取高度
})
URL.revokeObjectURL(url) // 释放URL对象
}
video.load() // 开始加载视频文件
})
}
优化 MD5 的获取方法
主要思路是:通过将文件拆分为多个块,最后再合并计算最终的 MD5。这样,即使处理大文件,也不会导致浏览器的占用过大。
js
// 主要思路,将文件分多次 spark.append,避免一次性处理
const getFileMd5 = file => {
return new Promise((resolve, reject) => {
const chunkSize = 1024 * 1024 * 2; // 拆分的大小
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
let currentChunk = 0;
const totalChunks = Math.ceil(file.size / chunkSize);
function loadNextChunk() {
const start = currentChunk * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end); // 拆分文件
reader.onload = (e) => {
spark.append(e.target.result);
currentChunk++;
// 如果未处理完,则继续处理
if (currentChunk < totalChunks) {
loadNextChunk();
} else {
resolve(spark.end()); // 输出 md5 值
}
};
reader.onerror = (error) => {
reject(error) ;
};
reader.readAsArrayBuffer(chunk); // Read the chunk
}
loadNextChunk();
})
}
优化视频信息获取方法
目前没有什么特别好的方法,只能减少 url 创建的次数,减轻一点负担。改进后如下:
js
const getVideoInfo = file => {
return new Promise((resolve, reject) => {
// 因为 element-plus 的 Upload 组件会默认生成 url,所以就可以省去重复创建 url 的步骤。
const url = file.url || URL.createObjectURL(file.raw || file)
const video = document.createElement('video')
video.src = url
video.onloadedmetadata = () => {
resolve({ duration: video.duration, width: video.videoWidth, height: video.videoHeight})
!file.url && URL.revokeObjectURL(url) // 释放URL对象
}
video.load() // 开始加载视频文件
})
}
限制最大执行队列的数量
上述两个方法在优化完成之后,效果并不显著,经过排查,可能是因为 on-change 事件在同一时间触发次数太多导致的。所以写了一套逻辑,限制了一下最大同时执行的队列。
js
// 定义数据
const state = reactive({
currentList: [], // 正在执行的队列
maxQueue: 5, // 最大执行队列数量
waitList:[], // 待执行队列
})
// 响应 element-plus upload 组件的 on-change 事件
function changeHandler (file){
if(state.currentList.length < state.maxQueue){
handleOne(file)
} else {
state.waitList.push(file)
}
}
// 单个文件处理
async function handleOne(file){
state.currentList.push(file)
const { duration, width, height } = await getVideoInfo(file)
file.duration = duration
file.dimension = width + 'x' + height
file.md5Hash = await getFileMd5(file.raw)
afterHandler(file)
}
// 执行完成之后,判断是否还有待处理的文件,继续执行
function afterHandler (file){
state.currentList = state.currentList.filter((fl) => {
return fl.md5Hash !== file.md5Hash && fl.name !== file.name
})
if(state.waitList.length){
let frist = state.waitList[0]
state.waitList = state.waitList.filter(fl => fl != frist)
handleOne(frist)
}
}
然后根据实际情况,调整 state.maxQueue 的值,来控制最大执行队列数量。
总结
经过以上的优化,可以同时选择的文件数量有了大幅提升,目前测试下来,数量提升到700时,页面也还是比较流畅的,再往上就没有进行更进一步的测试。
所以,对于文件总量较多的场景,还是需要通过分批、分片的形式进行处理,减少并发数量,这样的话,可以有效避免出现浏览器性能问题。