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
檔案都引入進來,然後主要引入了http
、koa-compose
和events
,其他的我先省略了。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
方法,分別建立了一個物件,這些物件的原型分別指向context
、request
和response
,分別對應最開始引入的其他三個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
傳給被合併後的中介軟體,然後使用then
和catch
分別來處理中介軟體等正常處理和異常監控。
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
的型別支援Buffer
、string
、Stream
和JSON
。
小結
通過梳理application.js
可以知道,它核心主要是做了這樣幾個事情:
- 通過
http
啟動web
服務,建立koa
例項。 - 處理合並中介軟體為洋蔥模型。
- 建立和封裝高內聚的context。
- 實現非同步函式的統一錯誤處理機制。