Koa2 源碼解析(1)


Koa2 源碼解析

其實本來不想寫這個系列文章的,因為Koa本身很精簡,一共就4個文件,千十來行代碼。
但是因為想寫 egg[1] 的源碼解析,而egg是基於Koa2的,所以就先寫個Koa2的吧,用作承上啟下。

[1] egg 是阿里巴巴團隊開源的企業級web開發框架

面向讀者

我們假定讀者具備javascript基礎知識,簡單了解promise、generator和async。

入口

我們以 koajs中文官網 的例子作為入口。

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

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

這樣就啟動起來了一個Koa2網站,可以看到只做了3件事: Koa的構造函數、app實例的use函數、app實例的listen函數。

查看Koa源碼的package.json文件得知,默認入口是 application.js文件,也就是上面代碼的Koa,那么讓我們來看看里面是什么?

Application.js

module.exports = class Application extends Emitter {

我們可以看到Application是繼承自 Event 模塊的事件監聽器,並且 Koa2 已經使用了 ES6 的 class 語法。

構造函數

constructor() {
  super();

  this.proxy = false;
  this.middleware = [];
  this.subdomainOffset = 2;
  this.env = process.env.NODE_ENV || 'development';
  this.context = Object.create(context);
  this.request = Object.create(request);
  this.response = Object.create(response);
}

下面來看看構造函數,也沒什么稀奇的。
調用父類的構造函數,然后4個屬性的初始化,我們先不管它們都是干什么的。

接下來是創建3個對象context、request和response,其實這就是koa2的核心了,構造出的context表示本次請求的上下文,request和response這個大家都知道。

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/tree/v2.x#old-signature-middleware-v1x---deprecated');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  this.middleware.push(fn);
  return this;
}

use函數更簡單:判斷是不是function,判斷是不是generator,如果是generator那么轉換一下,將fn放入middleware數組。

listen函數

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

listen看似也沒什么,其實不然,獲取createServer的callback函數是個核心的東西,我們來看下

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

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

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

首先,把所有middleware進行了組合,使用了koa-compose,我們也不用去管他的內部實現,簡單來說就是返回了一個promise數組的遞歸調用。

然后,我們看看這個匿名函數,把http code默認設置為404,接着利用createContext函數把node返回的req和res進行了組合創建出context,可以看看createContext函數

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

這里面都是一堆的組合和賦值,例如把req掛到context下面啦、把request掛到reponse下面啦、獲取下accept啦、ip啦。

回頭來看看創建完context又干了些什么

const onerror = err => ctx.onerror(err);
onFinished(res, onerror);
fn(ctx).then(() => respond(ctx)).catch(onerror);

前兩行沒什么,給res注冊了一個onerror事件,然后第三行就是具體所有middleware的執行了,所有middleware都執行完后,調用respond(ctx)函數

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

這個respond函數里面也不過是一些收尾工作,例如判斷http code為空如何輸出啦,http method是head如何輸出啦,body返回是流或json時如何輸出啦。

完事,你看koa是不是很簡單,只不過是把res和req組合成了context,並提供了一些便利的函數,以及最重要的把middleware promise化了,寫異步更爽了,加上es2017的async/await語法,跟C#也很像了。

接下來我們還會對context、request和response對象進行一番解析,敬請期待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM