前端文件上传功能实战——前端实现(中)


如果没有看过分析篇的铁铁可以先去看看分析篇: 功能分析篇

Chunk

Chunk类用于构造一个文件分片对象,通过这个类我们能获取当前分片的hash,lob对象,大小以及当前分片是处于整个文件中的第几个等属性

核心方法

  1. chunkHash
    chunkHash属性用于获取当前分片的hash

相关实现

class Chunk {
    constructor(chunk, index) {
        this.chunk = chunk;
        this.index = index;
        this.hash = "";
        this.size = this.chunk.size;
        this.reader = null;
    }
    get chunkHash() {
        return hash
    }
}

计算文件hash

文件hash是一个CPU密集型任务,所以只会在任务被启动时才进行计算,确保主线程不会被阻塞,,因为计算hash涉及了IO操作,所以是异步的,为了防止在读取文件的过程中又一次的对reader进行重新赋值,我们可以对reader进行一个缓存处理,如果后续还需要hash,则可以将之前已经计算好的hash直接返回,而不需要重新计算,具体实现如下

get chunkHash() {
    return new Promise((resolve, reject) => {
        if (this.hash) resolve(this.hash);
        if (!this.reader) {
            this.reader = new FileReader();
        }
        this.reader.readAsArrayBuffer(this.chunk);
        this.reader.onload = () => {
            if (this.reader.result) {
                this.hash = SparkMD5.ArrayBuffer.hash(this.reader.result);
                resolve(this.hash);
            } else {
                reject(new Error('Failed to read the chunk.'));
            }
        };
        this.reader.onerror = (event) => {
            reject(event.target.error);
        };
        this.reader.onloadend = () => {
            this.reader = null;
        };
    });
}

Task

每一个分片都需要上传到服务器上,每一个上传都是一个任务——即Task类,我们通过Task实例来执行上传任务,包括当前分片上传进度的监控,取消当前分片的上传等功能

核心成员

  1. start
    开始分片上传,在上传之前会先询问后端此分片是否已经上传过了如果已经上传过了则会跳过此次上传,返回一个Promise,用于TaskQueue处理任务并行调度
  2. changeExist
    用于向后端请求是否需要发送本次分片
  3. stop
    取消本次上传
  4. onUpload
    监听本次分片的上传进度
  5. uploadCallBack
    传入的监听进度的回调函数,调用时会传入当前上传的数据量,当前分片的index
  6. path
    文件上传的路径
  7. existPath
    验证文件是否存在的路径
  8. finish
    此任务是否已经完成

相关实现

class Task {
    constructor(chunk, path, existPath) {
        this.chunk = chunk;
        this.size = this.chunk.size;
        this.xhr = new XMLHttpRequest();
        this.formData = new FormData();
        this.path = path;
        this.existPath = existPath;
        this.finish = false;
        this.hash = "";
        this.uploadCallback = null;
    }
    start() {
        return new Promise((resolve, reject) => {
            this.changeExist().then(exist => {
                if (exist) {
                    this.uploadCallback(this.size, this.chunk.index);
                    this.finish = true;
                    resolve();
                } else {
                    this.xhr.open("POST", this.path, true);
                    this.formData.set("hash", this.hash);
                    this.formData.set("file", this.chunk.chunk);
                    this.xhr.onload = () => {
                        if (this.xhr.status === 200) {
                            this.finish = true;
                            resolve(this.xhr.response);
                        } else {
                            reject(this.xhr.response);
                        }
                    };
                    this.xhr.onerror = () => reject(new Error('Network error'));
                    this.xhr.send(this.formData);
                }
            }).catch(reject);
        });
    }
    changeExist() {
        return new Promise(async (resolve, reject) => {
            this.hash = await this.chunk.chunkHash;
            const xhr = new XMLHttpRequest();
            xhr.open("GET", `${this.existPath}?hash=${encodeURIComponent(this.hash)}`, true);
            xhr.onload = () => {
                const response = JSON.parse(xhr.response);
                if (response.code === 300) {
                    resolve(true);
                } else {
                    resolve(false);
                }
            }
            xhr.onerror = () => reject(new Error('Network error'));
            xhr.send();
        })
    }
    stop() {
        this.xhr.abort();
        this.formData = new FormData();
    }
    onUpload(cb) {
        this.uploadCallback = cb;
        this.xhr.upload.onprogress = (e) => {
            this.uploadCallback(e.loaded, this.chunk.index)
        };
    }
}

TaskQueue

当多个Task实例组合到一起,就是整个文件上传的流程,但我们不能通过直接控制每个Task实例来达成整个文件的开始暂停任务,所以我们通过TaskQueue类来执行包括整个任务队列的启停,整个文件上传进度的监听等功能

核心成员

  1. start
    按顺序开始本次文件上传,同一时间最大上传数量由maxRunning决定
  2. pause
    暂停本次文件上传活动,将当前正在执行的任务全部暂停,重新放回队列等待调用
  3. setOnUpload
    添加文件上传的进度监控事件
  4. next
    每次调用next都会从任务队列中取出任务执行,并在任务执行完后自动循环调用自身
  5. tasks
    任务队列
  6. runTask
    当前正在执行的任务
  7. onFinished
    文件上传结束后的回调函数数组
  8. status
    当前文件上传的状态,分为waiting,paused,running,fulfilled四种

相关实现

class TaskQueue {
    constructor(maxRunning = 1) {
        this.tasks = [];
        this.status = "waiting";
        this.maxRunning = maxRunning;
        this.running = 0;
        this.runTask = [];
        this.onFinished = [];
    }
    get taskCount() {
        return this.tasks.length;
    }
    add(...tasks) {
        this.tasks.push(...tasks)
    }
    start() {
        if (this.status === "running") return;
        this.status = "running";
        while (this.running < this.maxRunning && this.taskCount > 0) {
            this.next()
            this.running++;
        }
    }
    pause() {
        if (["waiting", "fullfilled", "paused"].includes(this.status)) return;
        this.status = "paused"
        this.add(...this.runTask);
        this.runTask.forEach(task => task.stop());
        this.running = 0;
        this.runTask = [];
    }
    setOnFinished(cb) {
        this.onFinished.push(cb);
    }
    finish() {
        this.pause();
        this.status = "fullfilled";
        this.tasks = [];
        this.runTask = [];
        this.running = 0;
        this.onFinished.forEach(cb => cb());
    }
    setOnUpload(cb) {
        cb()
    }
    next() {
        if (this.taskCount <= 0) {
            return;
        }
        const task = this.tasks.shift();
        this.runTask.push(task);
        task.start().then(() => {
            this.runTask = this.runTask.filter(task => !task.finish);
            if (this.runTask.length <= 0) {
                this.status = "fullfilled";
                if (this.onFinished.length > 0) {
                    this.onFinished.forEach(cb => cb());
                }
            }
            this.next();
        }).catch(error => console.error('Error in task:', error));
    }
}

文件上传的进度监听

我们的Task本身将当前请求的相关事件暴露了出来,同时还提供了上传数据量以及是第几个分片,通过构造一个和分片数量一致的数组,每次触发这个事件时将得到的数据量写入数组对应下标中,将整个数组中的数字相加就是整个文件上传的数据量,具体实现如下

setOnUpload(cb) {
    const totals = Array(this.taskCount).fill(0)
    function _upload(loaded, index) {
        totals[index] = loaded;
        cb(totals.reduce((a, b) => a + b, 0))
    }
    this.tasks.forEach(task => task.onUpload(_upload))
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值