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


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

BigFile

一个BigFile通常会由当前文件对应的TaskQueue,由Chunk组成的数组,当前文件的hash以及对应事件的处理组成,BIgFile也是我们在整个流程唯二需要手动操作的类

核心成员

  1. cutFile
    将获得的文件对象分割成指定大小的分片并构造Chunk对象存入chunks中
  2. fileExist
    在获得当前文件的hash之后需要向后端询问此文件是否已经存在服务器上,如果存在则需要终止本次文件上传
  3. upload
    开始或继续上传任务
  4. pause
    暂停本次上传
  5. finish
    文件分片是否上传完成
  6. emerge
    文件是否已经合并
  7. fileUploadPath
    文件上传路径
  8. fileExistPath
    文件校验是否存在服务器路径
  9. chunks
    Chunk对象数组
  10. tasks
    该文件的TaskQueue对象

相关实现

class BigFile {
    constructor({ file, maxRunning = 1, chunkSize = 1 * 1024 * 1024, fileUploadPath, fileExistPath } = opts) {
        this.file = file;
        this.filename = this.file.name;
        this.filesize = this.file.size;
        this.tasks = new TaskQueue(maxRunning);
        this.chunks = [];
        this.chunkSize = chunkSize;
        this.chunksCount = Math.ceil(this.filesize / this.chunkSize);
        this.fileHash = "";
        this.finish = false;
        this.emerge = false;
        this.fileDOM = new FileDOM(this.filename);
        this.fileUploadPath = fileUploadPath;
        this.fileExistPath = fileExistPath;
        this.cutFile();
    }
    cutFile() {
        for (let i = 0; i < this.chunksCount; i++) {
            const start = i * this.chunkSize;
            const end = Math.min(start + this.chunkSize, this.filesize);
            const chunk = this.file.slice(start, end);
            this.chunks.push(new Chunk(chunk, i));
            const task = new Task(this.chunks[i], this.fileUploadPath, this.fileExistPath);
            this.tasks.add(task);
        }
    }
    setFileHash(hash) {
        this.fileHash = hash;
        if (this.finish) return;
        this.fileExist()
    }
    fileExist() {
        const xhr = new XMLHttpRequest();
        xhr.open('GET', `${this.fileExistPath}?hash=${hash}&isFile=${true}&filename=${encodeURIComponent(this.filename)}`);
        xhr.setRequestHeader('Content-Type', 'application/json');
        xhr.onload = () => {
            try {
                const response = JSON.parse(xhr.response);
                if (response.code === 300) {
                    this.finish = true;
                    this.emerge = true;
                    this.tasks.finish();
                }
            } catch (e) {
                console.error("Failed to parse JSON response.", e);
            }
        };
        xhr.onerror = () => {
            console.error("Network error occurred.");
        };
        xhr.send();
    }
    upload() {
        this.tasks.start();
    }
    stop() {
        this.tasks.pause();
    }
    onProgress(cb) {
        const _upload = (total) => {
            cb(this.filesize, total)
        }
        this.tasks.setOnUpload(_upload);
    }
    onFinished(cb) {
        this.tasks.setOnFinished(cb);
    }
}

BigFileList

我们并不能设想在同一时间内用户只会上传一个文件,在同一时间时如果用户上传多个文件那么我们该怎么实现控制多个文件的上传顺序,启停单独的文件,使所有文件同时暂停开始的功能,我们通过构造BigFileList类来实现上述功能

核心方法

  1. startItem
    传入一个下标,用于启动当前下标位置的文件上传任务
  2. stopItem
    同startItem
  3. getFileIndex
    传入一个BigFile对象,获取当前对象在BigFileList中的下标
  4. start
    根据MaxRunning来开启对应数量的文件上传任务
  5. next
    功能同TaskQueue中的next
  6. stop
    停止所有的文件上传
  7. clear
    在停止所有的文件上传任务的同时清除所有BigFile对象,清空页面中的文件列表
  8. maxRunning
    同一时间内文件的最大上传数量
  9. status
    当前文件列表的状态,同TaskQueue中的status

相关实现

class BigFileList {
    constructor(fileMaxRunning = 3) {
        this.list = [];
        this.maxRunning = fileMaxRunning;
        this.running = 0;
        this.status = "waiting";
    }
    add(bigFile) {
        this.list.push(bigFile);
    }
    getFileIndex(file) {
        return this.list.findIndex(item => item === file);
    }
    get listCount() {
        return this.list.length;
    }
    start(cb) {
        this.status = "running";
        if (this.running > this.maxRunning) return;
        for (let i = 0; i < this.listCount; i++) {
            this.startItem(i);
            cb && cb(this.list[i]);
        }
    }
    startItem(index) {
        this.status = "running";
        if (this.running >= this.maxRunning) return;
        if (this.list[index].finish) return;
        this.next(index);
    }
    stop(cb) {
        if (this.status === "waiting") return;
        this.status = "waiting";
        this.running = 0;
        for (const file of this.list) {
            file.stop();
            cb && cb(file);
        }
    }
    stopItem(index) {
        if (this.status === "waiting") return;
        const file = this.list[index];
        file.stop();
        this.running--;
        if (this.running === 0) this.status = "waiting";
    }
    deleteItem(index) {
        this.stopItem(index);
        this.list[index].fileDOM.onDeleteEvent();
        this.list = this.list.filter((v, i) => i !== index)
    }
    clear(cb) {
        this.stop()
        for (const file of this.list) {
            file.fileDOM.onDeleteEvent();
        }
        this.list = [];
        this.running = 0;
        this.status = "waiting";
        cb && cb();
    }
    next(index) {
        if (index >= this.listCount) {
            return;
        }
        if (this.list[index].finish) {
            this.next(index + 1);
            return;
        }
        const file = this.list[index];
        this.running++;
        file.upload();
        file.onFinished(() => {
            this.running--;
            this.next(index + 1);
        }, (error) => {
            console.error("Error:", error);
        });
    }
}

