koa 框架
koa 是Node.js平台下一代Web 开发框架,是 Express原班人马打造。
架设 http 服务
let Koa = require('koa');
let app = new Koa();
app.listen(3000);
复制代码
以上是一个最简单的 http 服务,访问
http://localhost:3000
页面返回的是 Not Found,是因为我们告诉 koa 应该显示什么内容。ctx.status 默认404,下面会讲解到 app.listen(3000) 只是下面方法的语法糖
let http = require('http');
http.createServer().listen(3000);
复制代码
Context
Context 将 Node 的 request 和 response 对象封装在单个对象中,每个请求都将创建一个 context,并在中间件中作为接收器使用,或者可以使用 ctx 标识符。
let Koa = require('koa');
let app = new Koa();
app.use(ctx=>{
ctx.body = 'hello world!';
});
app.listen(3000);
复制代码
上面代码中,使用 use 方法加载一个函数,使用 ctx.body发送给用户内容,ctx.body等同于ctx.response.body
meddleware 中间件
中间件就是就是处于http request 和 http response 的中间,用来实现某种中间的功能,比如,请求参数解析等 每个中间件都有两个参数ctx对象,next函数,只要调用 next 函数,就可以把执行权交给下一个中间件。
let Koa = require('koa');
let app = new Koa();
app.use((ctx, next)=>{
console.log('1');
next();
console.log('2');
})
app.use((ctx, next)=>{
console.log('3');
next();
console.log('4');
})
app.use((ctx, next)=>{
console.log(5);
})
app.listen(4000);
//输出结果
1
3
5
4
2
复制代码
next 函数执行后, 相当于把将上面代码改成下面代码:
let Koa = require('koa');
let app = new Koa();
app.use((ctx,next)=>{
console.log('1');
//next() 当调用next函数的时候,其实相当于把下一个 use 函数嵌套进来
(ctx, next)=>{
console.log('3');
//next(); 当调用next函数的时候,其实相当于把下一个 use 函数嵌套进来
(ctx, next)=>{
console.log(5);
}
console.log('4');
}
console.log('2');
})
app.listen(4000);
复制代码
从上面修改后的代码可以看出,next函数执行后,代码形成嵌套的形式执行,形成洋葱模型,所以最终输出 1 3 5 4 2
use 函数
官网称:将给定的中间件方法添加到此应用程序,白话就是,use 方法是添加中间件的,那么,use 到底是这么添加中间件方法的呢,看下面代码:
//上面已经讲过 use 的使用案例
let Koa = require('koa');
let app = new Koa();
app.use((ctx, next)=>{
console.log(1);
next();
})
app.use((ctx, next)=>{
console.log(2);
})
app.listen(4000);
复制代码
从上面我们可以看出,use 接受一个函数,这个函数中接受两个参数 ctx对象 和 next函数,use方法可以给应用程序绑定多个中间件,如果中间件中执行 next 方法,会将执行权交给下一个中间件函数执行,当所有的 next 函数执行完成后,再讲执行权交给上游去继续执行,下面我们来实现 use函数 原理
function app() {};
app.middleware = [];
app.use = (fn)=>{
app.middleware.push(fn); //将所有 use 函数中的方法存放在一起
}
//下面两个 use 方法会将 use 中的两个函数全部添加到 middleware数组中
app.use((ctx, next)=>{
console.log('1');
next();
})
app.use((ctx, next)=>{
console.log('2');
})
let index = 0;
function dispatch(index){
if(index >= app.middleware.length) return;
app.middleware[index]({},()=>{ dispatch(index+1) })
}
dispatch(index); //开始执行 middleware中的每个中间件,从第一个开始
复制代码
异步中间件
上面的代码虽然表面上没有什么问题,是因为 next 方法是同步执行的,但是如果遇到下面这种 下一个 use 方法中带有异步的情况,程序会输出什么?
let Koa = require('koa');
let app = new Koa();
app.use((ctx, next)=>{
console.log('1');
setTimeout(()=>{
console.log('2');
},2000)
next();
console.log('3');
})
app.use((ctx, next)=>{
console.log('4');
})
app.listen(4000);
//按照我们之前案例,应该输出 1 2 4 3,但是。。。。。
//结果输出:
1
4
3
2 //2是等待了两秒
复制代码
为什么跟我们预想的不一样,是因为我们注意到,在下一个中间件中存在异步方法,next 函数并不等待异步函数执行完才执行,而是绕过异步函数,直接执行 next 方法。 但是我们肯定是想让 next 方法等待异步方法执行完后再执行,所以自然而然应该想到用 promise去包装一下异步函数, 并使用 async+await 去执行
//将上面的 setTimeout封装成 promise
function setT(){
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log('2');
resolve();
})
})
}
let Koa = require('koa');
let app = new Koa();
app.use(async (ctx, next)=>{
console.log('1');
await setT();
next();
console.log('3');
})
app.use((ctx, next)=>{
console.log('4');
})
app.listen(4000);
//运行结果,输出:
1
2
4
5
//跟我们之前预想的是一样的
复制代码
中间件用法案例
提交表单
现在我们模拟真实场景,中间件处理提交表单 中间件会拦截每一次请求
let Koa = require('koa');
let app = new Koa();
app.use((ctx, next)=>{
if(ctx.path == '/' && ctx.method == 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = `
<form action="/" method="post">
<input type="text" name="username" />
<input type="text" name="password" />
<input type="submit"/>
</form>
`;
}else{
next();
}
});
app.use((ctx, next)=>{
if(ctx.method == 'POST' && ctx.path == '/'){
let arr = [];
ctx.req.on('data', data=>{
arr.push(data);
})
ctx.req.on('end', ()=>{
console.log(Buffer.concat(arr).toString());
ctx.body = Buffer.concat(arr).toString()
})
}
})
app.listen(4000);
复制代码
执行上面代码,我们发现一个问题,在控制台可以输出我们打印的信息,但是ctx.body并没有输出到页面上,为什么? 原因还是上面讲到的,on监听方法是异步的,当执行 on 方法时,其实 use 方法已经走完了,我们还是要将 on 方法包装成 promise 方法
let Koa = require('koa');
let app = new Koa();
app.use(async (ctx, next)=>{
if(ctx.path == '/' && ctx.method == 'GET'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = `
<form action="/" method="post">
<input type="text" name="username" />
<input type="text" name="password" />
<input type="submit"/>
</form>
`;
}else{
await next(); //这里不要忘记加上 await
}
});
function bodyParaser(ctx){
return new Promise((resolve, reject)=>{
let arr = [];
ctx.req.on('data', data=>{
arr.push(data);
})
ctx.req.on('end', ()=>{
let body = Buffer.concat(arr).toString();
resolve(body);
})
})
}
app.use(async (ctx, next)=>{
if(ctx.method == 'POST' && ctx.path == '/'){
let body = await bodyParaser(ctx);
console.log(body);
ctx.body = body;
}
})
app.listen(3000);
复制代码
文件上传
我们在页面中有上传文件功能时,我们点击提交按钮后,我们来看下 request 请求:

