koa源碼部分解析(1)本體

koa自身的代碼比較少,但是有很多依賴的npm包,有不少是在jshttp這個project裏(比如on-finished)。

版本2.7.0

先來看一個使用koa的例子

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

function render(page) {
  return new Promise((resolve, reject) => {
    let viewUrl = `./view/${page}`;
    fs.readFile(viewUrl, 'binary', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

async function route(url) {
  let view = '404.html';
  switch (url) {
    case '/':
      view = 'index.html';
      break;
    case '/index':
      view = 'index.html';
      break;
    case '/404':
      view = '404.html';
      break;
    default:
      break;
  }
  let html = await render(view);
  return html;
}

app.use(async ctx => {
  let url = ctx.request.url;
  let html = await route(url);
  ctx.body = html;
});

app.listen(3000);

比較簡單,應該都能看得懂。這一段簡單的代碼涵蓋了一些平時不太注意的點,比如app.listen、ctx.body、app.use等,基本上講完這幾個,koa的本體就差不多了。

koa一共就4個文件,大家都知道,上面提到的app開頭的函數都在application.js裏,ctx.body在response.js裏,我們一個一個來看

1.application.js

app繼承自node的EventEmitter類,app的構造函數中引用了其他三個文件,並創建了成員變量(其實後面只有一處用到)

this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);

listen函數用node的http模塊的createServer函數創建了server實例,並進行監聽

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

調用createServer的時候,傳入了this.callback(),我們來看一下。

callback返回了一個函數,這個函數先調用了createContext,然後返回了handleRequest成員函數

callback() {
		// 處理中間件,等一下看compose和this.middleware
    const fn = compose(this.middleware);
		// 錯誤處理,listenerCount是EventEmitter類的函數
    if (!this.listenerCount('error')) this.on('error', this.onerror);
		// 傳遞給createServer的就是下面這個函數
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }
  
handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    // 這裏等到response再看
    const handleResponse = () => respond(ctx);
    // 給請求結束增加一個回調,這個onerror是ctx的onerror,不是app的onerror
    onFinished(res, onerror);
    // 等一下看這個
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

createContext(req, res) {
	// 每次請求,ctx都是一個新的對象
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    // 原生的req和res
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    // koa生成的request和response
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

看到這裏app就剩下middleware了,middleware的輸入在app.use裏,只是一個push(如果是用generator寫的,會先經過koa-convert轉換),然後就是callback中koa-compose的處理,koa-compose就是將傳入的數組形式的中間處理函數都用Promise包了,然後組織成一層套一層的形式,可以自己看一下。

2.context.js

提供了一個相對具體的onerror,一個cookie讀寫屬性,另外全是代理了request和response的方法和屬性,代理方法是使用delegates這個包,這個包的核心是__defineGetter____defineSetter__這兩個方法,當然現在都建議用Object.defineProperty了。

3.request

沒什麼和流程相關的代碼,基本都是和http請求相關的內容,可以掃一眼,遇到問題了再看

4.response

body和set可以看一看。

set用的是setHeader方法,和writeHeader有挺多不一樣的地方,具體可以查閱node文檔。

body是個容易迷惑的地方,body從頭看到爲會發現最後也沒調用res.end或者res.write,其實是在app裏調用的,就是上面那個說到response再看的地方,自己看一下respond函數,會發現是調用了res.end方法。

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

  if (!ctx.writable) return;

  const res = ctx.res;
  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) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);
}

=====這些是我寫了koa-router之後回來寫的
koa的本體代碼不多,但是看了它的中間件以後發現還是需要了解一下哪些能力是本體提供的,有時候中間件調了一下ctx.xxx你都不知道這個xxx是哪裏來的,所以一上來可以不用把本體看的特別熟悉,遇到了問題可以再回頭來看,順便所有的中間件都存到了middleware裏面,調試的時候很方便查看

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章