Koa

通过利用 async 函数,Koa 帮你丢弃回调函数,并有力地增强错误处理。 Koa 并没有捆绑任何中间件, 而是提供了一套优雅的方法,帮助您快速而愉快地编写服务端应用程序。

基本使用

  • 需先建对象与 express 不同:const app = new Koa()
  • ctx(context)封装了 res 和 req
    • 返回使用 ctx.body,可使用 async 和 await
      • ctx.body 最终会转换为 res.end
      • 执行时机:洋葱模型的最外层,第一个中间件 promise 有结果后,将 ctx.body 转换为 res.end()
      • 异步使用时,不可放置异步函数中,需放置在 await 的 promise 中
    • 也可直接使用 ctx.res 和 ctx.req
    • 常用属性:req、res、request、response
  • ctx 属性:
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
const Koa = require("koa"); // ⼀个类
const app = new Koa();

// 中间件,可以使⽤async await
app.use(async function (ctx) {
// throw new Error('出错了')
// ctx封装了req和res的功能,这⾥相当于res.end
// 1、有输出,ctx.body在promise结果后
ctx.body = await Promise.resolve("zhuwa");
// 2、无输出
setTimeout(() => {
ctx.body = "zhuwa";
}, 3000);
});

// 错误事件
app.on("error", function (err, ctx) {
console.log(err);
ctx.res.end(`500 错误`);
});

app.listen(3002, () => {
console.log("koa server started at 3002");
});

// req request res response的区别
app.use(async function (ctx) {
console.log(ctx.req.path); // 原⽣的req
console.log(ctx.request.path); // koa封装的request
console.log(ctx.request.req.path);
console.log(ctx.path); // 代理到request
ctx.body = await Promise.resolve("zhuwa"); // ctx封装了req和res的功能,这⾥相当于res.end
//ctx.throw(401, "err msg");
});

中间件(洋葱模型)

使用 app.use,回调函数只能传一个,多个需多次注册
异步流程会等待后续中间件有结果时返回
在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行。

  • 多个中间件会形成一个栈结构(middlestack),以"先进后出"(first-in-last- out)的顺序执行。
  • 最外层的中间件首先执行。
  • 调用 next 函数,把执行权交给下一个中间件。
  • 最内层的中间件最后执行。
  • 执行结束后,把执行权交回上一层的中间件。
  • 最外层的中间件收回执行权之后,执行 next 函数后面的代码.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
app.use(async function (ctx, next) {
console.log("1-1");
await next();
console.log("1-2");
});
app.use(async function (ctx, next) {
console.log("2-1");
await next();
console.log("2-2");
});
app.use(async function (ctx, next) {
console.log("3-1");
await next();
console.log("3-2");
});
//输出:1-1、2-1、3-1、3-2、2-2、1-2

与 express 比较

框架 express koa
模型 线性模型 洋葱模型
表现 同步表现为洋葱模型,但遇到异步,即使使⽤了 await next()也不会等待,会继续向下执⾏,因为 next()返回 undefined ⽆论同步,还是异步,都是洋葱模型,可以使⽤ await 和 next() 来等待下⼀个中间件执⾏完成
原理 使⽤回调, next(),返回 undefined next(),返回 promise,会把中间件⽤ promise 包裹起来
错误 错误处理是在⼀个特殊签名的中间件中完成的,它必须被添加到链的后⾯才能捕获到前⾯的错误 可以使⽤ try catch 捕获中间件错误
注意 await next() 也不会等待下⼀个中间件异步完成 要使⽤ await 来等待异步操作;注意 res.end 的时机,等最外层的中间件(也就是第⼀个中间件)的 promise 有结果后,响应就会被返回 ctx.body ->res.end(ctx.body)

中间件编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//编写⼀个计算请求的时⻓的中间件
// koa 中间件
app.use(async function (ctx, next) {
console.time("serviceTime");
await next();
console.timeEnd("serviceTime");
});

// express 中间件
app.use(function (req, res, next) {
console.time("service");
next();
res.once("close", () => {
console.timeEnd("service");
});
});

错误处理

在最顶层捕获未处理的错误
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// 可以使⽤try catch 捕获中间件错误
// 在最顶层捕获未处理的错误
app.use(async (ctx, next) => {
try {
// await 可以让异步函数的错误得到捕获
await next();
} catch (err) {
const status = err.status || 500;
ctx.status = status;
ctx.type = "html";
ctx.body = `
<b>${status}</b> ${err}
`;
// emmit
ctx.app.emit("error", err, ctx);
}
});

app.use(async (ctx, next) => {
try {
await next()
} catch (err) {
ctx.status = 400
ctx.body = `Uh-oh: ${err.message}`
console.log('Error handler:', err.message)
}
})

app.use(async (ctx) => {
if (ctx.query.greet !== 'world') {
throw new Error('can only greet "world"')
}
console.log('Sending response')
ctx.status = 200
ctx.body = `Hello ${ctx.query.greet} from Koa`
})

// 也可以使⽤总的错误事件来统⼀处理
// 错误事件
app.on('error', function(err) {
console.log(err)
})

````

## 常用中间件

