跳到主要內容

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

留言

這個網誌中的熱門文章

旅館疑有臭蟲 北市府稽查找嘸

有民眾抱怨,日前投宿的北市某旅館疑似出現臭蟲,北市觀傳局與衛生局、環保局聯合稽查,但因為沒有發現蟲屍,無法確認該旅館是否真有臭蟲,市府下周將召開專家會議處理該案,市長蔣萬安則允諾會以最高規格防範臭蟲。 北市某旅館傳出疑有臭蟲,議員陳宥丞23日在市政總質詢詢問市府處理進度,並指出法國、澳洲、韓國的臭蟲,起初都現蹤公車或地鐵卻沒被發現,直到大規模爆發,才付出大量社會成本處理,而且一般殺蟲劑無法殺掉臭蟲,北市是否有因應措施? 觀傳局主任祕書蕭君杰表示,21日聯合環保局、衛生局到該旅館稽查,但沒有發現臭蟲,也沒有查到蟲卵跡象,只能檢查現場環境是否符合衛生相關規定,但環保局有指導業者如何針對臭蟲清潔消毒。觀傳局長王秋冬指出,下周會與專家學者召開會議,以最高規格處理此案。 想知道購買電動車哪裡補助最多? 台中電動車 補助資訊懶人包彙整 ;推薦評價好的 iphone維修 中心擁有專業的維修技術團隊,同時聘請資深iphone手機維修專家,現場說明手機問題,快速修理,沒修好不收錢住家的頂樓裝 太陽光電 聽說可發揮隔熱功效一線推薦東陽能源擁有核心技術、產品研發、系統規劃設置、專業團隊的太陽能發電廠商。 網頁設計 一頭霧水該從何著手呢? 回頭車 貨運收費標準宇安交通關係企業,自成立迄今,即秉持著「以誠待人」、「以實處事」的企業信念 台中搬家公司 教你幾個打包小技巧,輕鬆整理裝箱!還在煩惱搬家費用要多少哪?台中大展搬家線上試算搬家費用,從此不再擔心「物品怎麼計費」、「多少車才能裝完」 台中搬家 公司費用怎麼算?擁有20年純熟搬遷經驗,提供免費估價且流程透明更是5星評價的搬家公司好山好水 露營車 漫遊體驗露營車x公路旅行的十一個出遊特色。走到哪、玩到哪,彈性的出遊方案,行程跟出發地也可客製 廣告預算用在刀口上, 台北網頁設計 公司幫您達到更多曝光效益; 電動車補助 衛生局疾病管制科長張惠美表示,現場查到與臭蟲無關的多項衛生缺失,包含未提供從業人員體檢報告、簡易外傷藥品及器材超過有效期、紗窗有破損等,已要求業者2周內改善,將擇期複查,如果複查不合格,將依法裁罰3000元至2萬元罰鍰。 但陳宥丞批評,環保局未公告哪些藥劑能殺死臭蟲,很擔心北市會和韓國、法國一樣,把臭蟲防治交給民間恐造成大規模爆發,且北市內的廢棄傢俱回收廠也有可能成為臭蟲孳生的...

必知必會-存儲器層次結構

相信大家一定都用過各種存儲技術,比如mysql,mongodb,redis,mq等,這些存儲服務性能有非常大的區別,其中之一就是底層使用的存儲設備不同。作為一個程序員,你需要理解存儲器的層次結構,這樣才能對程序的性能差別瞭然於心。今天帶大家了解下計算機系統存儲器的層次結構。 存儲技術 首先了解下什麼是存儲器系統? 實質上就是一個具有不同容量、成本和訪問時間的存儲設備的層次結構。從快到慢依次為:CPU寄存器、高速緩存、主存、磁盤; 這裏給大家介紹一組數據,讓大家有一個更清晰的認識: 如果數據存儲在CPU寄存器,需要0個時鐘周期就能訪問到,存儲在高速緩存中需要4~75個時鐘周期。如果存儲在主存需要上百個周期,而如果存儲在磁盤上,大約需要幾千萬個周期! -- 出自 CSAPP 接下來一起深入了解下計算機系統涉及的幾個存儲設備: 隨機訪問存儲器 隨機訪問存儲器(RAM)分為靜態RAM (SRAM) 和動態RAM(DRAM)。SRAM的速度更快,但也貴很多,一般不會超過幾兆字節,通常用來做告訴緩存存儲器。DRAM就是就是我們常說的主存。 訪問主存 數據流是通過操作系統中的總線的共享电子電路在處理器和DRAM之間來來回回。每次CPU和主存之間的數據傳送都是通過一系列複雜的步驟完成,這些步驟成為總線事務。讀事務是將主存傳送數據到CPU。寫事務從CPU傳送數據到主存。 總線是一組并行的導線,能攜帶地址、數據和控制信號。下圖展示了CPU芯片是如何與主存DRAM連接的。 那麼我們在加載數據和存儲數據時,CPU和主存到底是怎樣交互實現的呢? 首先來看一個基本指令,加載內存數據到CPU寄存器中: movq A,%rax 將地址A的內容加載到寄存器%rax中,這個命令會使CPU芯片上稱為總線接口(bus interface)的電路在總線上發起讀事務,具體分為三個步驟: CPU將地址A放到系統總線上,I/O橋將信號傳遞到內存總線。詳情看下下圖a 主存感覺到內存總線上的地址信號,從內存總線讀地址,從DRAM取出數據字,將其寫到內存總線。I/O橋將內存總線信號翻譯成系統總線信號,沿着系統總線傳遞到CPU總線接口。下圖b CPU感覺到系統總線上的數據,從總線上讀數據,並將數據複製到寄存器%rax...

2016年電動車和插電式混合動力車銷量預計將超過70萬輛

中汽協日前預測,2016年全國電動汽車和插電式混合動力車的銷量預計將超過70萬輛,較2015年的銷量增長一倍。 2015年電動車和插電式混合動力車的合併銷量為331092輛,較2014年增長了340%。其中包括247482輛電動車和83610輛插電式混合動力車,在24萬輛多的電動汽車銷量中,包括146719輛乘用車,另有100763輛為商用車。插電式混合動力車的銷量中,60663輛為乘用車,22947輛為商用車。 根據2015年起草的藍圖,政府計畫到2020年在全國範圍內新建12000個充電站和480枚充電樁。2014年年底,全國共有780個充電站共31000枚充電樁。2015年政府還為27個省市自治區設定了電動車的最低銷量目標。 政府預計,這些措施到位後,自主品牌車企的電動車和插電式混合動力車銷量到2020年可達100萬輛,到2025年可達300萬輛。 本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理 【其他文章推薦】 ※為什麼 USB CONNECTOR 是電子產業重要的元件? ※ 網頁設計 一頭霧水??該從何著手呢? 找到專業技術的 網頁設計公司 ,幫您輕鬆架站! ※想要讓你的商品成為最夯、最多人討論的話題? 網頁設計公司 讓你強力曝光 ※想知道最厲害的 台北網頁設計公司推薦 、 台中網頁設計公司推薦 專業設計師"嚨底家"!! Orignal From: 2016年電動車和插電式混合動力車銷量預計將超過70萬輛