koa2基於stream(流)進行文件上傳和下載

閱讀目錄javascript

一:上傳文件(包括單個文件或多個文件上傳)html

在以前一篇文章,咱們瞭解到nodejs中的流的概念,也瞭解到了使用流的優勢,具體看我以前那一篇文章介紹的。
如今咱們想使用流作一些事情,來實踐下它的應用場景及用法。今天我給你們分享的是koa2基於流的方式實現文件上傳和下載功能。java

首先要實現文件上傳或下載確定是須要使用post請求,之前咱們使用 koa-bodyparser這個插件來解析post請求的。可是今天給你們介紹另外一個插件 koa-body, 
該插件便可以解析post請求,又支持文件上傳功能,具體能夠看這篇文章介紹(http://www.ptbird.cn/koa-body.html), 或看官網github(https://github.com/dlau/koa-body).node

其次就是koa-body的版本問題,若是舊版本的koa-body經過ctx.request.body.files獲取上傳的文件。而新版本是經過ctx.request.files獲取上傳的文件的。不然的話,你會一直報錯:ctx.request.files.file ---------->終端提示undefined問題. 以下圖所示:ios

我這邊的koa-body 是4版本以上的("koa-body": "^4.1.0",), 所以使用 ctx.request.files.file; 來獲取文件了。git

那麼上傳文件也有兩種方式,第一種方式是使用form表單提交數據,第二種是使用ajax方式提交。那麼二種方式的區別我想你們也應該瞭解,無非就是頁面刷不刷新的問題了。下面我會使用這兩種方式來上傳文件演示下。es6

1. 上傳單個文件github

首先先來介紹下我項目的目錄結構以下:ajax

|----項目demo
|  |--- .babelrc       # 解決es6語法問題
|  |--- node_modules   # 全部依賴的包
|  |--- static
|  | |--- upload.html  # 上傳html頁面
|  | |--- load.html    # 下載html頁面
|  | |--- upload       # 上傳圖片或文件都放在這個文件夾裏面
|  |--- app.js         # 編寫node相關的入口文件,好比上傳,下載請求
|  |--- package.json   # 依賴的包文件

如上就是我目前項目的基本架構。如上我會把全部上傳的文件或圖片會放到 /static/upload 文件夾內了。也就是說把上傳成功後的文件存儲到我本地文件內。而後上傳成功後,我會返回一個json數據。json

在項目中,我用到了以下幾個插件:koa, fs, path, koa-router, koa-body, koa-static. 如上幾個插件咱們並不陌生哦。下面咱們分別引用進來,以下代碼:

const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const koaBody = require('koa-body');
const static = require('koa-static');

const app = new Koa();

/* 
  koa-body 對應的API及使用 看這篇文章 http://www.ptbird.cn/koa-body.html
  或者看 github上的官網 https://github.com/dlau/koa-body
*/
app.use(koaBody({
  multipart: true, // 支持文件上傳
  formidable: {
    maxFieldsSize: 2 * 1024 * 1024, // 最大文件爲2兆
    multipart: true // 是否支持 multipart-formdate 的表單
  }
}));

app.use(static(path.join(__dirname)));

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is listen in 3001');
});

如上代碼就是我app.js 基本架構,使用koa來監聽服務,端口號是3001,而後使用koa-router來作路由頁面指向。使用koa-body插件來解析post請求,及支持上傳文件的功能。使用 koa-static插件來解析靜態目錄資源。使用fs來使用流的功能,好比 fs.createWriteStream 寫文件 或 fs.createReadStream 讀文件功能。使用path插件來解析目錄問題,好比 path.join(__dirname) 這樣的。

咱們但願當咱們 當咱們訪問 http://localhost:3001/ 的時候,但願頁面指向 咱們的 upload.html頁面,所以app.js請求代碼能夠寫成以下:

router.get('/', (ctx) => {
  // 設置頭類型, 若是不設置,會直接下載該頁面
  ctx.type = 'html';
  // 讀取文件
  const pathUrl = path.join(__dirname, '/static/upload.html');
  ctx.body = fs.createReadStream(pathUrl);
});

注意:如上 ctx.type = 'html', 必定要設置下,不然打開該頁面直接會下載該頁面的了。而後咱們使用fs.createReadStream來讀取咱們的頁面後,把該頁面指向 ctx.body 了,所以當咱們訪問 http://localhost:3001/ 的時候 就指向了 咱們項目中的 static/upload.html 了。

下面咱們來看下咱們項目下的 /static/upload.html 頁面代碼以下:

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>文件上傳</title>
</head>
<body>
  <form action="http://localhost:3001/upload" method="post" enctype="multipart/form-data">
    <div>
      <input type="file" name="file">
    </div>
    <div>
      <input type="submit" value="提交"/>
    </div>
  </form>
</body>
</html>

如上upload.html頁面,就是一個form表單頁面,而後一個上傳文件按鈕,上傳後,咱們點擊提交,便可調用form表單中的action動做調用http://localhost:3001/upload這個接口,所以如今咱們來看下app.js中 '/upload' 代碼以下:

const uploadUrl = "http://localhost:3001/static/upload";
// 上傳文件
router.post('/upload', (ctx) => {

  const file = ctx.request.files.file;
  // 讀取文件流
  const fileReader = fs.createReadStream(file.path);

  const filePath = path.join(__dirname, '/static/upload/');
  // 組裝成絕對路徑
  const fileResource = filePath + `/${file.name}`;

  /*
   使用 createWriteStream 寫入數據,而後使用管道流pipe拼接
  */
  const writeStream = fs.createWriteStream(fileResource);
  // 判斷 /static/upload 文件夾是否存在,若是不在的話就建立一個
  if (!fs.existsSync(filePath)) {
    fs.mkdir(filePath, (err) => {
      if (err) {
        throw new Error(err);
      } else {
        fileReader.pipe(writeStream);
        ctx.body = {
          url: uploadUrl + `/${file.name}`,
          code: 0,
          message: '上傳成功'
        };
      }
    });
  } else {
    fileReader.pipe(writeStream);
    ctx.body = {
      url: uploadUrl + `/${file.name}`,
      code: 0,
      message: '上傳成功'
    };
  }
});

如上代碼 '/post' 請求最主要作了如下幾件事:
1. 獲取上傳文件,使用 const file = ctx.request.files.file; 咱們來打印下該file,輸出以下所示:

2. 咱們使用 fs.createReadStream 來讀取文件流;如代碼:const fileReader = fs.createReadStream(file.path);  咱們也能夠打印下 fileReader 輸出內容以下:

3. 對當前上傳的文件保存到 /static/upload 目錄下,所以定義變量:const filePath = path.join(__dirname, '/static/upload/');

4. 組裝文件的絕對路徑,代碼:const fileResource = filePath + `/${file.name}`;

5. 使用 fs.createWriteStream 把該文件寫進去,如代碼:const writeStream = fs.createWriteStream(fileResource);

6. 下面這段代碼就是判斷是否有該目錄,若是沒有改目錄,就建立一個 /static/upload 這個目錄,若是有就直接使用管道流pipe拼接文件,如代碼:fileReader.pipe(writeStream);

if (!fs.existsSync(filePath)) {
  fs.mkdir(filePath, (err) => {
    if (err) {
      throw new Error(err);
    } else {
      fileReader.pipe(writeStream);
      ctx.body = {
        url: uploadUrl + `/${file.name}`,
        code: 0,
        message: '上傳成功'
      };
    }
  });
} else {
  fileReader.pipe(writeStream);
  ctx.body = {
    url: uploadUrl + `/${file.name}`,
    code: 0,
    message: '上傳成功'
  };
}

最後咱們使用 ctx.body 返回到頁面來,所以若是咱們上傳成功了,就會在upload頁面返回以下信息了;以下圖所示:

所以全部的app.js 代碼以下:

const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const koaBody = require('koa-body');
const static = require('koa-static');

