Koa源码分析

最近一直都在开发基于node的前后端项目,分享一下Koa的源码。

Koa自己的说法是next generation web framework for node.js

Koa算是比较主流的node的web框架了,前身是express。相比于express,koa去除了多余的middleware,只留下了最基本的对node的网络模块的继承和封装,并且提供了方便的中间件调用机制,Koa的源码总共加起来就1600+,很快就可以看完。

基础知识

在分析koa的源码之前需要先了解一下node的http模块。

const http = require('http');
const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader(‘Content-Type’, ‘text/plain’);
  res.end(‘Hello World’);
}
server.listen(3000);

node的http模块主要负责了node对HTTP处理的封装。以上这段代码启动了一个监听3000端口的web server,并且返回'Hello World'给接收到的请求。

每一次接收到一个新的请求的时候会调用回调函数,参数req和res分别是请求的实体和返回的实体,操作req可以获取收到的请求,操作res对应的是将要返回的packet。

如果你需要对接收到的请求进行一系列处理的话,则需要按顺序写在回调函数里面。

同样的功能对应的Koa的写法如下:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

这里的user实际上是Koa的中间件机制提供的一个方便的处理请求的接口。请求会被use后面的函数依次按照use的顺序被处理,通常称这些函数为中间件,他们的参数ctx为koa基于node的http模块的req和res封装的一个对象,集合了req和res的功能为一体,并且增加了一些简单的操作。可以通过ctx.req和ctx.res获取到原生的req和res,与此同时ctx.request和ctx.response是Koa基于req和res封装的拥有一些新的功能的请求和返回的实体。 以下代码是Koa中间件使用的栗子:

const Koa = require('koa');
const app = new Koa();

app.use(async (ctx, next) => {
  console.log(‘pre1’)
  await next();
  console.log(‘post1’);
});
app.use(async (ctx, next) => {
  console.log(‘pre2’);
  await next();
  console.log(‘post2’)
});
app.use(async ctx => {
  console.log(‘pre3’)
  ctx.body = 'Hello World';
  console.log(‘post3’);
});

app.listen(3000);

next()表示将请求的处理交给下一个中间件。如果没有next(),在该中间件函数执行结束后,将返回执行上一个中间件的next()后续的内容直到最开始的中间件的next()后面的内容执行完毕。

上面的代码的结果是

pre1
pre2
pre3
post3
post2
post1

执行的结果和函数递归调用何其相似,之后了解了Koa的中间件机制后自然会明白这个结果的原因。

正题

在了解了koa的基本的使用和带着以上中间件执行的结果,我们来看看koa的源码吧。

对Koa的理解主要分为两个部分:

  • Koa对node的http模块的封装
  • Koa的中间件机制

按照【栗子】代码的顺序从上到下:

初始化Koa

const app = new Koa();

使用use加载中间件

对应的源码中

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

将中间件函数push到middleware数组中。

调用listen方法启动web server

listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

回调函数:

callback() {
  const fn = compose(this.middleware);

  if (!this.listeners('error').length) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    res.statusCode = 404;
    const ctx = this.createContext(req, res);
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fn(ctx).then(handleResponse).catch(onerror);
  };

  return handleRequest;
}

这里的compose函数是实现Koa的中间件机制的地方之后再细说。

Koa对node的http模块的封装

const ctx = this.createContext(req, res);

createContext(req, res) {
   const context = Object.create(this.context);
   const request = context.request = Object.create(this.request);
   const 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.originalUrl = request.originalUrl = req.url;
   context.cookies = new Cookies(req, res, {
     keys: this.keys,
     secure: request.secure
   });
   request.ip = request.ips[0] || req.socket.remoteAddress || '';
   context.accept = request.accept = accepts(req);
   context.state = {};
   return context;
 }

createContext实际上只是创建之前说的集合了req & res & request & response的一个对象,作为参数传递给中间件。 对于request和response其中除了一些拓展的方便的接口之外大部分都是直接继承的http的req和response。 实现的新的接口也是比较简单的封装,举个栗子(response.js):

set status(code) {
  assert('number' == typeof code, 'status code must be a number');
  assert(statuses[code], `invalid status code: ${code}`);
  assert(!this.res.headersSent, 'headers have already been sent');
  this._explicitStatus = true;
  this.res.statusCode = code;
  this.res.statusMessage = statuses[code];
  if (this.body && statuses.empty[code]) this.body = null;
},

/**
 * Get response status message
 *
 * @return {String}
 * @api public
 */

get message() {
  return this.res.statusMessage || statuses[this.status];
}

以上是response中实现的两个新的接口,实际上也就是res的接口再简单封装了一下然后返回。 再看(context.js)的最底部

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

delegate的作用是将对应的对象上的method,getter,setter继承到另一个对象上。 可以看到,直接继承了大部分req和res的方法。

接下来就是看看Koa的中间件机制的实现了。

Koa的中间件机制

compose的源码也是非常简单的:

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以看到其实就是按照顺序将middleware数组中的中间件s按照顺序递归执行,每次执行next()的时候就是执行下一个中间件,最后一个next也就是第一个中间件的第二个参数,因为是undefined,所以会结束递归调用反向依次执行每个中间件next后续的代码。每次return的都是一个Promise对象,因此我们写的时候是await来等待这个异步调用的结束,然后执行下一个中间件。而我们一般的写法是将await next()写在中间件函数的最后,从而用尾递归的方式来实现每个请求依次被中间件函数处理的效果。

恩,Koa的主要的概念就是这些,它的目的就是一个极简的框架,只提供最基本的接口,大部分的功能,开发者根据需求使用use添加中间件来实现。

关注下面的标签,发现更多相似文章
评论
说说你的看法