【js】单文件上传和大文件分片上传功能实现

上传功能需要的一些辅助库

1、使用第三方 vant 组件中的提示做 loading 来区分和提示。

2、大文件上传需要计算 MD5 值,使用 spark-md5 库。

3、上传接口,自定义的 axios接口,使用 Apis['方法'] 的方式调用接口(此处不是接口封装的重点)

功能实现

此处使用 vue 形式编写,功能可以提出使用在js方法中,注意去掉 ts 内容

html 复制代码
<template>
	<input ref="inputFile" type="file" hidden @change="checkFiles" />
	<van-button class="upBut" color="#BF234D" @click="onClick">点击上传文件</van-button>
</template>
typescript 复制代码
<script setup lang='ts'>
import Apis from '@/utils/apis';
import FileReaderUpoad from "@/utils/upoad";
import { showLoadingToast, showSuccessToast, showFailToast } from 'vant';

/** 创建上传组件实例 */
const inputFile = ref<HTMLFieldSetElement | null>(null);

/** 创建公共字段集合 */
const data = reactive({
    loading: false,
});
// 点击自定义上传按钮调用上传点击事件
const onClick = () => {
    // 调用 input file 的点击事件
    inputFile.value && inputFile.value.click();
};

// input 获取文件成功后触发的change事件
const checkFiles = (e: Event) => {
	/** 判断拦截是否正在上传 */
	if (data.loading) return;
    data.loading = true;
    /** 获取元素内容 */
    let event: any = e.target as EventTarget;
    /** 获取选中的文件 */
    let file = event.files[0];
    /** 文件使用的验证ID */
    let storageId = "";
    let fileName = "", suffix = "";
    if (file.name) {
        let arr = file.name.split('.');
        fileName = arr.filter((item: object, index: number) => arr.length - 1 != index).join(".");
        suffix = arr.filter((item: object, index: number) => arr.length - 1 == index).join(".");
    };
    /** 提示 */
    showLoadingToast({
        message: `正在解析文件`,
        forbidClick: true,
        duration: 0,
        overlay: true,
    });
	/** 创建文件分片并发实例(方法参数作用请向下找到自定义封装的分片处理文件中查找) */
    let upoad = new FileReaderUpoad({
        file,
        maxNum: 4,
        size: 5242880,
        byteLength: file.size,
        singleFileSize: 209715200,
        /** 单文件上传 */
        once: () => {
            showLoadingToast({
                message: `正在上传中`,
                forbidClick: true,
                duration: 0,
                overlay: true,
            });
            // 单文件上传
            let obj: any = {...};
            Apis.upLoad(obj).then((res: any) => {
                if (res.code == 200) {
                    data.loading = false;
                    showSuccessToast("上传成功");
                };
            }).catch((err) => {
                showFailToast("上传出现问题");
                data.loading = false;
            });
        },
        /** 开始分片处理,创建分片信息,上传分片信息 */
        start: (data: any, next: Function) => {
            /** 开始创建分片信息上传信息, 具体需要什么信息,请联系自己的后端,打印 data 数据查找对应字段 */
            let obj: any = {
            	/** 计算好的 md5 值 */
                fileMd5: data.md5,
                /** 文件总片数 */
                sliceTotalChunks: data.total,
                /** 其他字段,自己添加,从 file 文件数据,或者 data 中查找 */
                ...
			};
            /** 上传分片信息 */
            Apis.start(obj).then((res: any) => {
                if (res.code == 200) {
                	/** 这里后台可能会返回一个验证 ID 要带在每个分片上传中,不需要删除即可 */
                    storageId = res.obj.storageId;
                    /** 当前分片信息已经上传成功,通过调用 next() 函数通知执行下一个逻辑 */
                    next();
                };
            }).catch(() => {
                showFailToast("创建上传信息失败");
                data.loading = false;
            });
        },
        /** 单个分片上传方法 */
        upload: (val: any, next: Function) => {
        	/** 上传分片参数,根据需要添加参数 **/
            let obj: any = {
            	/** 文件名称 */
                name: fileName,
                /** 当前是第几个分片 */
                chunk: val.page,
                /** 一共多少分片 */
                chunks: val.total,
                /** 总文件的md5哈希值 */
                md5: val.totalMd5,
                /** 总文件的大小 */
                size: val.file.size,
                /** 验证 ID 值 */
                tenantId: storageId,
                /** 当前分片的 md5 哈希值 */
                currentFileMd5: val.md5,
                /** 当前分片文件 */
                file: val.blob
            };
            /** 上传单片文件的接口 */
            Apis.burst(obj).then((res: any) => {
            	/** 上传文件成功,直接通过 next() 执行下一步 */
                if (res.code == 200 && res.success) next();
                /** 如果上传失败或者超时,则将当前分片传入 next(val),让分片重新进入队列上传分片 */
                else next(val);
            }).catch(() => {
				/** 如果上传失败或者超时,则将当前分片传入 next(val),让分片重新进入队列上传分片 */
                next(val);
            });
        },
        /** 分片上传,加载进度返回 */
        loading: (val: string) => {
            showLoadingToast({
                message: `已上传${val}%`,
                forbidClick: true,
                duration: 0,
                overlay: true,
            });
        },
        /** 分片上传结束,使用当前函数,通过接口通知后台上传结束 */
        end: (res: any) => {
        	/** 调用结束接口 */
            Apis.end({ tenantId: storageId }).then((res: any) => {
                if (res.code == 200) {
                    showSuccessToast("上传成功");                    
                }
            }).catch(() => {
                showFailToast("上传失败");
                data.loading = false;
            })
        }
    });
    /** 调用函数,开始对文件进行分片处理 */
    upoad.uploadStart();
};
</script>