const app = new Koa();

/* 
  koa-body 對應的API及使用 看這篇文章 http://www.ptbird.cn/koa-body.html
  或者看 github上的官網 https://github.com/dlau/koa-body
*/
app.use(koaBody({
  multipart: true, // 支持文件上傳
  formidable: {
    maxFieldsSize: 2 * 1024 * 1024, // 最大文件爲2兆
    multipart: true // 是否支持 multipart-formdate 的表單
  }
}));

const uploadUrl = "http://localhost:3001/static/upload";

router.get('/', (ctx) => {
  // 設置頭類型, 若是不設置,會直接下載該頁面
  ctx.type = 'html';
  // 讀取文件
  const pathUrl = path.join(__dirname, '/static/upload.html');
  ctx.body = fs.createReadStream(pathUrl);
});

// 上傳文件
router.post('/upload', (ctx) => {

  const file = ctx.request.files.file;
  console.log(file);
  // 讀取文件流
  const fileReader = fs.createReadStream(file.path);
  console.log(fileReader);
  const filePath = path.join(__dirname, '/static/upload/');
  // 組裝成絕對路徑
  const fileResource = filePath + `/${file.name}`;

  /*
   使用 createWriteStream 寫入數據,而後使用管道流pipe拼接
  */
  const writeStream = fs.createWriteStream(fileResource);
  // 判斷 /static/upload 文件夾是否存在,若是不在的話就建立一個
  if (!fs.existsSync(filePath)) {
    fs.mkdir(filePath, (err) => {
      if (err) {
        throw new Error(err);
      } else {
        fileReader.pipe(writeStream);
        ctx.body = {
          url: uploadUrl + `/${file.name}`,
          code: 0,
          message: '上傳成功'
        };
      }
    });
  } else {
    fileReader.pipe(writeStream);
    ctx.body = {
      url: uploadUrl + `/${file.name}`,
      code: 0,
      message: '上傳成功'
    };
  }
});

app.use(static(path.join(__dirname)));

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is listen in 3001');
});

如上是使用 form表單提交的,咱們也可使用 ajax來提交,那麼須要改下 upload.html代碼了。

2. 使用ajax方法提交。

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>文件上傳</title>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
  <!-- 使用form表單提交
  <form action="http://localhost:3001/upload" method="post" enctype="multipart/form-data">
    <div>
      <input type="file" name="file">
    </div>
    <div>
      <input type="submit" value="提交"/>
    </div>
  </form>
  -->
  <div>
    <input type="file" name="file" id="file">
  </div>
  <script type="text/javascript">
    var file = document.getElementById('file');
    const instance = axios.create({
      withCredentials: true
    });
    file.onchange = function(e) {
      var f1 = e.target.files[0];
      var fdata = new FormData();
      fdata.append('file', f1);
      instance.post('http://localhost:3001/upload', fdata).then(res => {
        console.log(res);
      }).catch(err => {
        console.log(err);
      });
    }
  </script>
</body>
</html>

如上咱們打印 console.log(res); 後,能夠看到以下信息了;

3. 上傳多個文件

爲了支持多個文件上傳,和單個文件上傳,咱們須要把代碼改下,改爲以下:

html代碼以下:

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>文件上傳</title>
  <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
  <!-- 使用form表單提交
  <form action="http://localhost:3001/upload" method="post" enctype="multipart/form-data">
    <div>
      <input type="file" name="file">
    </div>
    <div>
      <input type="submit" value="提交"/>
    </div>
  </form>
  -->
  <!--  上傳單個文件
  <div>
    <input type="file" name="file" id="file">
  </div>
  <script type="text/javascript">
    var file = document.getElementById('file');
    const instance = axios.create({
      withCredentials: true
    });
    file.onchange = function(e) {
      var f1 = e.target.files[0];
      var fdata = new FormData();
      fdata.append('file', f1);
      instance.post('http://localhost:3001/upload', fdata).then(res => {
        console.log(res);
      }).catch(err => {
        console.log(err);
      });
    }
  </script>
  -->
  <div>
    <input type="file" name="file" id="file" multiple="multiple">
  </div>
  <script type="text/javascript">
    var file = document.getElementById('file');
    const instance = axios.create({
      withCredentials: true
    });
    file.onchange = function(e) {
      var files = e.target.files;
      var fdata = new FormData();
      if (files.length > 0) {
        for (let i = 0; i < files.length; i++) {
          const f1 = files[i];
          fdata.append('file', f1);
        }
      }
      instance.post('http://localhost:3001/upload', fdata).then(res => {
        console.log(res);
      }).catch(err => {
        console.log(err);
      });
    }
  </script>
