基于Vue的Upload组件实现

    技术2025-05-27  16

    Upload组件基本实现

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

    前端

    1. 组件结构

    <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="`width:${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事件

    根据父组件传入的规则对选中的文件进行过滤遍历过滤后的数组,生成监听的数组(直接监听原数组浪费性能)设置属性监听各种操作将请求函数存入队列,延迟执行调用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 })) } }) }
    Processed: 0.011, SQL: 9