以下是自定义库的内容,可直接使用

typescript 复制代码
import SparkMD5 from 'spark-md5';
interface FileType {
    file: File;              // 源文件
    size: number;            // 单片大小
    once?: Function;         // 单文件上传
    maxNum?: number;         // 设置最大并发数
    start?: Function;        // 创建开始上传初始化
    end?: Function;          // 分片派发完结束返回
    upload?: Function;       // 抛出所有拆分的分片
    loading?: Function;      // 加载进度
    byteLength: number;      // 文件总大小
    singleFileSize: number;  // 文件超出多大值做分片处理
};

export default class FileReaderUpoad {
    private option: any = [];       // 所有分片存储
    private Setting: FileType;      // 传进来的参数
    private totalFilm!: number;     // 总片数
    private result: string = "";    // 二进制串
    private current: number = 0;    // 当前是第几片
    private totalMd5: string = '';  // 总文件的md5哈希值
    private arrLength: number = 0;  // 总长度

    constructor(Setting: FileType) {
        this.Setting = Setting;
    };

    // 开始执行
    public uploadStart = () => {
        let that = this;
        // 判断文件是否需要分片
        if (this.Setting.file.size >= this.Setting.singleFileSize) {
            // 将文件转化为二进制串
            let fileReader = new FileReader();
            fileReader.readAsBinaryString(this.Setting.file);
            fileReader.onload = function () {
                // 设置并发数
                that.Setting.maxNum = that.Setting.maxNum || 1;
                // 获取二进制串
                that.result = fileReader.result as string;
                // 计算总片数
                that.totalFilm = Math.ceil(that.Setting.byteLength / that.Setting.size);
                // 计算md5值;
                that.getDataMd5(that.result).then((res: any) => {
                    // 计算成功后将参数传出, start 方法调用上传创建信息
                    that.totalMd5 = res;
                    if (typeof (that.Setting.start) === 'function') that.Setting.start({ md5: res, total: that.totalFilm }, () => {
                        that.current = 1;
                        that.option = [];
                        that.burstParam();
                    });
                });
            };
        } else {
            typeof this.Setting.once == 'function' && this.Setting.once();
        };
    };

    // 单个分片解析计算以及参数补齐
    private burstParam() {
        // 如果当前片数小于总片数
        if (this.current < this.totalFilm) {
            // 计算当前片的起始位置和结束位置
            let start = (this.current - 1) * this.Setting.size;
            let end = this.current * this.Setting.size;
            // 对当前片进行分片处理
            this.fileSlice(start, end, (val: string) => {
                // 计算当前片的md5值
                this.getDataMd5(val).then((res: unknown) => {
                    // 将当前片的参数存入option数组中
                    this.option.push({
                        md5: res,
                        chunkFile: val,
                        size: val.length,
                        page: this.current -1,
                        total: this.totalFilm,
                        totalMd5: this.totalMd5,
                        file: this.Setting.file,
                        blob: this.Setting.file.slice(start, end),
                    });
                    // 当前片数加1
                    this.current++;
                    // 递归调用burstParam函数
                    this.burstParam();
                });
            });
        } else {
            // 计算最后一片的起始位置和结束位置
            let start = (this.current - 1) * this.Setting.size;
            let end = this.current * this.Setting.size;
            // 对最后一片进行分片处理
            this.fileSlice(start, end, (val: string) => {
                // 计算最后一片的md5值
                this.getDataMd5(val).then((res: unknown) => {
                    // 将最后一片的参数存入option数组中
                    this.option.push({
                        md5: res,
                        chunkFile: val,
                        size: val.length,
                        page: this.current -1,
                        total: this.totalFilm,
                        totalMd5: this.totalMd5,
                        file: this.Setting.file,
                        blob: this.Setting.file.slice(start, this.Setting.byteLength),
                    });
                    // 记录option数组的长度
                    this.arrLength = this.option.length;
                    // 判断并发数量存不存在,并且要大于 0
                    if (this.Setting.maxNum && this.Setting.maxNum > 0) {
                        // 调用multiRequest函数,传入option数组和最大并发数
                        this.multiRequest(this.option, this.Setting.maxNum).then((res: any) => {
                            // 调用end函数,传入结果
                            typeof this.Setting.end == 'function' && this.Setting.end(res);
                        });
                    } else {
                        // 并发数不能为零
                        new Error("并发数不能为零!");
                    };
                });
            });
        };
    };