</body>
</html>

如上是多個文件上傳的html代碼和js代碼,就是把多個數據使用formdata一次性傳遞多個數據過去,如今咱們須要把app.js 代碼改爲以下了,app.js 代碼改的有點多,最主要是要判斷 傳過來的文件是單個的仍是多個的邏輯,全部代碼以下:

const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const koaBody = require('koa-body');
const static = require('koa-static');

const app = new Koa();

/* 
  koa-body 對應的API及使用 看這篇文章 http://www.ptbird.cn/koa-body.html
  或者看 github上的官網 https://github.com/dlau/koa-body
*/
app.use(koaBody({
  multipart: true, // 支持文件上傳
  formidable: {
    maxFieldsSize: 2 * 1024 * 1024, // 最大文件爲2兆
    multipart: true // 是否支持 multipart-formdate 的表單
  }
}));

const uploadUrl = "http://localhost:3001/static/upload";

router.get('/', (ctx) => {
  // 設置頭類型, 若是不設置,會直接下載該頁面
  ctx.type = 'html';
  // 讀取文件
  const pathUrl = path.join(__dirname, '/static/upload.html');
  ctx.body = fs.createReadStream(pathUrl);
});
/*
 flag: 是不是多個文件上傳
*/
const uploadFilePublic = function(ctx, files, flag) {
  const filePath = path.join(__dirname, '/static/upload/');
  let file,
    fileReader,
    fileResource,
    writeStream;

  const fileFunc = function(file) {
    // 讀取文件流
    fileReader = fs.createReadStream(file.path);
    // 組裝成絕對路徑
    fileResource = filePath + `/${file.name}`;
    /*
     使用 createWriteStream 寫入數據,而後使用管道流pipe拼接
    */
    writeStream = fs.createWriteStream(fileResource);
    fileReader.pipe(writeStream);
  };
  const returnFunc = function(flag) {
    console.log(flag);
    console.log(files);
    if (flag) {
      let url = '';
      for (let i = 0; i < files.length; i++) {
        url += uploadUrl + `/${files[i].name},`
      }
      url = url.replace(/,$/gi, "");
      ctx.body = {
        url: url,
        code: 0,
        message: '上傳成功'
      };
    } else {
      ctx.body = {
        url: uploadUrl + `/${files.name}`,
        code: 0,
        message: '上傳成功'
      };
    }
  };
  if (flag) {
    // 多個文件上傳
    for (let i = 0; i < files.length; i++) {
      const f1 = files[i];
      fileFunc(f1);
    }
  } else {
    fileFunc(files);
  }
  
  // 判斷 /static/upload 文件夾是否存在,若是不在的話就建立一個
  if (!fs.existsSync(filePath)) {
    fs.mkdir(filePath, (err) => {
      if (err) {
        throw new Error(err);
      } else {
        returnFunc(flag);
      }
    });
  } else {
    returnFunc(flag);
  }
}

// 上傳單個或多個文件
router.post('/upload', (ctx) => {
  let files = ctx.request.files.file;
  const fileArrs = [];
  if (files.length === undefined) {
    // 上傳單個文件,它不是數組,只是單個的對象
    uploadFilePublic(ctx, files, false);
  } else {
     uploadFilePublic(ctx, files, true);
  }
});

app.use(static(path.join(__dirname)));

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is listen in 3001');
});