- koa-router:路由
- koa-static:静态资源
- koa-mounut:子路由
- koa-body:body解析
- koa-parameter:参数校验
```javascript
//1、koa-router
const KoaRouter = require('koa-router')
router.get('/page', async ctx => {
ctx.body = await new Promise((resolve, reject) => {
resolve('ok')
})
})
// 注册中间件
app.use(router.routes())
// 添加前缀
const router = new Router({ prefix: '/user' })
// 重定向
router.get('/home', async ctx => {
ctx.redirect('http://baidu.com');
});
// 路由参数
router.get('/:id', async ctx => {
ctx.body = {
msg: 'index page',
params: ctx.params.id // 通过 ctx.params.id 获取到的是 1
};
});
router.get('/', async ctx => {
ctx.body = {
msg: 'index page',
query: ctx.query // ctx.query会获取url后⾯携带的参数对象
};
});
// allowedMethod
// 如果没有这⼀⾏, 当使⽤其他请求⽅式请求user时, 会提示404
// 如果有这⼀⾏, 当使⽤其他请求⽅式请求user时, 提示405 method not allowed
app.use(router.allowedMethod());

//2、koa-static
const KoaStatic = require('koa-static')
// 动态加载静态资源
app.use(KoaStatic(path.resolve(__dirname, '../dist/assets')))

//3、koa-mounut
const KoaMount = require('koa-mount')
// 添加前缀
app.use(KoaMount('/assets', KoaStatic(path.resolve(__dirname, '../dist/assets'))))

//4、koa-body
const bodyparser = require('koa-body');
// 在注册此中间件之后, 会在 ctx 上下⽂注⼊ ctx.request.body 属性 ⽤于获取客户端传递来的数据
app.use(bodyparser());
router.post('/', async ctx => {
ctx.body = {
code: 200,
//body 可以获取form表单数据, json数据
msg: ctx.request.body
}
})

//5、koa-parameter
const parameter = require('koa-parameter');
parameter(app);
router.post('/', async ctx => {
//接收⼀个对象
ctx.verifyParams({
// 校验的简写形式
username: 'string',
password: { type: 'string', required: true } // 或者也可以这样
})
ctx.body = { code: 1 }
})
````

## koa-compose

中间件合并处理

```javascript
compose(stack)({});
  • stack 是数组,收集了多个 async 函数,即返回值是 promise,内部有异步,总之就是收集了所有的异步操作;
  • compose 是核心代码,函数接收 stack 数组返回一个匿名函数(这里采用了闭包);
  • 核心代码就是执行这个匿名函数;

我们来看下 compose 函数:

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
function compose(middleware) {
if (!Array.isArray(middleware))
throw new TypeError("Middleware stack must be an array!");
for (const fn of middleware) {
if (typeof fn !== "function")
throw new TypeError("Middleware must be composed of functions!");
}

/**
* @param {Object} context
* @return {Promise}
* @api public
*/

// 核心匿名函数:接收上下文对象context和自定义next函数;
return function (context, next) {
// last called middleware #
let index = -1;
return dispatch(0); //调用第一个元素函数
function dispatch(i) {
// 防止连续执行两次next
if (i <= index)
return Promise.reject(new Error("next() called multiple times"));
index = i;
let fn = middleware[i];
if (i === middleware.length) fn = next; //此处i对应的fn是undefined,也支持自定义next
if (!fn) return Promise.resolve(); // 最后一个直接返回
try {
// 递归去执行dispatch,本质是获取下一个fn元素
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err);
}
}
};
}

Context(上下文)

Koa Context 将 node 的 request 和 response 对象封装到单个对象中,为编写 Web 应用程序和 API 提供了许多有用的方法。

1
2
3
4
5
app.use(async (ctx) => {
ctx; // 这是 Context
ctx.request; // 这是 koa Request
ctx.response; // 这是 koa Response
});

为方便起见许多上下文的访问器和方法直接委托给它们的 ctx.request 或 ctx.response ,不然的话它们是相同的。 例如 ctx.type 和 ctx.length 委托给 response 对象,ctx.path 和 ctx.method 委托给 request。

ctx.req

Node 的 request 对象.

ctx.res

Node 的 response 对象.
绕过 Koa 的 response 处理是 不被支持的. 应避免使用以下 node 属性:

  • res.statusCode
  • res.writeHead()
  • res.write()
  • res.end()

别名

Request 别名

以下访问器和 Request 别名等效:

  • ctx.header
  • ctx.headers
  • ctx.method
  • ctx.method=
  • ctx.url
  • ctx.url=
  • ctx.originalUrl
  • ctx.origin
  • ctx.href
  • ctx.path
  • ctx.path=
  • ctx.query
  • ctx.query=
  • ctx.querystring
  • ctx.querystring=
  • ctx.host
  • ctx.hostname
  • ctx.fresh
  • ctx.stale
  • ctx.socket
  • ctx.protocol
  • ctx.secure
  • ctx.ip
  • ctx.ips
  • ctx.subdomains
  • ctx.is()
  • ctx.accepts()
  • ctx.acceptsEncodings()
  • ctx.acceptsCharsets()
  • ctx.acceptsLanguages()
  • ctx.get()

Response 别名

以下访问器和 Response 别名等效:

  • ctx.body
  • ctx.body=
  • ctx.status
  • ctx.status=
  • ctx.message
  • ctx.message=
  • ctx.length=
  • ctx.length
  • ctx.type=
  • ctx.type
  • ctx.headerSent
  • ctx.redirect()
  • ctx.attachment()
  • ctx.set()
  • ctx.append()
  • ctx.remove()
  • ctx.lastModified=
  • ctx.etag=