基於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="`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事件

  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
            }))
        }
    })
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章