而後我如今來演示下,當我選擇多個文件,好比如今選擇兩個文件,會返回以下數據:

當我如今只選擇一個文件的時候,只會返回一個文件,以下圖所示:

如上app.js改爲以後的代碼如今支持單個或多個文件上傳了。

注意:這邊只是演示下多個文件上傳的demo,可是在項目開發中,我不建議你們這樣使用,而是多張圖片多個請求比較好,由於大小有限制的,好比a.png 和 b.png 這兩張圖片,若是a圖片比較小,b圖片很大很大,那麼若是兩張圖片一塊兒上傳的話,接口確定會上傳失敗,可是若是把請求分開發,那麼a圖片會上傳成功的,b圖片是上傳失敗的。這樣比較好。

固然咱們在上傳以前咱們還能夠對文件進行壓縮下或者對文件的上傳進度實時顯示下優化下均可以,可是目前我這邊先不作了,下次再把全部的都弄下。這裏只是演示下 fs.createReadStream 流的一些使用方式。

二:下載文件

文件下載須要使用到koa-send這個插件,該插件是一個靜態文件服務的中間件,它能夠用來實現文件下載的功能。

html代碼以下:

<!DOCTYPE html>
<html>
<head>
  <meta charset=utf-8>
  <title>文件下載演示</title>
</head>
<body>
  
  <div>
    <button onclick="fileLoad()">文件下載</button>
    <iframe name="iframeId" style="display:none"></iframe>
  </div>
  <script type="text/javascript">
    function fileLoad() {
      window.open('/fileload/Q4彙總.xlsx', 'iframeId');
    }
  </script>
</body>
</html>

app.js 全部的代碼改爲以下:

const Koa = require('koa');
const fs = require('fs');
const path = require('path');
const router = require('koa-router')();
const koaBody = require('koa-body');
const static = require('koa-static');
const send = require('koa-send');
const app = new Koa();
app.use(koaBody());

router.get('/', (ctx) => {
  // 設置頭類型, 若是不設置,會直接下載該頁面
  ctx.type = 'html';
  // 讀取文件
  const pathUrl = path.join(__dirname, '/static/load.html');
  ctx.body = fs.createReadStream(pathUrl);
});

router.get('/fileload/:name', async (ctx) => {
  const name = ctx.params.name;
  const path = `static/upload/${name}`;
  ctx.attachment(path);
  await send(ctx, path);
});

app.use(static(path.join(__dirname)));
app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3001, () => {
  console.log('server is listen in 3001');
});

如上代碼就能夠了,當我頁面訪問 http://localhost:3001/ 這個的時候,會顯示我項目下的 load.html頁面,該頁面有一個下載文件的按鈕,當我點擊該按鈕的時候,就會下載我本地上某一個文件。好比上面的代碼,咱們使用了window.open. 跳轉指定到了某個隱藏的iframe,若是咱們使用window.open(url), 後面不指定任何參數的話,它會以 '_blank' 的方式打開,最後會致使頁面會刷新下,而後下載,對於用戶體驗來講很差,隱藏咱們就讓他在iframe裏面下載,所以頁面看不到跳動的感受了。
固然若是咱們使用window.open(url, '_self') 也是能夠的,可是貌似有小問題,好比可能會觸發 beforeunload 等頁面事件,若是你的頁面監聽了該事件作一些操做的話,那就會有影響的。 因此咱們使用隱藏的iframe去作這件事。

注意:上面的window.open('/fileload/Q4彙總.xlsx'); 中的 Q4彙總.xlsx 是我本地項目中剛剛上傳的文件。也就是說該文件在我本地上有這個的文件的就能夠下載的。若是我本地項目中沒有該文件就下載不了的。

注意:固然批量文件下載也是能夠作的,這裏就不折騰了。有空本身研究下,或者百度下都有相似的文章,本身折騰下便可。這篇文章最主要想使用 fs.createReadStream 的使用場景。

查看github上的源碼

相關文章
相關標籤/搜索