Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa源码解读 #9

Open
dwqs opened this issue Jul 30, 2016 · 0 comments
Open

koa源码解读 #9

dwqs opened this issue Jul 30, 2016 · 0 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Jul 30, 2016

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.requestctx.response 返回的是 koa 的对应对象,ctx.reqctx.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()

deldelete 的别名:

// Alias for `router.delete()` because delete is a reserved word
Router.prototype.del = Router.prototype['delete'];

这些动词方法可以接受两个参数,第一个是路径模式,第二个是对应的控制器方法(中间件),定义用户请求该路径时服务器行为。

注意,路径匹配的时候,不会把查询字符串考虑在内。比如,/index?param=xyz 匹配路径 /index

关于 koa-router 的更多细节,且听下回分解。

相关阅读

Koa框架
koa的中间件机制

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant