文件的上傳、刪除猪勇、下載基本是每個(gè)項(xiàng)目都有的功能设褐,免不了和后端小伙伴的對接。本文從前端的角度泣刹,實(shí)現(xiàn)簡易的demo助析,幫助梳理這些功能前后端的交互流程。
1椅您、頁面功能描述
- 上傳:只能單個(gè)文件上傳外冀,上傳成功后,刷新文件列表掀泳。一個(gè)文件可以重復(fù)上傳多次雪隧,但是內(nèi)存中只存一份(以文件名唯一來判斷西轩,不考慮其它復(fù)雜情況,可以考慮存文件hash)脑沿;
- 刪除:先刪除在數(shù)據(jù)庫中文件的上傳記錄藕畔,若該文件記錄已經(jīng)已經(jīng)為0,那么從內(nèi)存中刪除該文件庄拇;
- 下載:下載指定文件名的文件(get請求注服,利用a標(biāo)簽download屬性);
2措近、前端部分
前端使用vue
溶弟、file-saver
,請?zhí)崆皀pm install
2.1 前端整體代碼(可能缺失部分css樣式)
<template>
<div class="container">
<span class="loading" v-show="isLoading"></span>
<p>
<input type="file" id="file" @change="changeFile">
<button type="button" @click="uploadFile">上傳文件</button>
</p>
<table>
<thead>
<tr>
<th>序號(hào)</th>
<th>上傳時(shí)間</th>
<th>文件名</th>
<th>文件大小</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<template v-if="fileList.length">
<tr v-for="(item, index) in fileList">
<td>{{index + 1}}</td>
<td>{{item.dateTime}}</td>
<td>{{item.fileName}}</td>
<td>{{item.size}}</td>
<td>
<button type="button" @click="downloadFile(item.fileName)">下載</button>
<button type="button" @click="deleteFile(item)" style="background-color: #e52050; margin-left: 10px;">刪除</button>
</td>
</tr>
</template>
<template v-else>
<tr>
<td colspan="5" class="tc">無數(shù)據(jù)</td>
</tr>
</template>
</tbody>
</table>
</div>
</template>
<script>
// 通過blob下都會(huì)先獲取文件瞭郑,然后生成文件辜御,再下載 沒有直接下載方式友好(直接下載會(huì)馬上出來瀏覽器下載進(jìn)度)。
// 在相同來源內(nèi)使用URL只會(huì)使用a[download]
import {saveAs} from 'file-saver';
export default {
name: 'FileList',
data() {
return {
isLoading: true,
fileName: '',
fileList: []
}
},
methods: {
getFileList() {
this.isLoading = true;
// 設(shè)置了es的刷新間隔為1s 所以要用定時(shí)器屈张。(后面看后端處理方式)
setTimeout(() => {
fetch('/api/fileList', {
method: 'get'
}).then(res => res.json())
.then(data => {
this.fileList = data.status === 200 ? data.data : [];
this.isLoading = false;
})
.catch(error => {
this.isLoading = false;
console.error('Error:', error);
});
}, 1000);
},
deleteFile(item) {
fetch(`/api/file?fileName=${item.fileName}&id=${item.id}`, {
method: 'delete'
}).then(res => res.json())
.then(data => {
data.status === 200 && this.getFileList();
})
.catch(error => console.error('Error:', error));
},
uploadFile() {
if (!this.fileName) {
window.alert('請選擇需要上傳的文件');
return 0;
}
let formData = new FormData();
let fileField = document.getElementById('file');
formData.append('fileName', this.fileName);
formData.append('file', fileField.files[0]);
fetch('/api/file', {
method: 'post',
body: formData
}).then(res => res.json())
.then(data => {
if (data.status === 200) {
fileField.value = '';
this.fileName = '';
this.getFileList();
}
})
.catch(error => console.error('Error:', error));
},
// 這種方式是把文件流讀取到瀏覽器內(nèi)存中
downloadFileByFileSaver(url, method) {
let fileName = url.split('fileName=')[1];
fetch(url, {
method
}).then(res => res.blob())
.then(data => {
let blob = new Blob([data], {
type: 'application/octet-stream'
});
saveAs(blob, fileName);
})
.catch(error => console.error('Error:', error));
},
downloadFileByIframe(url) {
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 100);
},
downloadFileByAtagClick(url, fileName = '') {
let a = document.createElement('a');
a.style.display = 'none';
a.href = url;
document.body.appendChild(a);
fileName && a.setAttribute('download', fileName);
a.click();
document.body.removeChild(a);
// 方法二: saveAs(url);
},
// 這種方式是把文件流讀取到瀏覽器內(nèi)存中
downloadFileByBlob(url, method = 'get') {
// 使用這種方式多了創(chuàng)建請求的時(shí)間
let fileName = url.split('fileName=')[1];
fetch(url, {
method
}).then(res => {
console.log(res.headers.get('content-length'));
return res.blob();
}).then(data => {
let blob = new Blob([data], {
type: 'application/octet-stream'
});
// url表示指定的 File 對象或 Blob 對象擒权。
let url = URL.createObjectURL(blob);
this.downloadFileByAtagClick(url, fileName);
// 釋放URL對象
URL.revokeObjectURL(url);
})
.catch(error => console.error('Error:', error));
},
downloadFile(fileName) {
// this.downloadFileByIframe(`/api/file?fileName=${fileName}`);
this.downloadFileByAtagClick(`/api/file?fileName=${fileName}`);
// this.downloadFileByBlob(`/api/file?fileName=test3.zip`, 'get');
// this.downloadFileByFileSaver(`/api/file?fileName=${fileName}`, 'get');
},
changeFile(e) {
this.fileName = e.target.files.length ? e.target.files[0].name : '';
}
},
mounted() {
this.getFileList();
}
}
</script>
<style scoped>
.loading {
position: fixed;
left: 50%;
top: 50%;
width: 150px;
height: 100px;
background: url("../assets/loading.gif");
background-size: cover;
transform: translateX(-50%) translateY(-50%);
}
th {
padding: 10px;
background-color: #60bb63;
color: white;
font-size: 15px;
}
td {
padding: 6px 10px;
}
</style>
2.2 前端文件下載的幾種方式
前端文件下載主要有2種方式:
- 利用瀏覽器提供的get請求,來直接下載文件袜茧,前端常見處理方式為:
a [href][download]
菜拓、iframe [src]
;
選這種方式要看后端下載的實(shí)現(xiàn)方式笛厦,是否能讓前端直接下載纳鼎。這種方式比好的是下載是同步的,點(diǎn)擊下載按鈕后裳凸,能直接看到瀏覽器的反應(yīng)贱鄙。 - 利用blob,獲取后端文件流姨谷,再生成URL逗宁,利用
a [href][download]
來下載文件(這種方式通用度比較高);
這種方式通用度比較高梦湘,不好的是前端要先把后端返回的文件流全部獲取瞎颗,才能進(jìn)行下載。在獲取文件流這段時(shí)間是前端ajax異步加載過程捌议。
這2種方式都是把文件流存在瀏覽器內(nèi)存中哼拔,比較依賴于客戶端硬件配置。手機(jī)端下載大文件一般會(huì)直接寫到硬盤(手機(jī)存儲(chǔ))中瓣颅。
2.2.1 直接下載-利用a標(biāo)簽
downloadFileByAtagClick(url, fileName = '') {
let a = document.createElement('a');
a.style.display = 'none';
a.href = url;
document.body.appendChild(a);
fileName && a.setAttribute('download', fileName);
a.click();
document.body.removeChild(a);
// 方法二: saveAs(url);
},
這個(gè)可以直接利用file-saver的saveAs
方法倦逐,它會(huì)根據(jù)傳入的參數(shù),來選擇使用blob
還是a [href]
2.2.2 直接下載-利用iframe標(biāo)簽
downloadFileByIframe(url) {
let iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(() => document.body.removeChild(iframe), 100);
},
iframe [src]
標(biāo)簽跟a [href]
標(biāo)簽原理差不多宫补。
2.2.3 blob-原生js
downloadFileByBlob(url, method = 'get') {
// 使用這種方式多了創(chuàng)建請求的時(shí)間
let fileName = url.split('fileName=')[1];
fetch(url, {
method
}).then(res => {
console.log(res.headers.get('content-length'));
return res.blob();
}).then(data => {
let blob = new Blob([data], {
type: 'application/octet-stream'
});
// url表示指定的 File 對象或 Blob 對象檬姥。
let url = URL.createObjectURL(blob);
this.downloadFileByAtagClick(url, fileName);
// 釋放URL對象
URL.revokeObjectURL(url);
})
.catch(error => console.error('Error:', error));
},
2.2.3 blob-"file-saver"saveAs方法
downloadFileByFileSaver(url, method) {
let fileName = url.split('fileName=')[1];
fetch(url, {
method
}).then(res => res.blob())
.then(data => {
let blob = new Blob([data], {
type: 'application/octet-stream'
});
saveAs(blob, fileName);
})
.catch(error => console.error('Error:', error));
},
3曾我、后端部分
后端主要依賴庫:fs、express健民、multer抒巢、elasticsearch
,除了fs
秉犹,其它模塊請?zhí)崆皀pm install虐秦。
3.1 后端代碼-web交互
/**用來實(shí)現(xiàn)文件上傳、下載凤优、刪除功能**/
let fs = require('fs');
let express = require('express');
let multer = require('multer');
let esUtil = require('./esUtil');
let app = express();
let storage = multer.diskStorage({
destination: 'file/',
filename: function (req, file, cb) {
cb(null, file.originalname);
}
});
let upload = multer({ storage: storage });
esUtil.initEsUtil({ host: 'localhost:9200' });
app.listen(3100);
// 單文件上傳,file指代上傳時(shí)候的文件屬性名
app.post('/file', upload.single('file'), async (req, res, next) => {
let file = req.file;
let fileInfo = {};
// 獲取文件信息
fileInfo.mimetype = file.mimetype;
fileInfo.fileName = file.originalname;
fileInfo.size = file.size;
fileInfo.path = file.path;
fileInfo.dateTime = new Date();
// 存入es
let result = await esUtil.post('files', 'file', fileInfo);
res.send(result);
});
// 文件下載
app.get('/file', (req, res, next) => {
let fileName = req.query.fileName;
let path = './file/' + fileName;
let size = fs.statSync(path).size; // 文件大小
res.writeHead(200, {
// 告訴瀏覽器文件是二進(jìn)制文件蜈彼,不想直接顯示內(nèi)容
'Content-type': 'application/octet-stream',
// 告訴瀏覽器這是一個(gè)需要下載的文件(以附件的形式下載)筑辨,設(shè)置下載文件名
'Content-Disposition': 'attachment; filename=' + encodeURI(fileName),
'Content-Length': size
});
let readStream = fs.createReadStream(path); // 得到輸入文件流
readStream.pipe(res);
});
// 文件刪除
app.delete('/file', async (req, res, next) => {
let fileName = req.query.fileName;
let results = await esUtil.get('files', 'file', {
term: {
'fileName.keyword': {
value: fileName
}
}
});
// 刪除指定數(shù)據(jù)
let result = await esUtil.delete('files', 'file', req.query.id);
if (results.data.length === 1 && result.status === 200) fs.unlinkSync('./file/' + fileName);
res.send(result);
});
// 獲取文件列表
app.get('/fileList', async (req, res, next) => {
let results = await esUtil.getAll('files', 'file');
res.send(results);
});
app.use((req, res, next, err) => {
res.send({status: 500});
});
3.1.1 文件上傳說明
multer實(shí)現(xiàn)文件的上傳依賴庫,可配置文件上傳路徑幸逆、單個(gè)文件上傳和多個(gè)文件上傳等棍辕。本文配置的單文件上傳,上傳目錄是當(dāng)前執(zhí)行代碼相對路徑file/
还绘。
3.1.2 文件下載說明
后端返回響應(yīng)頭需要添加:
- 'Content-type': 'application/octet-stream'楚昭,告訴瀏覽器文件是二進(jìn)制文件,不想直接顯示內(nèi)容拍顷。
- 'Content-Disposition': 'attachment; filename=' + encodeURI(fileName)抚太,告訴瀏覽器這是一個(gè)需要下載的文件(以附件的形式下載),并設(shè)置下載文件名昔案。
- 'Content-Length': size尿贫,設(shè)置文件大小,這樣前端在直接下載文件的時(shí)候會(huì)顯示文件大小踏揣,對用戶交互界面也更加友好庆亡。
3.2 后端代碼-es操作
let elasticsearch = require('elasticsearch');
let client;
const ERROR_STATUS = 500;
const SUCCESS_STATUS = 200;
exports.initEsUtil = (config) => {
client = elasticsearch.Client(config);
return client;
};
// 插入一條數(shù)據(jù)
exports.post = (index, type, data) => {
return new Promise((resolve, reject) => {
client.index({
index,
type,
body: data
}, (err, res) => {
err ? reject({status: ERROR_STATUS}) : resolve({status: SUCCESS_STATUS});
});
});
};
// 獲取所有數(shù)據(jù)
exports.getAll = async (index, type) => {
let res = await client.search({
index,
type,
body: {
sort: [
{
dateTime: {
order: 'desc'
}
}
],
}
});
let list = [];
if (res instanceof Error) return {status: ERROR_STATUS, data: list};
if (res && res.hits && res.hits.hits) {
let arr = res.hits.hits;
for (let item of arr) {
list.push(Object.assign({id: item._id}, item._source));
}
}
return {status: SUCCESS_STATUS, data: list};
};
// 查詢指定數(shù)據(jù)
exports.get = (index, type, param) => {
return new Promise((resolve, reject) => {
client.search({
index,
type,
body: {
query: param
}
}, (err, res) => {
let list = [];
if (res && res.hits && res.hits.hits) {
let arr = res.hits.hits;
for (let item of arr) {
list.push(Object.assign({id: item._id}, item._source));
}
}
err ? reject({status: ERROR_STATUS, data: []}) : resolve({status: SUCCESS_STATUS, data: list});
});
});
};
// 刪除指定數(shù)據(jù)
exports.delete = (index, type, id) => {
return new Promise((resolve, reject) => {
client.delete({
index,
type,
id
}, (err, res) => {
err ? reject({status: ERROR_STATUS}) : resolve({status: SUCCESS_STATUS});
});
});
};
3.2.1 es操作說明
es操作還是新手級(jí)別,只會(huì)用很簡單的操作捞稿。不過最好把es相關(guān)操作和web交互代碼分開又谋,看著舒服一點(diǎn)。代碼種用了async awite promise
相關(guān)寫法娱局,代碼看起來直觀一點(diǎn)彰亥,也避免回調(diào)地獄。
4铃辖、遺留問題
- 超大文件的上傳和下載剩愧,如何友好處理;