dwqs / dwqs/blog

koa源码解读

Open
#9 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
JavaScript
Stars
3.8k
Forks
443
PR merge metrics
No merged PRs in 30d

Description

[Koa](https://github.com/koajs/koa/) 是一个类似于 [Express](https://github.com/expressjs/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](http://es6.ruanyifeng.com/#docs/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](https://github.com/alexmingoia/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框架](http://javascript.ruanyifeng.com/nodejs/koa.html)
[koa的中间件机制](http://syaning.com/2015/11/02/koa-middleware/)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading the issue body and the referenced Koa and Express documentation. The repository files, tests, and a concrete documentation change are not identified, so the desired wording, scope, and definition of done must be clarified before work can begin.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
documentation
Issue type
Documentation
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
15/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.