我们再来看下提交的表单数据:
我们最终想要的数据是:
{
username: 'xx',
password: 'cc'
}
复制代码
所以我们需要对提交的数据进行解析,那么我们应该怎么解析呢?不难发现我们可以用请求中的 Content-Type 中的 boundary去分割 请求的数据,然后将数据组装成我们想要的格式:
let Koa = require('koa');
let app = new Koa();
//Buffer对象没有 split 方法
Buffer.prototype.split = function(sep){
let index = 0;
let pos = 0;
let arr = [];
let len = Buffer.from(sep).length;
while(-1 != (pos = this.indexOf(sep, index))){
arr.push(this.slice(index, pos));
index = pos + len;
}
arr.push(this.slice(index));
return arr;
}
function bodyParser(uploader){
return async (ctx, next) =>{
await new Promise((resolve, reject)=>{
let buffer = [];
ctx.req.on('data', data=>{
buffer.push(data);
})
ctx.req.on('end', ()=>{
let buf = Buffer.concat(buffer); //获取到所有的请求数据
let contentType = ctx.get('Content-Type').split('----')[1]; //获取 boundary
let boundary = '-------' + contentType;
let arrs = buf.split(boundary); //用 boundary 去分割请求数据
console.log(arrs);
arrs = arrs.slice(1, -1); //去掉开头和末尾
console.log(arrs);
let body = {};
arrs.forEach(item => { //获取每一个 field
let [head,tail] = item.split('\r\n\r\n'); //将每一组 field 进行空行回车分割
head = head.toString(); //将 Buffer 转码
console.log(head);
if(head.includes('filename')){ //如果是文件
let tail = lines.slice(head.length + 4, -2);
let ws = require('fs').createWriteStream(Date.now() + Math.random() + "");
ws.end(tail);
}else{ //否则是普通文本
let key = head.match(/name="(\w+)"/)[1];
console.log(key);
body[key] = tail.toString().slice(0, -2);
}
});
ctx.request.fields = body;
resolve();
})
})
await next();
}
}
app.use(bodyParser({
uploadDir: __dirname
}))
app.use(async (ctx, next)=>{
if(ctx.method == 'GET' && ctx.path == '/'){
ctx.set('Content-Type', 'text/html;charset=utf-8');
ctx.body = `
<form action="/" method="post" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="text" name="password" />
<input type="file" name="filename">
<input type="submit"/>
</form>
`;
}else{
await next();
}
})
app.use(async (ctx, next)=>{
if(ctx.method == 'POST' && ctx.path == '/'){
ctx.body = ctx.request.fields;
}
})
app.listen(4000);
复制代码