1. 程式人生 > 程式設計 >Koa 梳理分析【一:koa 例項】

Koa 梳理分析【一:koa 例項】

之前梳理Redux的時候,說到它的中介軟體的處理方式與koa是一樣的,所以就想到那就把koa也看一遍吧,梳理一遍吧。koa非常的簡潔適合閱讀,該文章分析的當前版本為2.8.1

目錄結構

通過github上的package.json可以看到,koa的入口檔案是lib/application.js。整個lib才四個檔案,當然裡面引入了一些其他的工具函式。

── lib
   ├── application.js
   ├── context.js
   ├── request.js
   └── response.js
複製程式碼

下面從程式入口出發,也就是application.js

application.js

先看一下關鍵的相關依賴:

const response = require('./response');
const context = require('./context');
const request = require('./request');
const compose = require('koa-compose');
const http = require('http');
const Emitter = require('events');
...
複製程式碼

可以看到,入口檔案把其他三個js檔案都引入進來,然後主要引入了httpkoa-composeevents,其他的我先省略了。koa-compose

庫主要是為了對koa的中介軟體進行合併的工具函式,其他兩個都是node的標準庫。

該檔案使用module.exports = class Application extends Emitter {...} 匯出了koa類。這裡看到Application 繼承了 Emitter,所有它也包含了非同步事件的處理能力,後面可以看到koa的錯誤處理會使用到Emitter提供的事件模型方法。

建構函式

constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false
; this.subdomainOffset = options.subdomainOffset || 2; this.env = options.env || process.env.NODE_ENV || 'development'; if (options.keys) this.keys = options.keys; this.middleware = []; this.context = Object.create(context); this.request = Object.create(request); this.response = Object.create(response); if (util.inspect.custom) { this[util.inspect.custom] = this.inspect; } } 複製程式碼

重點可以看到:

  • 宣告和初始化了 middleware 屬性,是用來存放之後新增的中介軟體的。
  • 使用Object.create方法,分別建立了一個物件,這些物件的原型分別指向contextrequestresponse,分別對應最開始引入的其他三個js檔案。

到這裡,先寫一份使用koa的使用的示例程式碼,主要引導整個處理流程:

const Koa = require('koa')

const app = new Koa()

app.use(async (ctx,next) => {
  console.log('@@ start 1')
  next()
  console.log('@@ end 1')
})

app.use(async (ctx,next) => {
  console.log('@@ start 2')
  next()
  console.log('@@ end 2')
})

app.listen(3000)
複製程式碼

use

從上往下,當執行new Koa()的時候,也就是呼叫上面說的建構函式。來到app.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;
}
複製程式碼

use函式會判斷傳入的中介軟體如果是generator,就會使用convert函式去將生成器轉成類async/await函式,這個相容會在3.*之後去掉。核心的一步就是將函式新增到了this.middleware佇列裡面。

listen

這裡就是正式的使用http庫提供的createServer方法來建立一個web服務了,並且監聽相應的埠。

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
複製程式碼

根據http.createServer的檔案和使用,可以肯定這裡的this.callback()函式執行會返回一個 (req,res) => {} 這樣的函式。

callback

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

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

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

    return handleRequest;
}
複製程式碼

首先就將已經註冊的中介軟體進行了合併,這裡就是經典的洋蔥模型中介軟體機制。然後判斷了一下是否有過錯誤監聽,沒有的話就新增一個,這裡利用了Emitter的事件模型,最後返回一個http.createServer能夠使用的回撥函式。

到這裡,整個服務的啟動流程已經結束。

處理請求

當服務收到請求,最後執行的就是傳入http.createServer的回撥函式。

handleRequest = (req,fn);
};
複製程式碼

在處理請求之前,首先使用this.createContext將請求和響應物件進行了聚合封裝成了ctx物件,然後再交給handleRequest函式去處理。

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.state = {};
    return context;
}
複製程式碼

首先通過Object.create函式去建立了一個原型為this.context的空物件,之後就是為這個物件賦值了,可以看到在平時使用的時候訪問的一些屬性是怎麼來的了,他們之間的關係是怎麼樣的可以很清楚的看見。

handleRequest

handleRequest(ctx,fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res,onerror); // 當http的請求關閉,出現錯誤的時候,執行回撥。
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
複製程式碼

預設給響應狀態設定為404,然後建立一個統一錯誤處理的回撥函式和響應處理函式函式。將ctx傳給被合併後的中介軟體,然後使用thencatch分別來處理中介軟體等正常處理和異常監控。

respond

當請求經過了所有的中介軟體處理之後,在最後呼叫handleResponse方法,然後去執行respond函式,最終組織響應物件,進行服務響應。

function respond(ctx) {
  ...
  // 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);
}
複製程式碼

看到body的型別支援BufferstringStreamJSON

小結

通過梳理application.js可以知道,它核心主要是做了這樣幾個事情:

  1. 通過http啟動web服務,建立koa例項。
  2. 處理合並中介軟體為洋蔥模型。
  3. 建立和封裝高內聚的context。
  4. 實現非同步函式的統一錯誤處理機制。

參考文章 可能是目前最全的koa原始碼解析指南 on-finished

原文連結,歡迎star