nodejs實(shí)現(xiàn)(含前端代碼)單文件的上傳朵锣、刪除谬盐、下載

文件的上傳、刪除猪勇、下載基本是每個(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-saversaveAs方法倦逐,它會(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铃辖、遺留問題

  • 超大文件的上傳和下載剩愧,如何友好處理;
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末娇斩,一起剝皮案震驚了整個(gè)濱河市仁卷,隨后出現(xiàn)的幾起案子穴翩,更是在濱河造成了極大的恐慌,老刑警劉巖锦积,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芒帕,死亡現(xiàn)場離奇詭異,居然都是意外死亡丰介,警方通過查閱死者的電腦和手機(jī)背蟆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來哮幢,“玉大人带膀,你說我怎么就攤上這事〕裙福” “怎么了垛叨?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長柜某。 經(jīng)常有香客問我嗽元,道長,這世上最難降的妖魔是什么喂击? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任剂癌,我火速辦了婚禮,結(jié)果婚禮上翰绊,老公的妹妹穿的比我還像新娘佩谷。我一直安慰自己,他們只是感情好辞做,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布琳要。 她就那樣靜靜地躺著,像睡著了一般秤茅。 火紅的嫁衣襯著肌膚如雪稚补。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天框喳,我揣著相機(jī)與錄音课幕,去河邊找鬼。 笑死五垮,一個(gè)胖子當(dāng)著我的面吹牛乍惊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播放仗,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼润绎,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起莉撇,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤呢蛤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后棍郎,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體其障,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年涂佃,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了励翼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辜荠,死狀恐怖汽抚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情伯病,我是刑警寧澤殊橙,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站狱从,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏叠纹。R本人自食惡果不足惜季研,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望誉察。 院中可真熱鬧与涡,春花似錦、人聲如沸持偏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸿秆。三九已至酌畜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卿叽,已是汗流浹背桥胞。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留考婴,地道東北人贩虾。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像沥阱,于是被迫代替她去往敵國和親缎罢。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容