Koa (koajs)源码解读(高手必看)

作者:袖梨 2022-06-25

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     if (routes[i].name && routes[i].name === name) {
      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());

相关文章

精彩推荐