前言
文件上传,是写后端服务必须要遇到的一道坎儿,周末做chrome日志分析的时候,用到了上传功能,因为以前都是用java来写后端,这次改用nodejs写后端,遇到了一点小坎坷,特此做一个总结。
koa介绍
Koa是基于Node.js的下一代web框架,由Express团队打造,特点:优雅、简洁、灵活、体积小。几乎所有功能都需要通过中间件实现。
- Express是第一代最流行的web框架,它对Node.js的http进行了封装,用起来如下:
1 2 3 4 5 6 7 8 9 10
| var express = require('express'); var app = express();
app.get('/', function (req, res) { res.send('Hello World!'); });
app.listen(3000, function () { console.log('Example app listening on port 3000!'); });
|
虽然Express的API很简单,但是它是基于ES5的语法,要实现异步代码,只有一个方法:回调。如果异步嵌套层次过多,代码写起来就非常难看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| app.get('/test', function (req, res) { fs.readFile('/file1', function (err, data) { if (err) { res.status(500).send('read file1 error'); } fs.readFile('/file2', function (err, data) { if (err) { res.status(500).send('read file2 error'); } res.type('text/plain'); res.send(data); }); }); });
|
- koa 1.0
随着新版Node.js开始支持ES6,Express的团队又基于ES6的generator重新编写了下一代web框架koa。和Express相比,koa 1.0使用generator实现异步,代码看起来像同步的:
1 2 3 4 5 6 7 8 9 10
| var koa = require('koa'); var app = koa();
app.use('/test', function *() { yield doReadFile1(); var data = yield doReadFile2(); this.body = data; });
app.listen(3000);
|
用generator实现异步比回调简单了不少,但是generator的本意并不是异步。Promise才是为异步设计的,但是Promise的写法……想想就复杂。为了简化异步代码,ES7引入了新的关键字async
和await
,可以轻松地把一个function变为异步模式:
1 2 3
| async function () { var data = await fs.read('/file1'); }
|
这是JavaScript未来标准的异步代码,非常简洁,并且易于使用。
- koa2
koa团队并没有止步于koa 1.0,他们非常超前地基于ES7开发了koa2,和koa 1相比,koa2完全使用Promise并配合async来实现异步。
koa2的代码看上去像这样:
1 2 3 4 5 6
| app.use(async (ctx, next) => { await next(); var data = await doReadFile(); ctx.response.type = 'text/plain'; ctx.response.body = data; });
|
ES7是大势所趋,所以本次上传功能,直接使用基于ES7的koa2来实现。
Stream流介绍
Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。
Stream 有四种流类型:
- Readable - 可读操作。
- Writable - 可写操作。
- Duplex - 可读可写操作.
- Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
- data - 当有数据可读时触发。
- end - 没有更多的数据可读时触发。
- error - 在接收和写入过程中发生错误时触发。
- finish - 所有数据已被写入到底层系统时触发。
上传的本质就是,客户端输入流,服务器端接收后输出流,我们上传要用的就是流中的管道流:用于从一个流中获取数据并将数据传递到另外一个流中。
1 2 3 4 5 6 7 8 9 10 11
| var fs = require("fs");
var readerStream = fs.createReadStream('input.txt');
var writerStream = fs.createWriteStream('output.txt');
readerStream.pipe(writerStream);
console.log("程序执行完毕");
|
上传功能实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| const fs = require('fs') const path = require('path') const Koa = require('koa') const router = require('koa-router')() const koaBody = require('koa-body');
const app = new Koa()
app.use(async (ctx, next) => { console.log(`Process ${ctx.request.method} ${ctx.request.url}...`) await next() })
app.use(koaBody({ multipart: true, formidable: { maxFileSize: maxSize * 1000 * 1024 * 1024 } }))
router.post('/upload', async (ctx, next) => { const file = ctx.request.files.file;
var myDate = new Date(); var newFilename = myDate.getTime() + '-' + file.name; var targetPath = path.join(__dirname, '/upload/' + `${newFilename}`); const reader = fs.createReadStream(file.path); const upStream = fs.createWriteStream(targetPath); reader.pipe(upStream);
return ctx.body = { code: 200, data: { url: 'http://' + ctx.headers.host + '/' + newFilename, local: targetPath } }; });
app.use(router.routes())
app.listen(port) console.log(`应用程序已经启动,访问地址:http://127.0.0.1:${port}`)
|
上面的实现,是一个异步的,即如果上传完成之后,立刻对该上传的文件做操作,文件本身是没有写入完成的,会导致程序异常,笔者就在这里被小坑了一把(也有可能是学艺不精导致的-_-||),所以如果需要确保上传文件肯定可用,需要将上传改为异步的,改动方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| router.post('/upload', async (ctx, next) => { const file = ctx.request.files.file; var myDate = new Date(); var newFilename = myDate.getTime() + '-' + file.name; var targetPath = path.join(__dirname, '/upload/' + `${newFilename}`); var writeFile = function() { return new Promise(function (resolve, reject) { const reader = fs.createReadStream(file.path); const upStream = fs.createWriteStream(targetPath); reader.pipe(upStream); upStream.on('finish', () => { resolve('finish'); }); }); } await writeFile(); return ctx.body = { code: 200, data: { url: 'http://' + ctx.headers.host + '/' + newFilename, local: targetPath } }; });
|