koa原理淺析

koa原理淺析

選取的版本爲koa2

原文連接

koa的源碼由四個文件組成git

application.js    koa的骨架
context.js        ctx的原型
request.js        request的原型
response.js       response的原型

基本用法

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

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

app.listen(3000);

初始服務器

利用http模塊建立服務器github

const app = http.createServer((req, res) => {
    ...
})  
app.listen(3000)

事實上koa把這些包在了其listen方法中json

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

顯然this.callback()返回的是一個形以下面的函數promise

(req, res) => {}

上下文ctx

callback方法以下服務器

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

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

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

ctx在koa中事實上是一個包裝了request和response的對象,從createContext中能夠看到起繼承自contextcookie

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;
  }

能夠看到ctx.request繼承自request,ctx.response繼承自response,查看response和request能夠看到裏面大都是set和get方法(獲取query,設置header)等等。而且ctx代理了ctx.request和ctx.response的方法,在源碼中能夠看到app

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');

因此咱們能夠直接這麼寫dom

ctx.url

等價於

ctx.request.url

中間件

咱們再看一下callback函數,觀察發現compose模塊十分的神奇,我暫且把它稱爲是一個迭代器,它實現了中間件的順序執行koa

const fn = compose(this.middleware);

打印fn以下

  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)
      }
    }
  }

最初接觸koa的時候我疑惑爲何我寫了socket

ctx.body = 'hello world'

並無ctx.response.end()之類的方法,事實上koa已經幫咱們作了處理,在handleRequest方法中

const handleResponse = () => respond(ctx);

// fnMiddleware即爲上面compose以後的fn
fnMiddleware(ctx).then(handleResponse).catch(onerror)

fnMiddleware返回的是一個promise,在中間件邏輯完成後在respond函數中最終去處理ctx.body

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

錯誤處理

  • (非首部)中間件層處理(我瞎起的)

對於每一箇中間件可能發生的錯誤,能夠直接在該中間件捕獲

app.use((ctx, next) => {

    try {
        ...        
    } catch(err) {
        ...
    }

})
  • (首部)中間件層處理

事實上,咱們只要在第一個中間件添加try... catch... ,整個中間件組的錯誤都是能夠捕獲的到的。

  • (應用級別)頂層處理
app.on('error', (err) = {})

在上面中間件執行時看到,koa會自動幫咱們捕獲錯誤並處理,以下

try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        //  捕獲錯誤
        return Promise.reject(err)
      }


//  在ctx.onerror中處理
const onerror = err => ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror)

咱們看ctx.onerror發現它事實上是出發app監聽的error事件

onerror(err) {


// delegate
    this.app.emit('error', err, this);

假如咱們沒有定義error回調怎麼辦呢,koa也爲咱們定義了默認的錯誤處理函數

callback方法作了判斷

callback() {

    ...

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

    ...
  }

全文完

相關文章
相關標籤/搜索