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


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

基础代码

html部分代码很简单:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件上传</title>
    <link rel="stylesheet" href="./font/iconfont.css">
    <link rel="stylesheet" href="./css/index.css">
</head>

<body>
    <div class="main">
        <input type="file" name="file" id="file" multiple>
        <label for="file" class="file-label" draggable="true">请将多个文件拖拽到此处进行扫描</label>
        <div class="buttons">
            <div class="left">
                <button class="file-choose">选择文件</button>
                <button class="folder-choose">选择文件夹</button>
            </div>
            <div class="right">
                <button class="list-stop">暂停全部任务</button>
                <button class="list-start">开始全部任务</button>
                <button class="list-delete">删除全部任务</button>
            </div>
        </div>
        <div class="file-list">
            <div class="list-title list-column">
                <div class="list-row">文件名</div>
                <div class="list-row">上传进度</div>
                <div class="list-row">状态</div>
                <div class="list-row">操作</div>
            </div>
            <div class="list-body">
            </div>
        </div>
    </div>
    <script src="./untils/spark-md5.js"></script>
    <script src="./js/main.js" type="module"></script>
</body>

</html>

css部分受限于篇幅故不展示

文件展示模板

const fileItem = document.createElement('div');
fileItem.classList.add('list-column');
fileItem.innerHTML = `
        <div class="list-row list-name">${filename}</div>
        <div class="list-row">
            <div class="progress" style="--percent: ${percent};">
                <div class="progress-bar">
                    <div class="progress-bar-inner"></div>
                </div>
                <div class="progress-number">
                    <span class="progress-percent">${percent}</span>
                    <span class="progress-speed">${speed}</span>
                </div>
            </div>
        </div>
        <div class="list-row">
            <div class="row-status">${this.status}</div>
        </div>
        <div class="list-row">
            <div class="row-operate">
                <div class="row-stop iconfont icon-zanting"></div>
                <div class="row-start iconfont icon-kaishi"></div>
                <div class="row-delete iconfont icon-shanchu"></div>
            </div>
        </div>
`;

整体效果

整体效果如下

效果

DOM

DOM类用于获取页面相关元素并提供对应的事件绑定

核心方法

它的核心方法如下:

  1. addFile
    接受一个BigFile类型的参数file
    将文件的DOM元素添加到文件列表中显示
  2. onFileChange
    接受一个函数类型的参数cb
    添加事件监听器,监听文件按钮的点击事件和拖放事件
    将处理好的file数组传入cb中
  3. onFolderChange
    接受一个函数类型的参数cb
    添加事件监听器,监听文件夹按钮的变化事件
    将处理好的file数组传入cb中

相关实现

class DOM {
    constructor() {
        this.files = []
        this.fileButton = document.querySelector('.file-choose');
        this.folderButton = document.querySelector('.folder-choose');
        this.fileDragger = document.querySelector('.file-label');
        this.fileList = document.querySelector('.list-body');
        this.fileListStopButton = document.querySelector('.list-stop');
        this.fileListStartButton = document.querySelector('.list-start');
        this.fileListClearButton = document.querySelector('.list-delete');
    }
    addFile(file) {
        this.files.push(file);
        this.fileList.appendChild(file.fileDOM.getDOM());
    }
    removeFile(file) {
        this.files = this.files.filter(f => f !== file);
    }
    onFileChange(cb) {
        this.fileButton.addEventListener('click', async (e) => {
            const files = []
            try {
                const handleFiles = await showOpenFilePicker({
                    multiple: true
                })
                for (const item of handleFiles) {
                    const file = await handleFile(item)
                    files.push(file)
                }
            } catch (err) {
                console.log(err);
                return;
            }
            cb(files);
        });
        this.fileDragger.addEventListener('dragover', (e) => {
            e.preventDefault();
        });
        this.fileDragger.addEventListener('dragenter', (e) => {
            e.preventDefault()
        });
        this.fileDragger.addEventListener('drop', async (e) => {
            e.preventDefault()
            const files = Array.from(e.dataTransfer.files)
            cb(files)
        });
    }
    onFolderChange(cb) {
        this.folderButton.addEventListener('change', cb);
    }
}

