koa2基于stream(流)进行文件上传和下载
阅读目录
一:上传文件(包括单个文件或多个文件上传)
在之前一篇文章,我们了解到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问题. 如下图所示:
我这边的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,输出如下所示:
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 的使用场景。
koa2基于stream(流)进行文件上传和下载的更多相关文章
- 基于Spring MVC的文件上传和下载功能的实现
配置文件中配置扫描包,以便创建各个类的bean对象 <context:component-scan base-package="com.neuedu.spring_mvc"& ...
- [原创]java WEB学习笔记49:文件上传基础,基于表单的文件上传,使用fileuoload 组件
本博客为原创:综合 尚硅谷(http://www.atguigu.com)的系统教程(深表感谢)和 网络上的现有资源(博客,文档,图书等),资源的出处我会标明 本博客的目的:①总结自己的学习过程,相当 ...
- Servlet3.0学习总结——基于Servlet3.0的文件上传
Servlet3.0学习总结(三)——基于Servlet3.0的文件上传 在Servlet2.5中,我们要实现文件上传功能时,一般都需要借助第三方开源组件,例如Apache的commons-fileu ...
- Struts2文件上传(基于表单的文件上传)
•Commons-FileUpload组件 –Commons是Apache开放源代码组织的一个Java子项目,其中的FileUpload是用来处理HTTP文件上传的子项目 •Commons-Fil ...
- 基于jsp的文件上传和下载
参考: 一.JavaWeb学习总结(五十)--文件上传和下载 此文极好,不过有几点要注意: 1.直接按照作者的代码极有可能listfile.jsp文件中 <%@taglib prefix=&qu ...
- 用c++开发基于tcp协议的文件上传功能
用c++开发基于tcp协议的文件上传功能 2005我正在一家游戏公司做程序员,当时一直在看<Windows网络编程> 这本书,把里面提到的每种IO模型都试了一次,强烈推荐学习网络编程的同学 ...
- Python 基于Python实现Ftp文件上传,下载
基于Python实现Ftp文件上传,下载 by:授客 QQ:1033553122 测试环境: Ftp客户端:Windows平台 Ftp服务器:Linux平台 Python版本:Python 2.7 ...
- 构建基于阿里云OSS文件上传服务
转载请注明来源:http://blog.csdn.net/loongshawn/article/details/50710132 <构建基于阿里云OSS文件上传服务> <构建基于OS ...
- 基于hap的文件上传和下载
序言 现在,绝大部分的应用程序在很多的情况下都需要使用到文件上传与下载的功能,在本文中结合hap利用spirng mvc实现文件的上传和下载,包括上传下载图片.上传下载文档.前端所使用的技术不限,本文 ...
随机推荐
- [apue] 使用 poll 检测管道断开
一般使用 poll 检测 socket 或标准输入时,只要指定 POLLIN 标志位,就可以检测是否有数据到达,或者连接断开: ]; fds[].fd = STDIN_FILENO; fds[].ev ...
- 2018.9.8 2018NOIP冲刺之配对
普及组第四题难度 主体思路竟然是贪心Q_Q 链接:https://www.nowcoder.com/acm/contest/164/D来源:牛客网 题目描述 小A有n个长度都是L的字符串.这些字符串只 ...
- seo外链发布之论坛外链
目前最常见的seo外链方式有5种,之前大发迹创业项目网写文章分享过,详情可以查看文章<[网站SEO优化]最常见的五种软文外链发布方式!>,这篇文章不说其他的几种发外链,就来讲讲通过论坛建设 ...
- 在windowx的Hyper-v 安装CentOS系统
博客写的很少,一方面是因为我觉得目前很多博客都是相互抄袭,或者有很多部分都是重复的内容.而我自己再去写同样的内容的画,有点浪费时间. 所以,如果我要写,我希望是写一些与众不同,或者重复率比较低的内容, ...
- Codeforces Gym101503E:XOR-omania(构造+思维)
题目链接 题意 给出m个数b,这些数是由n个数a两两异或组成的,问初始的那n个数分别是多少. 思路 存在多组解的情况...原来是个构造题. 考虑这样一种情况:b1 = a1 ^ a2,b2 = a2 ...
- Codeforces Gym100502G:Outing(缩点+有依赖的树形背包)
http://codeforces.com/gym/100502/attachments 题意:有n个点,容量为tol,接下来n个关系,表示选了第i个点,那么第xi个点就必须被选.问最多可以选多少个点 ...
- web前端兼容性问题总结
1. HTML对象获取问题 FireFox:document.getElementById("idName");ie:document.idname或者document.get ...
- 重新认识 async/await 语法糖
提起.Net中的 async/await,相信很多.neter 第一反应都会是异步编程,其本质是语法糖,但继续追查下去,既然是语法糖,那么经过编译之后,真正的代码是什么样的,如何执行的?带着这些疑问, ...
- [记录]NGINX配置HTTPS性能优化方案一则
NGINX配置HTTPS性能优化方案一则: 1)HSTS的合理使用 2)会话恢复的合理使用 3)Ocsp stapling的合理使用 4)TLS协议的合理配置 5)False Start的合理使用 6 ...
- WMI_COM_API
Win32_Processor // CPU 处理器 Win32_PhysicalMemory // 物理内存 Win32_Keyboard // 键盘 Win32_PointingDevice // ...