跳到主要內容

Koa源碼解析,帶你實現一個迷你版的Koa

前言


本文是我在閱讀 Koa 源碼后,並實現迷你版 Koa 的過程。如果你使用過 Koa 但不知道內部的原理,我想這篇文章應該能夠幫助到你,實現一個迷你版的 Koa 不會很難。


本文會循序漸進的解析內部原理,包括:



  1. 基礎版本的 koa

  2. context 的實現

  3. 中間件原理及實現


文件結構



  • application.js: 入口文件,裡面包括我們常用的 use 方法、listen 方法以及對 ctx.body 做輸出處理

  • context.js: 主要是做屬性和方法的代理,讓用戶能夠更簡便的訪問到requestresponse的屬性和方法

  • request.js: 對原生的 req 屬性做處理,擴展更多可用的屬性和方法,比如:query 屬性、get 方法

  • response.js: 對原生的 res 屬性做處理,擴展更多可用的屬性和方法,比如:status 屬性、set 方法


基礎版本


用法:


const Coa = require('./coa/application')
const app = new Coa()

// 應用中間件
app.use((ctx) => {
ctx.body = '<h1>Hello</h1>'
})

app.listen(3000, '127.0.0.1')

application.js:


const http = require('http')

module.exports = class Coa {
use(fn) {
this.fn = fn
}
// listen 只是語法糖 本身還是使用 http.createServer
listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}
callback() {
const handleRequest = (req, res) => {
// 創建上下文
const ctx = this.createContext(req, res)
// 調用中間件
this.fn(ctx)
// 輸出內容
res.end(ctx.body)
}
return handleRequest
}
createContext(req, res) {
let ctx = {}
ctx.req = req
ctx.res = res
return ctx
}
}

基礎版本的實現很簡單,調用 use 將函數存儲起來,在啟動服務器時再執行這個函數,並輸出 ctx.body 的內容。


但是這樣是沒有靈魂的。接下來,實現 context 和中間件原理,Koa 才算完整。


Context


ctx 為我們擴展了很多好用的屬性和方法,比如 ctx.queryctx.set()。但它們並不是 context 封裝的,而是在訪問 ctx 上的屬性時,它內部通過屬性劫持將 requestresponse 內封裝的屬性返回。就像你訪問 ctx.query,實際上訪問的是 ctx.request.query


說到劫持你可能會想到 Object.defineProperty,在 Kao 內部使用的是 ES6 提供的對象的 settergetter,效果也是一樣的。所以要實現 ctx,我們首先要實現 requestresponse


在此之前,需要修改下 createContext 方法:


// 這三個都是對象
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Coa {
constructor() {
this.context = context
this.request = request
this.response = response
}
createContext(req, res) {
const ctx = Object.create(this.context)
// 將擴展的 request、response 掛載到 ctx 上
// 使用 Object.create 創建以傳入參數為原型的對象,避免添加屬性時因為衝突影響到原對象
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)

ctx.app = request.app = response.app = this;
// 掛載原生屬性
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res

request.ctx = response.ctx = ctx;
request.response = response;
response.request = request;

return ctx
}
}

上面一堆花里胡哨的賦值,是為了能通過多種途徑獲取屬性。比如獲取 query 屬性,可以有 ctx.queryctx.request.queryctx.app.query 等等的方式。


如果你覺得看起來有點冗餘,也可以主要理解這幾行,因為我們實現源碼時也就用到下面這些:


const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)

ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res

request


request.js


const url = require('url')

module.exports = {
/* 查看這兩步操作
* const request = ctx.request = Object.create(this.request)
* ctx.req = request.req = response.req = req
*
* 此時的 this 是指向 ctx,所以這裏的 this.req 訪問的是原生屬性 req
* 同樣,也可以通過 this.request.req 來訪問
*/
// 請求的 query 參數
get query() {
return url.parse(this.req.url).query
},
// 請求的路徑
get path() {
return url.parse(this.req.url).pathname
},
// 請求的方法
get method() {
return this.req.method.toLowerCase()
}
}

response


response.js


module.exports = {
// 這裏的 this.res 也和上面同理
// 返回的狀態碼
get status() {
return this.res.statusCode
},
set status(val) {
return this.res.statusCode = val
},
// 返回的輸出內容
get body() {
return this._body
},
set body(val) {
return this._body = val
},
// 設置頭部
set(filed, val) {
if (typeof filed === 'string') {
this.res.setHeader(filed, val)
}
if (toString.call(filed) === '[object Object]') {
for (const key in filed) {
this.set(key, filed[key])
}
}
}
}

屬性代理


通過上面的實現,我們可以使用 ctx.request.query 來訪問到擴展的屬性。但是在實際應用中,更常用的是 ctx.query。不過 query 是在 request 的屬性,通過 ctx.query 是無法訪問的。


這時只需稍微做個代理,在訪問 ctx.query 時,將 ctx.request.query 返回就可以實現上面的效果。


context.js:


module.exports = {
get query() {
return this.request.query
}
}

實際的代碼中會有很多擴展的屬性,總不可能一個一個去寫吧。為了優雅的代理屬性,Koa 使用 delegates 包實現。這裏我就直接簡單封裝下代理函數,代理函數主要用到__defineGetter____defineSetter__ 兩個方法。


在對象上都會帶有 __defineGetter____defineSetter__,它們可以將一個函數綁定在當前對象的指定屬性上,當屬性被獲取或賦值時,綁定的函數就會被調用。就像這樣:


let obj = {}
let obj1 = {
name: 'JoJo'
}
obj.__defineGetter__('name', function(){
return obj1.name
})

此時訪問 obj.name,獲取到的是 obj1.name 的值。


了解這個兩個方法的用處后,接下來開始修改 context.js


const proto = module.exports = {
}

// getter代理
function delegateGetter(prop, name){
proto.__defineGetter__(name, function(){
return this[prop][name]
})
}
// setter代理
function delegateSetter(prop, name){
proto.__defineSetter__(name, function(val){
return this[prop][name] = val
})
}
// 方法代理
function delegateMethod(prop, name){
proto[name] = function() {
return this[prop][name].apply(this[prop], arguments)
}
}

delegateGetter('request', 'query')
delegateGetter('request', 'path')
delegateGetter('request', 'method')

delegateGetter('response', 'status')
delegateSetter('response', 'status')
delegateGetter('response', 'body')
delegateSetter('response', 'body')
delegateMethod('response', 'set')

中間件原理


中間件思想是 Koa 最精髓的地方,為擴展功能提供很大的幫助。這也是它雖然小,卻很強大的原因。還有一個優點,中間件使功能模塊的職責更加分明,一個功能就是一个中間件,多个中間件組合起來成為一個完整的應用。


下面是著名的"洋蔥模型"。這幅圖很形象的表達了中間件思想的作用,它就像一個流水線一樣,上游加工后的東西傳遞給下游,下游可以繼續接着加工,最終輸出加工結果。



原理分析


在調用 use 註冊中間件的時候,內部會將每个中間件存儲到數組中,執行中間件時,為其提供 next 參數。調用 next 即執行下一个中間件,以此類推。當數組中的中間件執行完畢后,再原路返回。就像這樣:


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

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

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

輸出結果如下:


1 start
2 start
3 start
3 end
2 end
1 end

有點數據結構知識的同學,很快就想到這是一個"棧"結構,執行的順序符合"先入后出"。


下面我將內部中間件實現原理進行簡化,模擬中間件執行:


function next1() {
console.log('1 start')
next2()
console.log('1 end')
}
function next2() {
console.log('2 start')
next3()
console.log('2 end')
}
function next3() {
console.log('3 start')
console.log('3 end')
}
next1()

執行過程:



  1. 調用 next1,將其入棧執行,輸出 1 start

  2. 遇到 next2 函數,將其入棧執行,輸出 2 start

  3. 遇到 next3 函數,將其入棧執行,輸出 3 start

  4. 輸出 3 end,函數執行完畢,next3 彈出棧

  5. 輸出 2 end,函數執行完畢,next2 彈出棧

  6. 輸出 1 end,函數執行完畢,next1 彈出棧

  7. 棧空,全部執行完畢


相信通過這個簡單的例子,都大概明白中間件的執行過程了吧。


原理實現


中間件原理實現的關鍵點主要是 ctxnext 的傳遞。


function compose(middleware) {
return function(ctx) {
return dispatch(0)
function dispatch(i){
// 取出中間件
let fn = middleware[i]
if (!fn) {
return
}
// dispatch.bind(null, i + 1) 為應用中間件接受到的 next
// next 即下一個應用中間件
fn(ctx, dispatch.bind(null, i + 1))
}
}
}

可以看到,實現過程本質是函數的遞歸調用。在內部實現時,其實 next 沒有做什麼神奇的操作,它就是下一个中間件調用的函數,作為參數傳入供使用者調用。


下面我們來單獨測試 compose,你可以將它粘貼到控制台上運行:


function next1(ctx, next) {
console.log('1 start')
next()
console.log('1 end')
}
function next2(ctx, next) {
console.log('2 start')
next()
console.log('2 end')
}
function next3(ctx, next) {
console.log('3 start')
next()
console.log('3 end')
}

let ctx = {}
let fn = compose([next1, next2, next3])
fn(ctx)

最後,因為 Koa 中間件是可以使用 async/await 異步執行的,所以還需要修改下 compose 返回 Promise


function compose(middleware) {
return function(ctx) {
return dispatch(0)
function dispatch(i){
// 取出中間件
let fn = middleware[i]
if (!fn) {
return Promise.resolve()
}
// dispatch.bind(null, i + 1) 為應用中間件接受到的 next
// next 即下一個應用中間件
try {
return Promise.resolve( fn(ctx, dispatch.bind(null, i + 1)) )
} catch (error) {
return Promise.reject(error)
}
}
}
}

應用


實現完成中間件的邏輯后,將它應用到迷你版Koa中,原來的代碼邏輯要做一些修改(部分代碼忽略)


application.js:


module.exports = class Coa {
constructor() {
// 存儲中間件的數組
this.middleware = []
}

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
// 將中間件加入數組
this.middleware.push(fn)
return this
}

listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}

callback() {
const handleRequest = (req, res) => {
// 創建上下文
const ctx = this.createContext(req, res)
// fn 為第一個應用中間件
const fn = this.compose(this.middleware)
// 在所有中間件執行完畢后 respond 函數用於處理 ctx.body 輸出
return fn(ctx).then(() => respond(ctx)).catch(console.error)
}
return handleRequest
}

compose(middleware) {
return function(ctx) {
return dispatch(0)
function dispatch(i){
let fn = middleware[i]
if (!fn) {
return Promise.resolve()
}
// dispatch.bind(null, i + 1) 為應用中間件接受到的 next
try {
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))
} catch (error) {
return Promise.reject(error)
}
}
}
}
}

function respond(ctx) {
let res = ctx.res
let body = ctx.body
if (typeof body === 'string') {
return res.end(body)
}
if (typeof body === 'object') {
return res.end(JSON.stringify(body))
}
}

完整實現


application.js:


const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

module.exports = class Coa {
constructor() {
this.middleware = []
this.context = context
this.request = request
this.response = response
}

use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
this.middleware.push(fn)
return this
}

listen(...args) {
const server = http.createServer(this.callback())
server.listen(...args)
}

callback() {
const handleRequest = (req, res) => {
// 創建上下文
const ctx = this.createContext(req, res)
// fn 為第一個應用中間件
const fn = this.compose(this.middleware)
return fn(ctx).then(() => respond(ctx)).catch(console.error)
}
return handleRequest
}

// 創建上下文
createContext(req, res) {
const ctx = Object.create(this.context)
// 處理過的屬性
const request = ctx.request = Object.create(this.request)
const response = ctx.response = Object.create(this.response)
// 原生屬性
ctx.app = request.app = response.app = this;
ctx.req = request.req = response.req = req
ctx.res = request.res = response.res = res

request.ctx = response.ctx = ctx;
request.response = response;
response.request = request;

return ctx
}

// 中間件處理邏輯實現
compose(middleware) {
return function(ctx) {
return dispatch(0)
function dispatch(i){
let fn = middleware[i]
if (!fn) {
return Promise.resolve()
}
// dispatch.bind(null, i + 1) 為應用中間件接受到的 next
// next 即下一個應用中間件
try {
return Promise.resolve(fn(ctx, dispatch.bind(null, i + 1)))
} catch (error) {
return Promise.reject(error)
}
}
}
}
}

// 處理 body 不同類型輸出
function respond(ctx) {
let res = ctx.res
let body = ctx.body
if (typeof body === 'string') {
return res.end(body)
}
if (typeof body === 'object') {
return res.end(JSON.stringify(body))
}
}

寫在最後


本文的簡單實現了 Koa 主要的功能。有興趣最好還是自己去看源碼,實現自己的迷你版 Koa。其實 Koa 的源碼不算多,總共4個文件,全部代碼包括註釋也就 1800 行左右。而且邏輯不會很難,很推薦閱讀,尤其適合源碼入門級別的同學觀看。


最後附上完整實現的代碼:github

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】



※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益



※別再煩惱如何寫文案,掌握八大原則!



※教你寫出一流的銷售文案?



※超省錢租車方案



※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益



※產品缺大量曝光嗎?你需要的是一流包裝設計!



※回頭車貨運收費標準



Orignal From: Koa源碼解析,帶你實現一個迷你版的Koa

留言

這個網誌中的熱門文章

掃地機器人可以隨身帶上飛機嗎?我想要拿去送給國外的朋友

掃地機器人如果要隨身戴上飛機需要滿足兩個條件: 一個是掃地機器人連同你的隨身行李,整體的體積和重量要符合上機條件,這個具體每家航空公司都不同,可以諮詢,簡單的說就是隨身行李不要超寬超重。 還有一個就是由於掃地機器人內置了鋰電池,所以內置電池的容量要符合相關規定,每個掃地機器人電池容量都不同,具體自行查詢。 根據民航的相關安全要求,凡帶有鋰電池的電子設備均不可以托運,但符合重量要求,尺寸要求以及電量要求的鋰電池及其設備是可以帶上飛機的。 《鋰電池航空運輸規範》中內含鋰離子電池的設備電池額定能量不應超過100Wh的規定,符合國標GB31241-2014,並通過UN38.3航空運輸認證等國際安全標準,所以可以帶上飛機。但是不能托運,只能隨身攜帶。 掃地機器人     掃地機器人     掃地機器人吸塵器 http://www.greenh3y.com/?p=400 Orignal From: 掃地機器人可以隨身帶上飛機嗎?我想要拿去送給國外的朋友

不滿國際規範斷財路 非洲多國擬退野生動保公約

摘錄自2019年09月01日中央通訊社非洲報導 非洲南部多國揚言退出「瀕臨絕種野生動植物國際貿易公約」,因為公約多數成員拒絕放寬象牙與犀牛角交易,並且幾乎全面禁止將野生捕獲的大象送到動物園。 這個公約嚴格規範全球野生動物交易,包括限制象牙與犀牛角交易。 本週在日內瓦召開修訂「瀕臨絕種野生動植物國際貿易公約」(CITES)的會議期間,由於區域集團非南開發共同體(SADC)的多項提案遭否決,這個集團與公約的關係惡化。 全球大象數量最多的區域波札那、納米比亞與辛巴威要求販售取自自然死亡、充公與汰除的大象象牙,這項提議被居多數的101票否決。 40多年前制訂的CITES規範約3萬6000種動植物交易,並設計有助於遏止非法交易和制裁違規國家的機制。 不過有16個成員國的非南開發共同體部分會員批評CITES對非洲國家的問題視若無睹。 坦尚尼亞環境部長西蒙巴徹恩(George Simbachawene)於日內瓦召開的會議中表示:「結果無法採取進步、公平、包容與基於科學的的保育策略。」 他說:「該是認真重新考慮我們加入CITES是否有任何實質益處的時候了。」 本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理 【搬家相關資訊指南】 台中搬家 , 彰化搬家 , 南投搬家 前需注意的眉眉角角,別等搬了再說! 避免吃悶虧無故遭抬價! 台中搬家公司 免費估價,有契約讓您安心有保障! 評比 彰化搬家公司費用 , 南投搬家公司費用 收費行情懶人包大公開 彰化搬家費用 , 南投搬家費用 ,距離,噸數怎麼算?達人教你簡易估價知識! Orignal From: 不滿國際規範斷財路 非洲多國擬退野生動保公約

全球第一國 帛琉立法禁用、禁售防曬乳

摘錄自2018年11月2日蘋果日報帛琉報導 為了保護珊瑚礁生態,帛琉政府昨(1)日表示已立法嚴禁販售並使用防曬乳,此法將於2020年1月1日起正式生效。帛琉也成為全球首個全面禁止防曬乳的國家。 帛琉國會上周通過此法案,全面禁止使用和販售含有10種有害化學物質的防曬乳,違者將被處以1000美元(約3萬783元台幣)罰款。若遊客被發現私帶防曬乳入境,也會遭到沒收。帛琉總統雷蒙傑索(Tommy Remengesau)說:「沒收(防曬乳)已經足夠讓人不進行商業使用,而這也是很聰明的一招,一方面教育觀光客,又不會把他們嚇跑。」 根據官方說法,帛琉的熱門潛水點每小時會有4艘載著觀光客的船隻造訪,他們身上的防曬乳化學物質相當可觀。總統府發言人說:「帛琉各潛水和浮潛地點每天都有好幾加崙的防曬乳入海。我們只是盡力要防止環境遭受污染。」 本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理 【其他文章推薦】 ※ 台中搬家 , 彰化搬家 , 南投搬家 前需注意的眉眉角角,別等搬了再說! ※在找尋 搬家 公司嗎? ※搬家不受騙不被宰 桃園搬家公司 , 桃園市搬家公司 公開報價讓你比價不吃虧! Orignal From: 全球第一國 帛琉立法禁用、禁售防曬乳