Koa 是一个类似于 Express 的Web开发框架,创始人也都是TJ。Koa 的主要特点是,使用了 ES6 的 Generator 函数,进行了架构的重新设计。Koa 的原理和内部结构很像 Express,但是语法和内部结构进行了升级。
创建Koa应用
创建一个 koa 非常简单:
var koa = require('koa');
var app = koa();
app.listen(3000);
或者可以酱紫:
var koa = require('koa');
var http = require('http');
var app = koa();
http.createServer(app.callback()).listen(4000);
这两种方式在 koa 内部是等价的,在 Application 模块中, listen 就会调用自身的 callback:
//listen的实现
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
callback 返回的函数会作为 server 的回调:
app.callback = function(){
/**
* 省略的代码
**/
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).then(function () {
respond.call(ctx);
}).catch(ctx.onerror);
}
};
callback 也会将多个中间件转成了一个 fn,在构建服务器函数时方便调用。状态码默认是 404,即没有任何中间件修改过就是 404。
每个请求都会通过 createContext 创建一个上下文对象,其参数则分别是 Node 的 request 对象和 response 对象:
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, {
keys: this.keys,
secure: request.secure
});
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
对于接收的参数,在返回上下文 context 之前,koa 会将参数注入到自身的 request 对象和 response 对象上,ctx.request 和 ctx.response 返回的是 koa 的对应对象,ctx.req 和 ctx.res 返回的是 Node 的对应对象;同时也会将 app 注册到 context/respose/request 对象上,方便在各自的模块中调用:
var app = Application.prototype;
module.exports = Application;
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development'; //环境变量
this.subdomainOffset = 2; //子域偏移量
this.middleware = []; //中间件数组
this.proxy = false; //是否信任头字段 proxy
this.context = Object.create(context); // koa的上下文(this)
this.request = Object.create(request); //koa的request对象
this.response = Object.create(response); //koa 的reponse对象
}
上下文:context
context 对象是 Koa context 模块扩展出来的,添加了诸如 state、cookie、req、res 等属性。
onFinished 是一个第三方函数,用于监听 http response 的结束事件,执行回调。如果找到 context.onerror 方法,这是 koa默认的错误处理函数,它处理的是错误导致的异常结束。错误的处理是在 callback 中监听的:
// callback
if (!this.listeners('error').length) this.on('error', this.onerror);
koa 本身是没有定义事件处理机制的,其事件处理机制继承自 Node 的 events:
var Emitter = require('events').EventEmitter;
Object.setPrototypeOf(Application.prototype, Emitter.prototype);
默认的错误分发是在 Context 模块中:
onerror : function(err){
//some code
this.app.emit('error', err, this);
//some code
}
此外,在 Context 模块中,还将 request 对象和 response 对象的一些方法和属性委托给了 context 对象:
//response委托
delegate(proto, 'response')
.method('attachment')
.method('append')
.access('status')
.access('body')
.getter('headerSent')
.getter('writable');
.....
//request委托
delegate(proto, 'request')
.method('acceptsLanguages')
.method('get')
.method('is')
.access('querystring')
.access('url')
.getter('origin')
.getter('href')
.getter('subdomains')
.getter('protocol')
.getter('host')
....
通过第三方模块 delegate 将 koa 在 Response 模块和 Request 模块中定义的方法委托到了 context 对象上,所以以下的一些写法是等价的:
//在每次请求中,this 用于指代此次请求创建的上下文 context(ctx)
this.body ==> this.response.body
this.status ==> this.response.status
this.href ==> this.request.href
this.host ==> this.request.host
.....
在 createContext 方法中,还给 context 定义了重要属性 state
context.state = {}
这个属性可以被各个中间件共享,用于在中间件之间传递数据,这也是 koa 推荐的方式:
this.state.user = yield User.find(id);
中间件
中间件是对 HTTP 请求进行处理的函数,对于每一个请求,都会通过中间件进行处理。在 koa 中,中间件通过 use 进行注册,且必须是一个 Generator 函数(未开启 this.experimental):
app.use(function* f1(next) {
console.log('f1: pre next');
yield next;
this.body = 'hello koa';
console.log('f1: post next');
});
app.use(function* f2(next) {
console.log(' f2: pre next');
console.log(' f2: post next');
});
输出如下:
f1: pre next
f2: pre next
f2: post next
f1: post next
与 Express 的中间件顺序执行不同,在koa中,中间件是所谓的“洋葱模型”或级联式(Cascading)的结构,也就是说,属于是层层调用,第一个中间件调用第二个中间件,第二个调用第三个,以此类推。上游的中间件必须等到下游的中间件返回结果,才会继续执行。
koa 对中间件的数量并没有限制,可以随意注册多个中间件。但如果有多个中间件,只要有一个中间件缺少 yield next 语句,后面的中间件都不会执行:
app.use(function *(next){
console.log('>> one');
yield next;
console.log('<< one');
});
app.use(function *(next){
console.log('>> two');
this.body = 'two';
console.log('<< two');
});
app.use(function *(next){
console.log('>> three');
yield next;
console.log('<< three');
});
上面代码中,因为第二个中间件少了yield next语句,第三个中间件并不会执行。
如果想跳过一个中间件,可以直接在该中间件的第一行语句写上return yield next:
app.use(function* (next) {
if (skip) return yield next;
})
koa中,中间件唯一的参数就是 next。如果要传入其他参数,必须另外写一个返回 Generator 函数的函数。
this.experimental 是为了判断是否支持es7,开启这个属性之后,中间件可以传入async函数:
app.use(async function (next){
await next;
this.body = body;
});
但 koa 默认是不支持 es7 的,如果想支持,需要在代码中明确指定 this.experimental = true
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
在 callback 中输出错误信息:
app.callback = function(){
if (this.experimental) {
console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
}
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
//省略
};
compose 的全名叫 koa-compose,它的作用是把一个个不相干的中间件串联在一起:
// 有3个中间件
this.middlewares = [function *m1() {}, function *m2() {}, function *m3() {}];
// 通过compose转换
var middleware = compose(this.middlewares);
// 转换后得到的middleware是这个样子的
function *() {
yield *m1(m2(m3(noop())))
}
从上述的 use 的实现可知,由于 use 的每次调用均会返回 this,因而可以进行链式调用:
app.use(function *m1() {}).use(function *m2() {}).use(function *m3() {})
路由处理
koa自身有 request 对象和 response 对象来处理路由,一个简单的路由处理如下:
app.use(function* () {
if(this.path == '/'){
this.body = 'hello koa';
} else if(this.path == '/get'){
this.body = 'get';
} else {
this.body = '404';
}
});
也可以通过 this.request.headers 来获取请求头。由于没有对响应头做设置,默认响应头类型是 text/plain,可以通过 response.set来设置:
app.use(function* (next) {
if(this.path == '/'){
this.body = 'hello koa';
} else if(this.path == '/get'){
this.body = 'get';
} else {
yield next;
}
});
app.use(function* () {
this.response.set('content-type', 'application/json;charset=utf-8');
return this.body = {message: 'ok', statusCode: 200};
});
上面代码中,每一个中间件负责部分路径,如果路径不符合,就传递给下一个中间件。
复杂的路由需要安装 koa-router:
var app = require('koa')();
var Router = require('koa-router');
var myRouter = new Router();
myRouter.get('/', function *(next) {
this.response.body = 'Hello World!';
});
app.use(myRouter.routes());
app.listen(4000);
由于 koa 使用 generator 作为中间件,所以 myRouter.routes() 返回的是一个 generator,并等同于 myRouter.middleware:
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function *dispatch(next) {
//code
}
//省略
return dispatch;
};
koa-router 提供了一系列于 HTTP 动词对应的方法:
router.get()
router.post()
router.put()
router.del()
router.patch()
del 是 delete 的别名:
// Alias for `router.delete()` because delete is a reserved word
Router.prototype.del = Router.prototype['delete'];
这些动词方法可以接受两个参数,第一个是路径模式,第二个是对应的控制器方法(中间件),定义用户请求该路径时服务器行为。
注意,路径匹配的时候,不会把查询字符串考虑在内。比如,/index?param=xyz 匹配路径 /index。
关于 koa-router 的更多细节,且听下回分解。
链式调用
在 koa 中,对中间件的使用是支持链接调用的。同样,
对于多个路径的请求,koa-router 也支持链式调用:
router
.get('/', function *(next) {
this.body = 'Hello World!';
})
.post('/users', function *(next) {
// ...
})
.put('/users/:id', function *(next) {
// ...
})
.del('/users/:id', function *(next) {
// ...
});
因为每个动词方法都会返回router本身:
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
if (typeof path === 'string' || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
this.register(path, [method], middleware, {
name: name
});
return this;
};
});
路由实现
Node 本身提供了数十个 HTTP 请求动词,koa-router 只是实现了部分常用的:
function Router(opts) {
if (!(this instanceof Router)) {
return new Router(opts);
}
this.opts = opts || {};
this.methods = this.opts.methods || [
'HEAD',
'OPTIONS',
'GET',
'PUT',
'PATCH',
'POST',
'DELETE'
];
//省略
};
这些请求动词的实现是通过第三方模块 methods 支持的,然后 koa-router 内部进行了注册处理:
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
//见上述代码
this.register(path, [method], middleware, {
name: name
});
return this;
};
});
this.register 接受请求路径,方法,中间件作为参数,返回已经注册的路由:
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
var stack = this.stack;
// create route
var route = new Layer(path, methods, middleware, {
//Layer是具体实现,包括匹配、中间件处理等
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
});
//other code
return route;
};
由上述代码可知,koa-router 是支持中间件来处理路由的:
myRouter.use(function* (next) {
console.log('aaaaaa');
yield next;
});
myRouter.use(function* (next) {
console.log('bbbbbb');
yield next;
});
myRouter.get('/', function *(next) {
console.log('ccccccc');
this.response.body = 'Hello World!';
});
myRouter.get('/test', function *(next) {
console.log('dddddd');
this.response.body = 'test router middleware';
});
通过 router.use 来注册中间件,中间件会按照顺序执行,并会在匹配的路由的回调之前调用:
router middleware
对于不匹配的路由则不会调用。同时,如果注册的路由少了 yield next, 则之后的中间件以及被匹配的路由的回调就不会被调用;路由的中间件也是支持链接调用的:
Router.prototype.use = function () {
var router = this;
//other code
return this;
};
中间件也支持特定路由和数组路由:
// session middleware will run before authorize
router
.use(session())
.use(authorize());
// use middleware only with given path
router.use('/users', userAuth());
// or with an array of paths
router.use(['/users', '/admin'], userAuth());
从上述分析可知,对于同一个路由,能用多个中间件处理:
router.get(
'/users/:id',
function (ctx, next) {
return User.findOne(ctx.params.id).then(function(user) {
ctx.user = user;
return next();
});
},
function (ctx) {
console.log(ctx.user);
// => { id: 17, name: "Alex" }
}
);
这样的写法看起来会更紧凑。
路由前缀
Koa-router允许为路径统一添加前缀:
var myRouter = new Router({
prefix: '/koa'
});
// 等同于"/koa"
myRouter.get('/', function* () {
this.response.body = 'koa router';
});
// 等同于"/koa/:id"
myRouter.get('/:id', function* () {
this.response.body = 'koa router-1';
});
也可以在路由初始化后设置统一的前缀,koa-router 提供了 prefix 方法:
Router.prototype.prefix = function (prefix) {
prefix = prefix.replace(//$/, '');
this.opts.prefix = prefix;
this.stack.forEach(function (route) {
route.setPrefix(prefix);
});
return this;
};
所以,以下代码是和上述等价的:
var myRouter = new Router();
myRouter.prefix('/koa');
// 等同于"/koa"
myRouter.get('/', function* () {
this.response.body = 'koa router';
});
// 等同于"/koa/:id"
myRouter.get('/:id', function* () {
this.response.body = 'koa router-1';
});
参数处理和重定向
路径的参数通过 this.params 属性获取,该属性返回一个对象,所有路径参数都是该对象的成员:
// 访问 /programming/how-to-node
router.get('/:category/:title', function *(next) {
console.log(this.params);
// => { category: 'programming', title: 'how-to-node' }
});
param 方法可以对参数设置条件,可用于常规验证和自动加载的验证:
router
.get('/users/:user', function *(next) {
this.body = this.user;
})
.param('user', function *(id, next) {
var users = [ '0号用户', '1号用户', '2号用户'];
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
param 接受两个参数:路由的参数和处理参数的中间件:
Router.prototype.param = function (param, middleware) {
this.params[param] = middleware;
this.stack.forEach(function (route) {
route.param(param, middleware);
});
return this;
};
如果 /users/:user 的参数 user 对应的不是有效用户(比如访问 /users/3),param 方法注册的中间件会查到,就会返回404错误。
也可以将参数验证不通过的路由通过 redirect 重定向到另一个路径,并返回301状态码:
router.redirect('/login', 'sign-in');
// 等同于
router.all('/login', function *() {
this.redirect('/sign-in');
this.status = 301;
});
all 是一个私有方法,会处理某路由的所有的动词请求,相当于一个中间件。如果在 all 之前或者之后出现了处理同一个路由的动词方法,则要调用 yield next,否则另一个就不会执行:
myRouter.get('/login',function* (next) {
this.body = 'login';
// 没有yield next,all不会执行
yield next;
}).get('/sign',function* () {
this.body = 'sign';
}).all('/login',function* () {
console.log('login');
});
myRouter.get('/sign2',function* () {
this.body = 'sign';
}).all('/login2',function* () {
console.log('login2');
//没有yield next,get不会执行
yield next;
}).get('/login2',function* (next) {
this.body = 'login';
});
redirect 方法的第一个参数是请求来源,第二个参数是目的地,两者都可以用路径模式的别名代替,还有第三个参数是状态码,默认是 301:
Router.prototype.redirect = function (source, destination, code) {
// lookup source route by name
if (source[0] !== '/') {
source = this.url(source);
}
// lookup destination route by name
if (destination[0] !== '/') {
destination = this.url(destination);
}
return this.all(source, function *() {
this.redirect(destination);
this.status = code || 301;
});
};
命名路由和嵌套路由
对于非常复杂的路由,koa-router 支持给复杂的路径模式起别名。别名作为第一个参数传递给动词方法:
router.get('user', '/users/:id', function *(next) {
// ...
});
然后可以通过 url 实例方法来生成路由:
router.url('user', 3);
// => "/users/3"
//等价于
router.url('user', { id: 3 });
//=> 'users/3'
该方法接收两个参数:路由别名和参数对象:
Router.prototype.url = function (name, params) {
var route = this.route(name);
if (route) {
var args = Array.prototype.slice.call(arguments, 1);
return route.url.apply(route, args);
}
return new Error("No route found for name: " + name);
};
第一个参数用于 route 方式查找匹配的别名,找到则返回 true,否则返回 false:
Router.prototype.route = function (name) {
var routes = this.stack; //路由别名
for (var len = routes.length, i=0; i
return routes[i];
}
}
return false;
};
除了实例方法 url 外,koa-router 还提供一个静态的方法 url 生成路由:
var url = Router.url('/users/:id', {id: 1});
// => "/users/1"
第一个参数是路径模式,第二个参数是参数对象。
除了给路由命名,koa-router 还支持路由嵌套处理:
var forums = new Router();
var posts = new Router();
posts.get('/', function (ctx, next) {...});
posts.get('/:pid', function (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());