    // 分片处理
    private fileSlice = (start: number, end: number, callback: Function) => {
        callback(this.result.slice(start, end));
    };

    // 计算md5值
    private getDataMd5 = (data: string) => {
        return new Promise((resolve, reject) => {
            if (data) resolve(new SparkMD5().appendBinary(data).end());
            else reject("计算失败");
        });
    };

    /**
     * 多个请求并发
     * @param urls 请求地址数组
     * @param maxNum 最大并发数
     * @returns Promise
     */
    private multiRequest = (urls: any, maxNum: number) => {
        // 初始化队列
        let queue: any = [];
        // 克隆urls数组
        let urlsClone = [...urls];
        // 初始化结果数组
        let result = new Array(urls.length);
        // 初始化请求是否完成数组
        let isDoneArr = new Array(urls.length).fill(0);
        // 用于判断请求是否全部完成
        let queueLimit = Math.min(maxNum, urls.length);
        // 初始化计数器
        let index = 0;

        // 请求返回
        const request = (queue: any, url: any) => {
            // 如果有上传函数, 调用 upload 函数传参
            if (typeof this.Setting.upload == 'function') this.Setting.upload(url, (val: object) => {
                // 保证result结果和 urls 顺序相同
                let i = urlsClone.indexOf(url);
                result[i] = url;
                isDoneArr[i] = 1;
                // 判断当返回值val有值时,则接口请求失败,再次添加到请求列表
                if(val) {
                    // 列表边长,说明计算 百分比总数发生变化
                    this.arrLength++;
                    // 添加失败的的接口到请求列表
                    urls.push(val);
                    // 添加到判断请求是否完毕的列表,添加请求次数
                    isDoneArr.push(val);
                }
                // 执行完一个请求后,执行出队操作
                outLine(queue, url);
            })
        };

        // 进入
        const EnterTheTeam = (queue: any = [], url: object) => {
            // 请求入队,并触发数据请求,返回队列长度
            let len = queue.push(url);
            if (len == this.Setting.maxNum) {
                index++;
                // 计算加载进度
                let t = parseInt(String(index / this.arrLength * 100));
                // 存在加载进度函数,传出当前进度
                typeof this.Setting.loading == 'function' && this.Setting.loading(t);
            };
            request(queue, url);
            return len;
        };

        // 初始化队列请求,在此处限制队列长度
        while (EnterTheTeam(queue, urls.shift()) < queueLimit) {};
        
        // 设置定义抛出内容
        let promise: any = {
            resolve: '',
            reject: '',
        };

        // 出队
        const outLine = (queue: any = [], url: object) => {
            // 一个请求完成,出队,下一个队列,如果存在
            queue.splice(queue.indexOf(url), 1);
            if (urls.length) EnterTheTeam(queue, urls.shift());
            else {
                // 判断所有请求完成,再 resolve
                if (isDoneArr.indexOf(0) === -1) {
                    promise.resolve(result);
                    typeof this.Setting.loading == 'function' && this.Setting.loading(100);
                };
            }
        };
        // 返回所有内容
        return new Promise((resolve, reject) => {
            return promise.resolve = resolve;
        });
    };
};
相关推荐
雾散声声慢2 分钟前
前端开发中怎么把链接转为二维码并展示?
前端
熊的猫3 分钟前
DOM 规范 — MutationObserver 接口
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
天农学子3 分钟前
Easyui ComboBox 数据加载完成之后过滤数据
前端·javascript·easyui
mez_Blog4 分钟前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪7 分钟前
vue文本高亮处理
前端·javascript·vue.js
开心工作室_kaic10 分钟前
ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js
放逐者-保持本心,方可放逐10 分钟前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
IT古董11 分钟前
【前端】vue 如何完全销毁一个组件
前端·javascript·vue.js
Henry_Wu00113 分钟前
从swagger直接转 vue的api
前端·javascript·vue.js
奋飞安全14 分钟前
初试js反混淆
开发语言·javascript·ecmascript