IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    koa源码解读

    dwqs发表于 2016-04-02 10:12:57
    love 0

    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的中间件机制

    转载请注明:淡忘~浅思 » koa源码解读



沪ICP备19023445号-2号
友情链接