【點擊查看文中的相關源碼】
根據官網的介紹,Koa 是一個新的 Web 框架,致力於成為 Web 應用和 API 開發領域中的一個更小、更富有表現力和更健壯的基石。
通過 async 函數,Koa 不僅遠離回調地獄,同時還有力地增強了錯誤處理。而且,一個關鍵的設計點是在其低級中間件層中提供了高級“語法糖”,這包括諸如內容協商,緩存清理,代理支持和重定向等常見任務的方法。
基礎
實際上,我們常見的一些 Web 框架都是通過使用 Http 模塊來創建了一個服務,在請求到來時通過一系列的處理後把結果返回給前台,事實上 Koa 內部大致也是如此。
通過查看源碼不難發現 Koa 主要分為四個部分:應用程序、上下文、請求對象和響應對象,當我們引入 Koa 時實際上就是拿到了負責創建應用程序的這個類。
我們先來看一下一個簡單的 Hello World 應用:
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
ctx.body = 'Hello World'
})
app.listen(3000, () => console.log('The app is running on localhost:3000'))
運行上面的代碼並訪問 http://localhost:3000/,一個簡單的應用就這樣創建好了。
實現
根據上面的使用方式我們可以很容易的想到下面的實現:
const http = require('http')
module.exports = class Application {
use(fn) {
this.middleware = fn
}
callback() {
const handleRequest = (req, res) => {
this.middleware(req, res)
}
return handleRequest
}
listen(...args) {
const server = http.createServer(this.callback())
return server.listen(...args)
}
}
在上面的例子中,中間件得到的參數還是原生的請求和響應對象。按照 Koa 的實現,現在我們需要創建一個貫穿整個請求的上下文對象,上下文中包括了原生的和封裝的請求、響應對象。
// request.js
module.exports = {}
// response.js
module.exports = {}
// context.js
module.exports = {}
// application.js
const http = require('http')
const request = require('./request')
const response = require('./response')
const context = require('./context')
module.exports = class Application {
constructor() {
// 確保每個實例都擁有自己的 request response context 三個對象
this.request = Object.create(request)
this.response = Object.create(response)
this.context = Object.create(context)
}
createContext() {
// ...
}
callback() {
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
this.middleware(ctx)
}
return handleRequest
}
}
在上面我們創建了三個對象並放置到了應用的實例上面,最後將創建好的上下文對象傳遞給中間件。在創建上下文的函數中首先要處理的就是請求、響應等幾個對象之間的關係:
module.exports = class Application {
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
return context
}
}
其中上下文上的 request 和 response 是我們後面要進一步封裝的請求和響應對象,而 req 和 res 則是原生的請求和響應對象。
Context
如上,在每一次收到用户請求時都會創建一個 Context 對象,這個對象封裝了這次用户請求的信息,並提供了許多便捷的方法來獲取請求參數或者設置響應信息。
除了自行封裝的一些屬性和方法外,其中也有許多屬性和方法都是通過代理的方式獲取的請求和響應對象上的值。
const delegate = require('delegates')
const context = (module.exports = {
onerror(err) {
const msg = err.stack || err.toString()
console.error(msg)
},
})
delegate(context, 'response')
// ...
.access('body')
delegate(context, 'request')
.method('get')
// ...
.access('method')
這裏我們看到的 delegates 模塊是由大名鼎鼎的 TJ 所寫的,利用委託模式,它使得外層暴露的對象將請求委託給內部的其他對象進行處理。
Delegator
接下來我們來看看delegates 模塊中的核心邏輯。
function Delegator(proto, target) {
if (!(this instanceof Delegator)) return new Delegator(proto, target)
this.proto = proto
this.target = target
}
Delegator.prototype.method = function(name) {
const proto = this.proto
const target = this.target
// 調用時這裏的 this 就是上下文對象,target 則是 request 或 response
// 所以,最終都會交給請求對象或響應對象上的方法去處理
proto[name] = function() {
return this[target][name].apply(this[target], arguments)
}
return this
}
Delegator.prototype.access = function(name) {
return this.getter(name).setter(name)
}
Delegator.prototype.getter = function(name) {
const proto = this.proto
const target = this.target
// __defineGetter__ 方法可以為一個已經存在的對象設置(新建或修改)訪問器屬性
proto.__defineGetter__(name, function() {
return this[target][name]
})
return this
}
Delegator.prototype.setter = function(name) {
const proto = this.proto
const target = this.target
// __defineSetter__ 方法可以將一個函數綁定在當前對象的指定屬性上,當那個屬性被賦值時,綁定的函數就會被調用
proto.__defineSetter__(name, function(val) {
return (this[target][name] = val)
})
return this
}
module.exports = Delegator
通過 method 方法在上下文上創建指定的函數,調用時會對應調用請求對象或響應對象上的方法進行處理,而對於一些普通屬性的讀寫則直接通過__defineGetter__ 和 __defineSetter__ 方法來進行代理。
Request
Request 是一個請求級別的對象,封裝了 Node.js 原生的 HTTP Request 對象,提供了一系列輔助方法獲取 HTTP 請求常用參數。
module.exports = {
get method() {
// 直接獲取原生請求對象上對應的屬性
return this.req.method
},
set method(val) {
this.req.method = val
},
}
和請求上下文對象類似,請求對象上除了會封裝一些常見的屬性和方法外,也會去直接讀取並返回一些原生請求對象上對應屬性的值。
Response
Response 是一個請求級別的對象,封裝了 Node.js 原生的 HTTP Response 對象,提供了一系列輔助方法設置 HTTP 響應。
module.exports = {
get body() {
return this._body
},
set body(val) {
// 省略了詳細的處理邏輯
this._body = val
},
}
其中的處理方式和請求對象的處理類似。
中間件
和 Express 不同,Koa 的中間件選擇了洋葱圈模型,所有的請求經過一箇中間件的時候都會執行兩次,這樣可以非常方便的實現後置處理邏輯。
function compose(middlewares) {
return function(ctx) {
const dispatch = (i = 0) => {
const middleware = middlewares[i]
if (i === middlewares.length) {
return Promise.resolve()
}
return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
}
return dispatch()
}
}
module.exports = compose
Koa 的中間件處理被單獨的放在了 koa-compose 模塊中,上面是插件處理的主要邏輯,核心思想就是將調用下一個插件的函數通過回調的方式交給當前正在執行的中間件。
存在的一個問題是,開發者可能會多次調用執行下箇中間件的函數(next),為此我們可以添加一個標識:
function compose(middlewares) {
return function(ctx) {
let index = -1
const dispatch = (i = 0) => {
if (i <= index) {
return Promise.reject(new Error('next() called multiple times'))
}
index = i
const middleware = middlewares[i]
if (i === middlewares.length) {
return Promise.resolve()
}
return Promise.resolve(middleware(ctx, () => dispatch(i + 1)))
}
return dispatch()
}
}
module.exports = compose
由於在每一個 dispatch 函數(也就是中間件中的 next 函數)中 i 的值是固定的,在調用一次後它的值就和 index 的值相等了,再次調用就會報錯。
Application
Application 是全局應用對象,在一個應用中,只會實例化一個,在它上面我們建立了幾個對象之間的關係,同時還會負責組織上面提到的插件。
另外,之前我們的 use 方法直接將指定的插件賦值給了 middleware,可是這樣只能有一個插件,因此我們需要改變一下,維護一個數組。
const compose = require('../koa-compose')
module.exports = class Application {
constructor() {
// ...
this.middleware = []
}
use(fn) {
this.middleware.push(fn)
}
callback() {
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
fn(ctx)
}
return handleRequest
}
}
目前為止,我們基本已經完成了本次請求的處理,但並沒有完成響應,我們還需要在最後返回 ctx.body 上的數據。
module.exports = class Application {
callback() {
const fn = compose(this.middleware)
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res)
this.handleRequest(ctx, fn)
}
return handleRequest
}
handleRequest(ctx, fnMiddleware) {
const onerror = err => ctx.onerror(err)
const handleResponse = () => respond(ctx)
return fnMiddleware(ctx)
.then(handleResponse)
.catch(onerror)
}
}
function respond(ctx) {
ctx.res.end(ctx.body)
}
現在一個基礎的 Koa 就算實現了。
其它
這裏寫下的實現也只是提供一個思路,歡迎大家一起交流學習。
輕拍【滑稽】。。。