• 基于Vue的Upload组件实现


    Upload组件基本实现

    仓库:https://gitee.com/aeipyuan/upload_component

    前端

    1. 组件结构

    upload组件

    <template>
    	<div class="uploadWrap">
    		<!-- 按钮 -->
    		<div class="upload">
    			<input type="file" class="fileUp" @change="uploadChange" multiple :accept="acceptTypes">
    			<button>点击上传</button>
    		</div>
    		<!-- 提示文字 -->
    		<span class="tips">
    			只能上传小于{{maxSize}}M的
    			<span v-for="type in fileTypes" :key="type">
    				{{type}}
    			</span>
    			格式图片,自动过滤
    		</span>
    		<transition-group appear tag="ul">
    			<!-- 上传标签 -->
    			<li class="imgWrap" v-for="item in fileList" :key="item.src">
    				<!-- 图片 -->
    				<div class="left">
    					<img :src="item.src" @load="revokeSrc(item.src)">
    				</div>
    				<!-- 右边文字和进度 -->
    				<div class="right">
    					<span class="name">{{item.name}} </span>
    					<span class="num">
    						<span>{{item.progress}} %</span>
    						<span class="continue" v-if="item.isFail" @click="continueUpload(item)">重试</span>
    					</span>
    					<div class="bar" :style="`${item.progress}%`"></div>
    				</div>
    				<!-- 取消上传标签 -->
    				<span class="cancle" @click="removeImg(item)">×</span>
    				<!-- 上传成功和失败tips -->
    				<span v-if="item.isFinished||item.isFail" :class="['flag',item.isFail?'redBd':(item.isFinished?'greenBd':'')]">
    					<span>{{item.isFail?'✗':(item.isFinished?'✓':'')}}</span>
    				</span>
    			</li>
    		</transition-group>
    	</div>
    </template>
    

    2. 响应式数据

    data() {
        return {
            fileList: [],/* 文件列表 */
            maxLen: 6,/* 请求并发数量 */
            finishCnt: 0/* 已完成请求数 */
        }
    }
    

    3. 父子传值

    父组件可以通过属性传值设置上传的url,文件大小,文件类型限制,并且可监听上传输入改变和上传完成事件获取文件列表信息

    /* 父组件 */
    <Upload
        :uploadUrl="`http://127.0.0.1:4000/multi`"
        :maxSize="5"
        :reqCnt="6"
        :fileTypes="['gif','jpeg','png']"
        @fileListChange="upChange"
        @finishUpload="finishAll" />
    /* 子组件 */
    props: {
        maxSize: {
            type: Number,
            default: 2
        },
        fileTypes: {
            type: Array,
            default: () => ['img', 'png', 'jpeg']
        },
        uploadUrl: {
            type: String,
            default: 'http://127.0.0.1:4000/multi'
        },
        reqCnt: {/* 最大请求并发量,在created赋值给maxLen */
            default: 4,
            validator: val => {
                return val > 0 && val <= 6;
            }
        }
    }
    

    4. 所有upload组件公用的属性和方法

    // 请求队列
    let cbList = [], map = new WeakMap;
    // 过滤不符合条件的文件
    function filterFiles(files, fileTypes, maxSize) {
    	return files.filter(file => {
    		let index = file.name.lastIndexOf('.');
    		let ext = file.name.slice(index + 1).toLowerCase();
    		// 处理jepg各种格式
    		if (['jfif', 'pjpeg', 'jepg', 'pjp', 'jpg'].includes(ext))
    			ext = 'jpeg';
    		if (fileTypes.includes(ext) && file.size <= maxSize * 1024 * 1024) {
    			return true;
    		} else {
    			return false;
    		}
    	})
    }
    // 格式化文件名
    function formatName(filename) {
    	let lastIndex = filename.lastIndexOf('.');
    	let suffix = filename.slice(0, lastIndex);
    	let fileName = suffix + new Date().getTime() + filename.slice(lastIndex);
    	return fileName;
    }
    // 请求
    function Ajax(options) {
    	// 合并
    	options = Object.assign({
    		url: 'http://127.0.0.1:4000',
    		method: 'POST',
    		progress: Function.prototype
    	}, options);
    	// 返回Promise
    	return new Promise((resolve, reject) => {
            let xhr = new XMLHttpRequest;
            /* 触发进度条更新 */
    		xhr.upload.onprogress = e => {
    			options.progress(e, xhr);
    		}
    		xhr.open(options.method, options.url);
    		xhr.send(options.data);
    		xhr.onreadystatechange = () => {
    			if (xhr.readyState === 4) {
    				if (/^(2|3)d{2}$/.test(xhr.status)) {
    					resolve(JSON.parse(xhr.responseText));
    				} else {
    					reject({ msg: "请求已中断" });
    				}
    			}
    		}
    	})
    }
    

    5. input标签change事件

    1. 根据父组件传入的规则对选中的文件进行过滤
    2. 遍历过滤后的数组,生成监听的数组(直接监听原数组浪费性能)
    3. 设置属性监听各种操作
    4. 将请求函数存入队列,延迟执行
    5. 调用request,若有剩余并发量则发起请求
    /* <input type="file" class="fileUp" @change="uploadChange" multiple :accept="acceptTypes"> */
    uploadChange(e) {
        let files = filterFiles([...e.target.files], this.fileTypes, this.maxSize);//过滤
        this.fileList = this.fileList.concat(files.map((file, index) => {
            // 创建新对象,不直接监听file提高性能
            let newFile = {};
            newFile.name = formatName(file.name);
            newFile.src = window.URL.createObjectURL(file);// 临时图片预览src
            newFile.progress = 0;
            newFile.abort = false;// 取消上传事件
            newFile.imgSrc = "";// 返回的真实src
            // 成功和失败标记
            newFile.isFinished = false;
            newFile.isFail = false;
            // 上传起始和结束点
            newFile.start = 0;
            newFile.total = file.size;
            // 存入队列后发起上传
            cbList.push(() => this.handleUpload(file, newFile));
            this.request();
            return newFile;
        }));
    },
    

    6. request函数

    request函数用于实现请求并发

    request() {
        // 还有剩余并发数则执行队头函数
        while (this.maxLen > 0 && cbList.length) {
            let cb = cbList.shift();
            this.maxLen--;
            cb();
        }
    }
    

    7. handleUpload函数

    handleUpload函数用于文件切片,发起Ajax请求,触发各种请求处理事件等功能

    handleUpload(file, newFile) {
        let chunkSize = 1 * 2048 * 1024;// 切片大小2M
        // 设置文件上传范围
        let fd = new FormData();
        let start = newFile.start;
        let total = newFile.total;
        let end = (start + chunkSize) > total ?
            total : (newFile.start + chunkSize);
        // 上传文件信息
        let fileName = newFile.name;
        fd.append('chunk', file.slice(start, end));
        fd.append('fileInfo', JSON.stringify({
            fileName, start
        }));
        return Ajax({
            url: this.uploadUrl,
            data: fd,
            progress: (e, xhr) => {
                // 因为会加上文件名和文件夹信息占用字节,还要等待响应回来,所以取小于等于95
                let proNum = Math.floor((newFile.start + e.loaded) / newFile.total * 100);
                newFile.progress = Math.min(proNum, 95);
                // 手动中断上传
                if (newFile.abort) {
                    xhr.abort();
                }
            }
        }).then(res => {
            if (end >= total) {
                // 跳至100
                newFile.progress = 100;
                // 存url
                newFile.imgSrc = res.imgSrc;
                // 状态改变通知
                newFile.isFinished = true;
                this.finishCnt++;
                this.fileListChange();
            } else {
                // 新的起始点
                newFile.start = end + 1;
                // 发送剩余资源
                cbList.push(() => this.handleUpload(file, newFile));
            }
        }, err => {
            newFile.isFail = true;
            // 建立映射,点击重传使用
            map.set(newFile, file);
        }).finally(() => {
            // 处理完一个请求,剩余并发数+1,重新调用request
            this.maxLen++;
            this.request();
        });
    }
    

    8. 清理图片缓存

    window.URL.createObjectURL(file)创建的src对应图片加载完毕以后需要移除缓存

    /* <img :src="item.src" @load="revokeSrc(item.src)"> */
    // 移除url缓存
    revokeSrc(url) {
        window.URL.revokeObjectURL(url);
    }
    

    9. 取消上传

    /* <span class="cancle" @click="removeImg(item)">×</span> */
    removeImg(item) {
        item.abort = true;//触发中断
        let index = this.fileList.indexOf(item);
        if (index !== -1) {
            this.fileList.splice(index, 1);
            this.fileListChange();
        }
    }
    

    10. 重试

    遇到断网等特殊情况请求处理失败后可通过点击重试重新发起请求

    /* <span class="continue" v-if="item.isFail" @click="continueUpload(item)">重试</span> */
    continueUpload(newFile) {
        newFile.isFail = false;
        let file = map.get(newFile);
        cbList.push(() => this.handleUpload(file, newFile));
        this.request();
    }
    

    后端

    1. 路由处理

    /* app.js */
    const app = require('http').createServer();
    const fs = require('fs');
    const CONFIG = require('./config');
    const controller = require('./controller');
    const path = require('path');
    app.on('request', (req, res) => {
        /* 跨域 */
        res.setHeader('Access-Control-Allow-Origin', '*');
        res.setHeader('Access-Control-Allow-Methods', '*');
        /* 处理请求 */
        let { method, url } = req;
        console.log(method, url)
        method = method.toLowerCase();
        if (method === "post") {
            /* 上传 */
            if (url === '/multi') {
                controller.multicleUpload(req, res);
            }
        }
        else if (method === 'options') {
            res.end();
        }
        else if (method === 'get') {
            /* 静态资源目录 */
            if (url.startsWith('/static/')) {
                fs.readFile(path.join(__dirname, url), (err, data) => {
                    if (err)
                        return res.end(JSON.stringify({ msg: err }));
                    res.end(data);
                })
            }
        }
    })
    app.listen(CONFIG.port, CONFIG.host, () => {
        console.log(`Server start at ${CONFIG.host}:${CONFIG.port}`);
    })
    

    2. 文件解析和写入

    function multicleUpload(req, res) {
        new multiparty.Form().parse(req, (err, fields, file) => {
            if (err) {
                res.statusCode = 400;
                res.end(JSON.stringify({
                    msg: err
                }))
            }
            try {
                // 提取信息
                let { fileName, start } = JSON.parse(fields.fileInfo[0]);
                // 文件块
                let chunk = file.chunk[0];
                let end = start + chunk.size;
                // 文件路径
                let filePath = path.resolve(__dirname, CONFIG.uploadDir, fileName);
                // 创建IO流
                console.log(start, end);
                let ws;
                let rs = fs.createReadStream(chunk.path);
                if (start == 0)
                    ws = fs.createWriteStream(filePath, { flags: 'w' });//创建
                else
                    ws = fs.createWriteStream(filePath, { flags: 'r+', start });//选定起始位修改
                rs.pipe(ws);
                rs.on('end', () => {
                    res.end(JSON.stringify({
                        msg: '上传成功',
                        imgSrc: `http://${CONFIG.uploadIP}:${CONFIG.port}/${CONFIG.uploadDir}/${fileName}`
                    }))
                })
            } catch (err) {
                res.end(JSON.stringify({
                    msg: err
                }))
            }
        })
    }
    
  • 相关阅读:
    convirt2.5在虚拟机上安装笔记
    myeclipse 自动提示JS
    Hadoop学习之HBase基础知识、操作和原理
    Hadoop学习之HBase的集群环境搭建
    学习面向对象之异常之三
    java学习面向对象之异常之二
    java学习面向对象之异常之一
    java学习面向对象之匿名内部类
    java学习面向对象之内部类
    java学习面向对象之多态
  • 原文地址:https://www.cnblogs.com/aeipyuan/p/13234515.html
Copyright © 2020-2023  润新知