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上的源码

相关文章
相关标签/搜索