其他代码

至此我们所有的功能都封装完毕,我们只需在现有的基础上调用暴露出来的接口就能完整整个文件上传的所有功能,以下是具体代码

import { BigFile, BigFileList } from "./File.js";
import { DOM } from "./render.js";
const dom = new DOM();
const bigFiles = new BigFileList(3);
dom.onFileChange((files) => {
    for (const file of files) {
        const bigFile = new BigFile({
            file: file,
            maxRunning: 3,
            fileUploadPath: "http://localhost:3000/file",
            fileExistPath: "http://localhost:3000/exist",
            chunkSize: 1024 * 1024 * 1,
        });
        bigFiles.add(bigFile);
        dom.addFile(bigFile);
        bigFile.onProgress(createProgressHandler(bigFile));
        bigFile.onFinished(createFinishHandler(bigFile));
        updateStatusIfNotFinished(bigFile, "暂停中");
        setupButtons(bigFile);
        createWorker(bigFile);
    }
    bigFiles.start();
});
dom.fileListStopButton.addEventListener("click", () => {
    bigFiles.stop((file) => {
        updateStatusIfNotFinished(file, "暂停中");
    });
})
dom.fileListStartButton.addEventListener("click", () => {
    bigFiles.start((file) => {
        updateStatusIfNotFinished(file, "上传中");
    })
})
dom.fileListClearButton.addEventListener("click", () => {
    bigFiles.clear();
})
function emerge(bigFile, cb) {
    if (!bigFile.finish || bigFile.emerge) return;
    const hashs = bigFile.chunks.map((v) => v.hash);
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "http://localhost:3000/emerge");
    const infos = {
        name: bigFile.filename,
        hashs: hashs,
        fileHash: bigFile.fileHash,
    };
    xhr.setRequestHeader("Content-Type", "application/json");
    xhr.send(JSON.stringify(infos));
    xhr.onload = () => {
        if (xhr.status === 200) {
            bigFile.emerge = true;
            cb && cb();
        } else {
            cb && cb(xhr.responseText);
        }
    };
    xhr.onerror = () => {
        cb && cb("网络请求失败");
    };
}
function createProgressHandler(bigFile) {
    let startDate = new Date();
    return (filesize, total) => {
        const newDate = new Date();
        bigFile.fileDOM.updatePercent(`${(total / filesize * 100).toFixed(2)}%`);
        bigFile.fileDOM.updateSpeed(`${(total / ((newDate - startDate) / 1000) / 1000000).toFixed(1)}MB/s`);
        updateStatusIfNotFinished(bigFile, "上传中");
    };
}
function createFinishHandler(bigFile) {
    return () => {
        bigFile.finish = true;
        bigFile.fileDOM.updateStatus("上传完成");
        bigFile.fileDOM.updatePercent("100%");
        bigFile.fileDOM.updateSpeed("0MB/s");
        emerge(bigFile);
    };
}
function setupButtons(bigFile) {
    bigFile.fileDOM.startButton.addEventListener("click", () => {
        if (bigFile.finish) return;
        const index = bigFiles.getFileIndex(bigFile);
        bigFiles.startItem(index);
    });
    bigFile.fileDOM.stopButton.addEventListener("click", () => {
        if (bigFile.finish) return;
        const index = bigFiles.getFileIndex(bigFile);
        bigFiles.stopItem(index);
        updateStatusIfNotFinished(bigFile, "暂停中")
    });
    bigFile.fileDOM.deleteButton.addEventListener("click", () => {
        const index = bigFiles.getFileIndex(bigFile);
        bigFiles.deleteItem(index);
    });
}
function createWorker(bigFile) {
    const myWorker = new Worker("./js/worker.js");
    myWorker.postMessage(bigFile.chunks);
    myWorker.onmessage = (e) => {
        const hash = e.data;
        bigFile.setFileHash(hash);
        emerge(bigFile);
    };
    myWorker.onerror = (error) => {
        console.error("Worker error:", error);
    };
    bigFile.onFinished(() => {
        myWorker.terminate();
    });
}
function updateStatusIfNotFinished(bigFile, status) {
    if (bigFile.finish) return;
    bigFile.fileDOM.updateStatus(status);
}

webWorker

webWorker中的代码

importScripts("../untils/spark-md5.js")
function hash(chunks) {
    return new Promise((resolve, reject) => {
        const sparkMD5 = new SparkMD5()
        function _read(i) {
            if (i >= chunks.length) {
                resolve(sparkMD5.end())
                return
            }
            const fileReader = new FileReader();
            fileReader.readAsArrayBuffer(chunks[i].chunk)
            fileReader.onload = () => {
                sparkMD5.append(fileReader.result)
                _read(i + 1)
            }
        }
        _read(0)
    })
}
onmessage = async function (e) {
    const chunks = e.data
    const md5 = await hash(chunks)
    postMessage(md5)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值