Koa源碼分析

最近一直都在開發基於node的先後端項目,分享一下Koa的源碼。javascript

Koa本身的說法是next generation web framework for node.jsjava

Koa算是比較主流的node的web框架了,前身是express。相比於express,koa去除了多餘的middleware,只留下了最基本的對node的網絡模塊的繼承和封裝,而且提供了方便的中間件調用機制,Koa的源碼總共加起來就1600+,很快就能夠看完。node

基礎知識

在分析koa的源碼以前須要先了解一下node的http模塊。git

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'給接收到的請求。github

每一次接收到一個新的請求的時候會調用回調函數,參數req和res分別是請求的實體和返回的實體,操做req能夠獲取收到的請求,操做res對應的是將要返回的packet。web

若是你須要對接收到的請求進行一系列處理的話,則須要按順序寫在回調函數裏面。express

一樣的功能對應的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中間件使用的栗子:api

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添加中間件來實現。

相關文章
相關標籤/搜索