Koa一款简洁,轻量级的HTTP中间件框架。它本身不包含任何中间件,你可以根据自己业务需求来添加使用中间件,中间件支持链接调用。

中间件是一个函数(异步或者同步)处在 HTTP request(请求)与 HTTP response (响应)之间,用来实现某种中间功能 app.use() 来加载中间件。基本上,Koa 所有功能都是通过中间件来实现的,中间件函数会被传入两个参数:1) ctx context 对象,表示一次对话的上下文(requset和response);2) next 函数,调用 next 函数可以把执行权交给下一个中间件,下一个中间件执行完会把执行权再交回上一个中间件。如果中间件中有异步操作,需要使用 async、await 关键字,将其写成异步函数

Koa开发团队和Express是同一群人,Koa和Express设计思想上很相近。Express开发中你需要写很多函数回调以实现异步编程。但Koa是通过async方法实现异步编程,在代码整洁和可阅读性上会更好。

Koa核心概念只有:Application(应用程序),Context(请求上下文),Request(http request对象),Response(http response对象)。

Hello World

Koa必须运行在node v7.6.0以上,因为它完全基于async方法之上。

1
$ yarn add koa
1
2
3
4
5
6
7
8
9
10
11
12
const Koa = require('koa');
const app = new Koa(); // 创建一个 Koa应用(Application)

/*
* ctx Koa上下文(context),包含Node http中的 request和response,并提供了包装对象。
*/
app.use(async ctx => {
ctx.body = 'Hello World!'
});

// 启动一个node http服务器
app.listen(3000)

Application

Koa 应用程序是包含了一组中间件函数的对象。Application接受到一个http请求,它会按照中间件之间的编码顺序在栈内依次执行。并且它允许你执行操作向下传递请求,之后过滤并逆序返回响应。

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
const Koa = require('koa');
const app = new Koa();

// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});

// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response
app.use(async ctx => {
ctx.body = 'Hello World';
});

app.listen(3000);

上面例子中通过app.use()给应用程序添加三个自定义的中间件。当一个http请求进入时,x-response-time中间件先执行。x-response-time内部调用了await next()方法,它表示先执行下一个中间件(logger)。logger中间件这时就会被调用,在logger内部也有个await next()方法。则最后一个response中间件被执行。response执行完后再继续执行logger中间件剩余代码,最后执行x-response-time中间件剩余代码。

app.listen()

创建并启用一个http服务器。

1
2
3
const Koa = require('koa');
const app = new Koa();
app.listen(3000); // 等同于:http.createServer(app.callback()).listen(3000);

app.use(function)

将一个中间件添加到应用程序中。当有http进入时可能会触发该中间件。

app.context

Context的原型。你可以通过编辑app.context为Context添加属性。添加的属性可以在任何中间件中的Context中访问到。

Context

每个请求都会创建一个Context,在中间件中通过参数(ctx)可以获得。Context将node得request和response对象封住在单个对象上,并提供了在HTTP服务器开发中会经常使用到得方法。

1
2
3
4
5
6
7
8
9
10
app.use(async ctx => {
ctx; // 这是 Context
ctx.request; // 这是 koa Request
ctx.response; // 这是 koa Response
ctx.res; // Node 的response对象
ctx.req; // Node 的request对象
ctx.state; // koa官方推荐的 命名空间,用于在中间件之间传递信息
ctx.app; // 应用程序实例引用
ctx.throw(400, 'name required'); // 抛出异常
});

设置、获取cookie

通过 ctx.cookies.get(name,[options])可以获取到网络中的cookie。

options支持的选项:

  • signed 所请求的cookie应该被签名

通过ctx.cookies.set(name,value,[options])创建cookie。

options支持的选项:

  • maxAge 有效期。如果为正数,表示改Cookie会在maxAge毫秒后自动失效如果为负数表示Cookie仅在本浏览器窗口以及子窗口打开的子窗口有效。
  • signed cookie签名值。
  • expires 过期时间。// new Date(‘2017-02-15’)
  • path cookie保存路径。
  • domain cookie所在域名
  • httpOnly 是否只用于http请求中获取
  • overwrite 是否允许重写
  • secure 是否只能用https传递

Request

Koa Request对象是在node 的vanilla请求对象之上的抽象,提供很多在HTTP服务中经常使用的方法

1
2
3
4
5
6
7
8
9
10
request.header // 请求标头   ctx.header
request.header= // 设置请求标头对象
request.method // 请求方法 ctx.method
request.url // 获取请求URL ctx.url
request.origin // 获取URL的来源,包括protocol 和host ctx.origin
request.href // 获取完整的请求URL 包括protocol host和url ctx.href
request.path // 获取请求路径名 别名 ctx.path
request.querystring // 根据?获取原始查询字符串 ctx.querystring
request.host // 获取当前主机(hostname:port) ctx.host
request.type // 获取请求 Content-Type

Response

1
2
3
4
5
6
7
8
9
response.header // 响应标头对象
response.socket // 请求套接字
response.status // 获取响应状态 ctx.status
response.status= // 设置数字代码响应状态
response.message // 获取响应的状态消息
response.message= // 将响应的状态消息设置为给定值
response.body // 获取响应主体 ctx.body
response.body= // 设置响应主体(string,Buffer,Stream,Object||Array JSON-字符串化)ctx.body=

插件

Koa-router

Koa-router 是使用最多的Koa路由中间件。

常用用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router(); // 实例化 rouer

router.get('/', (ctx, next) => {
ctx.redirect('/index'); // 跳转 首页
});

router.get('/index', (ctx, next) => {
ctx.body="index page!" // 返回响应信息
});

// router.routes() 将router添加仅Koa
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000, function () {
console.log('listen at http://localhost:' + 3000);
});