从句柄到文件对象

无论是通过showOpenFilePicker还是通过showDirectoryPicker来获取文件,本质上获取的是文件的句柄对象,句柄对象无法直接使用,我们可以通过getFile方法来得到句柄对象对应的file对象

async function handleFile(item) {
    const file = await item.getFile();
    return file;
}

如果此时的句柄对象指向的是文件夹的话,我们就需要通过entries方法得到此文件的所有子文件,通过kind属性来判断此时的句柄对象是否指向文件

    async processHandle(handle) {
        if (handle.kind === "file") {
            return;
        }
        const entries = await handle.entries();
        handle.children = [];
        for await (const entry of entries) {
            const subHandle = entry[1];
            handle.children.push(subHandle);
        }
    }

当然,如果文件夹里的子文件数量十分的多的话我们不断调用processHandle方法会造成页面阻塞,我们可以通过利用浏览器空余时间处理,利用多线程技术,当用户需要时在处理当前层数的子文件
具体关于浏览器文件系统相关API的内容可以看我这篇文章
未动笔,未来可寄

FileDOM

BigFileDOM类主要控制每个文件在页面的相关展示,功能绑定等

核心方法

它的核心方法如下:

  1. onStopEvent
    为暂停按钮添加点击事件监听器
  2. onStartEvent
    为开始按钮添加点击事件监听器
  3. onDeleteEvent
    为删除按钮添加点击事件监听器,同时会将该文件对应的DOM元素移除
  4. getDOM
    返回创建的DOM元素
  5. updateStatus
    更新文件的状态
  6. updatePercent
    更新文件的进度百分比
  7. updateSpeed
    更新文件的传输速度
  8. createDOM
    创建与文件相关的DOM结构

相关实现

class FileDOM {
    constructor(filename) {
        this.filename = filename;
        this.status = "读取中";
        this.percent = "0%";
        this.speed = "0KB/s";
        this.dom = this.createDOM();
        this.stopButton = this.dom.querySelector('.row-stop');
        this.startButton = this.dom.querySelector('.row-start');
        this.deleteButton = this.dom.querySelector('.row-delete');
        this.statusElement = this.dom.querySelector('.row-status');
        this.percentElement = this.dom.querySelector('.progress-percent');
        this.speedElement = this.dom.querySelector(".progress-speed");
        this.processElement = this.dom.querySelector('.progress');
    }
    onStopEvent(cb) {
        this.stopButton.addEventListener('click', cb);
    }
    onStartEvent(cb) {
        this.startButton.addEventListener('click', cb);
    }
    onDeleteEvent(cb) {
        this.deleteButton.addEventListener('click', (e) => {
            this.dom.remove();
            cb && cb();
        });
    }
    getDOM() {
        return this.dom;
    }
    updateStatus(status) {
        this.status = status;
        this.statusElement.innerText = this.status;
    }
    updatePercent(percent) {
        this.percent = percent;
        this.percentElement.innerText = this.percent;
        this.processElement.style = `--percent:${this.percent};`;
    }
    updateSpeed(speed) {
        this.speed = speed;
        this.speedElement.innerText = this.speed;
    }
    createDOM() {
        const fileItem = document.createElement('div');
        fileItem.classList.add('list-column');
        fileItem.innerHTML = `
        <div class="list-row list-name">${this.filename}</div>
        <div class="list-row">
            <div class="progress" style="--percent: ${this.percent};">
                <div class="progress-bar">
                    <div class="progress-bar-inner"></div>
                </div>
                <div class="progress-number">
                    <span class="progress-percent">${this.percent}</span>
                    <span class="progress-speed">${this.speed}</span>
                </div>
            </div>
        </div>
        <div class="list-row">
            <div class="row-status">${this.status}</div>
        </div>
        <div class="list-row">
            <div class="row-operate">
                <div class="row-stop iconfont icon-zanting"></div>
                <div class="row-start iconfont icon-kaishi"></div>
                <div class="row-delete iconfont icon-shanchu"></div>
            </div>
        </div>
`;
        return fileItem;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值