koa2基于stream(流)进行文件上传和下载

时间:2022-02-21 10:41:27

阅读目录

一:上传文件(包括单个文件或多个文件上传)

在之前一篇文章,我们了解到nodejs中的流的概念,也了解到了使用流的优点,具体看我之前那一篇文章介绍的。
现在我们想使用流做一些事情,来实践下它的应用场景及用法。今天我给大家分享的是koa2基于流的方式实现文件上传和下载功能。

首先要实现文件上传或下载肯定是需要使用post请求,以前我们使用 koa-bodyparser这个插件来解析post请求的。但是今天给大家介绍另一个插件 koa-body, 
该插件即可以解析post请求,又支持文件上传功能,具体可以看这篇文章介绍(http://www.ptbird.cn/koa-body.html), 或看官网github(https://github.com/dlau/koa-body).

其次就是koa-body的版本问题,如果旧版本的koa-body通过ctx.request.body.files获取上传的文件。而新版本是通过ctx.request.files获取上传的文件的。否则的话,你会一直报错:ctx.request.files.file ---------->终端提示undefined问题. 如下图所示:

koa2基于stream(流)进行文件上传和下载

我这边的koa-body 是4版本以上的("koa-body": "^4.1.0",), 因此使用 ctx.request.files.file; 来获取文件了。

那么上传文件也有两种方式,第一种方式是使用form表单提交数据,第二种是使用ajax方式提交。那么二种方式的区别我想大家也应该了解,无非就是页面刷不刷新的问题了。下面我会使用这两种方式来上传文件演示下。

1. 上传单个文件

首先先来介绍下我项目的目录结构如下:

|----项目demo
| |--- .babelrc # 解决es6语法问题
| |--- node_modules # 所有依赖的包
| |--- static
| | |--- upload.html # 上传html页面
| | |--- load.html # 下载html页面
| | |--- upload # 上传图片或文件都放在这个文件夹里面
| |--- app.js # 编写node相关的入口文件,比如上传,下载请求
| |--- package.json # 依赖的包文件

如上就是我目前项目的基本架构。如上我会把所有上传的文件或图片会放到 /static/upload 文件夹内了。也就是说把上传成功后的文件存储到我本地文件内。然后上传成功后,我会返回一个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,输出如下所示:

koa2基于stream(流)进行文件上传和下载

2. 我们使用 fs.createReadStream 来读取文件流;如代码:const fileReader = fs.createReadStream(file.path);  我们也可以打印下 fileReader 输出内容如下:

koa2基于stream(流)进行文件上传和下载

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页面返回如下信息了;如下图所示:

koa2基于stream(流)进行文件上传和下载

因此所有的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); 后,可以看到如下信息了;

koa2基于stream(流)进行文件上传和下载

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');
});

然后我现在来演示下,当我选择多个文件,比如现在选择两个文件,会返回如下数据:

koa2基于stream(流)进行文件上传和下载

当我现在只选择一个文件的时候,只会返回一个文件,如下图所示:

koa2基于stream(流)进行文件上传和下载

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

koa2基于stream(流)进行文件上传和下载的更多相关文章

  1. 基于Spring MVC的文件上传和下载功能的实现

    配置文件中配置扫描包,以便创建各个类的bean对象 <context:component-scan base-package="com.neuedu.spring_mvc"& ...

  2. &lbrack;原创&rsqb;java WEB学习笔记49:文件上传基础,基于表单的文件上传&comma;使用fileuoload 组件

    本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...

  3. Servlet3&period;0学习总结——基于Servlet3&period;0的文件上传

    Servlet3.0学习总结(三)——基于Servlet3.0的文件上传 在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileu ...

  4. Struts2文件上传(基于表单的文件上传)

    •Commons-FileUpload组件 –Commons是Apache开放源代码组织的一个Java子项目,其中的FileUpload是用来处理HTTP文件上传的子项目   •Commons-Fil ...

  5. 基于jsp的文件上传和下载

    参考: 一.JavaWeb学习总结(五十)--文件上传和下载 此文极好,不过有几点要注意: 1.直接按照作者的代码极有可能listfile.jsp文件中 <%@taglib prefix=&qu ...

  6. 用c&plus;&plus;开发基于tcp协议的文件上传功能

    用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学 ...

  7. Python&&num;160&semi;基于Python实现Ftp文件上传&comma;下载

    基于Python实现Ftp文件上传,下载   by:授客 QQ:1033553122 测试环境: Ftp客户端:Windows平台 Ftp服务器:Linux平台 Python版本:Python 2.7 ...

  8. 构建基于阿里云OSS文件上传服务

    转载请注明来源:http://blog.csdn.net/loongshawn/article/details/50710132 <构建基于阿里云OSS文件上传服务> <构建基于OS ...

  9. 基于hap的文件上传和下载

    序言 现在,绝大部分的应用程序在很多的情况下都需要使用到文件上传与下载的功能,在本文中结合hap利用spirng mvc实现文件的上传和下载,包括上传下载图片.上传下载文档.前端所使用的技术不限,本文 ...

随机推荐

  1. Objective-C 排序

    在Objective-C中,排序分为: 1.Foundation框架中的对象排序 2.自定义对象排序 例子:每个学生都有一个成绩score属性,根据成绩score对学生排序 自定义对象 Student ...

  2. 无需FQ,自建本地CDN,秒上*!

    *是一个面向程序员的技术问答平台.可是在不FQ的情况下,浏览*是一件让人极不舒服的事情,常常需要等待数十秒页面才慢慢显示出来.本文我教大家一种能够流畅地 ...

  3. 小游戏Item表

    [Config]1|0|我|1|10|500|0|8|2|4|b5222d10-55a7-4789-8541-8e7430345d54|0|0[Config] [Config]2|1|公主|1|0|5 ...

  4. Excel替换应用

    * 是通配符 ,代表任意字符 ?也是通配符, 代表单个字符替换回车 : 按住alt键不放,然后依次通过数字键盘输入1和0两个数字,放开数字键后再放开alt键.

  5. Android入门&lpar;五&rpar;UI-单位与尺寸、ListView

    原文链接:http://www.orlion.ga/453/ 一.单位与尺寸 布局文件中一共有以下单位供选择:px,pt,dp,sp px:是像素,屏幕中可见的最小元素单位. pt:是磅,1磅等于1/ ...

  6. The reference to entity &OpenCurlyDoubleQuote;idNo” must end with the &&num;39&semi;&semi;&&num;39&semi; delimiter 异常处理

    解决方案很简单,就是把配置项值中用到"&"的地方改成"&".原因是sax解析的类库在读取文件的时候是根据转义后的格式进行读取的,遇到" ...

  7. OC6-网址分割

    // // HtmlManger.h // OC6-网址分割 // // Created by qianfeng on 15/6/17. // Copyright (c) 2015年 qianfeng ...

  8. 再谈Cookies欺骗

    在上一篇关于cookies欺骗的随笔中,提到的解决方案是把密码MD5加密之后存入cookies中,确实这种方法实现了效果,不过把密码留在客户端等待着去被破解不是一个合适的方法,在此也感谢 @老牛吃肉 ...

  9. BZOJ3790神奇项链——manacher&plus;贪心

    题目描述 母亲节就要到了,小 H 准备送给她一个特殊的项链.这个项链可以看作一个用小写字 母组成的字符串,每个小写字母表示一种颜色.为了制作这个项链,小 H 购买了两个机器.第一个机器可以生成所有形式 ...

  10. Spring&plus;Quartz实现定时任务的配置方法(转)

    1.Scheduler的配置 <bean id="myScheduler" class="org.springframework.scheduling.quartz ...

相关文章