HTTP动词方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router
.get('/', (ctx, next) => {
ctx.body = 'Hello World!';
})
.post('/users', (ctx, next) => {
// ...
})
.put('/users/:id', (ctx, next) => {
// ...
})
.del('/users/:id', (ctx, next) => {
// ...
})
.all('/users/:id', (ctx, next) => {
// ...
});

router.all()匹配所有方法。

路由嵌套

1
2
3
4
5
6
7
8
9
var forums = new Router();
var posts = new Router();

posts.get('/', (ctx, next) => {...});
posts.get('/:pid', (ctx, next) => {...});
forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());

// responds to "/forums/123/posts" and "/forums/123/posts/123"
app.use(forums.routes());

URL参数

通过路由参数映射添加到ctx.params

1
2
3
4
router.get('/:category/:title', (ctx, next) => {
console.log(ctx.params);
// => { category: 'programming', title: 'how-to-node' }
});

指定路由前缀

路由器级别上可以添加路径前缀

1
2
3
4
5
6
var router = new Router({
prefix: '/users'
});

router.get('/', ...); // responds to "/users"
router.get('/:id', ...); // responds to "/users/:id"

实践设计

| app.js — 应用入口

| router.js — 路由文件

| controllers — 控制器目录

​ |login.js — 登录页 控制器

​ |index.js — 首页 控制器

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
// app.js
const Koa = require('koa');
const app = new Koa();

const router = require('./router')

app.use(router());
app.listen(3000,()=>{
console.log("koa service start")
})

// router.js
const fs = require('fs');
const Router = require('koa-router');
const router = Router();

// 将controller注册到 router中
function register (router, rules) {
for(let val of rules) {
let paramsArr = [val.path];
if (Array.isArray(val.ctr)) {
for(let ctrItem of val.ctr) {
paramsArr.push(ctrItem);
}
} else {
paramsArr.push(val.ctr);
}
router[val.method].apply(router, paramsArr);
}
}

// 收集controllers 目录下的js文件
function collectRule (router) {
let files = fs.readdirSync(__dirname+'/controllers');
let js_files = files.filter((f)=>{
return f.endsWith('.js');
})
for(let f of js_files ) {
let rules = require(__dirname +'/controllers/'+f);
register(router,rules)
}
}

module.exports = function () {
collectRule(router);
return router.routes();
}

// controllers/login
const login = async (ctx,next) => {
ctx.body = "Login page!"
}

module.exports = [
{
method: 'get',
path: ['/','/login'],
ctr: login
}
]

// controller/index
const index = async (ctx,next) => {
ctx.body = "Index page!"
}

module.exports = [
{
method: 'get',
path: '/index',
ctr: index
}
]

koa-bodyparser

koa-bodyparser会将请求的数据解析到ctx.request.body上。如果没有数据则是个空对象。

1
2
3
4
5
6
7
8
9
10
11
var Koa = require('koa');
var bodyParser = require('koa-bodyparser');

var app = new Koa();
app.use(bodyParser());

app.use(async ctx => {
// the parsed body will store in ctx.request.body
// if nothing was parsed, body will be an empty object {}
ctx.body = ctx.request.body;
});

Koa-static

Koa-static 静态资源服务中间件,实现静态文件的访问响应。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const app = new Koa();
const static = require('koa-static');
const staticPath = './static'// 相对启动文件 地址

// static有两个参数,第一个参数是静态文件入口地址,第二个参数是配置对象
app.use(static(
path.join( __dirname, staticPath)
))

app.use( async ( ctx ) => {
ctx.body = 'hello world'
})

app.listen(3000, () => {
console.log('[demo] static-use-middleware is starting at port 3000')
})

Koa-session

Koa-session中间件实现服务器端很重要的session创建读取删除等功能,并能指定session保存的位置。

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
const session = require('koa-session');
const Koa = require('koa');
const app = new Koa();

app.keys = ['some secret hurr'];

const CONFIG = {
key: 'koa:sess', /** cookie名称 */
maxAge: 86400000, /** 过期时间 */
overwrite: true, /** 能否复写 */
httpOnly: true, /** 只能用于http */
signed: true, /** 是否只限于https */
};

app.use(session(CONFIG, app));

app.use(ctx => {
// ignore favicon
if (ctx.path === '/favicon.ico') return;

let n = ctx.session.views || 0;
ctx.session.views = ++n;
ctx.body = n + ' views';
});

app.listen(3000);
console.log('listening on port 3000');

第三方存储

如果需要将 session保存到数据库中(redis、mysql、moongodb),需要在config配置项添加store属性。store中需要定义三个方法:

  1. get(key, maxAge, { rolling }) - 通过key获取session
  2. set(key, sess, maxAge, { rolling, changed }) - 设置session
  3. destroy(key) - 销毁session
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
const Koa = require('koa');
const app = new Koa();
const session = require('koa-session')
app.keys = ['secret'];
const { promisify } = require('util');
var redis = require("redis"),
client = redis.createClient();
const getAsync = promisify(client.get).bind(client);

client.on('ready',()=>{
console.log('redis,ready')
})

client.on("error", (err) => {
console.log("Error " + err);
});
const CONFIG = {
key: 'nodejs:session', /** cookie名称 */
maxAge: 86400000, /** 过期时间 */
overwrite: true, /** 能否复写 */
httpOnly: false, /** 只能用于http */
signed: false, /** 是否只限于https */
store: {
async get(key, maxAge) {
let result = await getAsync(key);
return JSON.parse(result);
},
async set(key, sess, maxAge) {
let result = client.set(key, JSON.stringify(sess));
},
destroy(key) {
client.del(key);
}
}
};
app.use(session(CONFIG, app));