之前講過Koa2從零到腳手架,以及從淺入深瞭解Koa2源碼
這篇文章講解如何手寫一個 Koa2
Step 1:封裝 HTTP 服務和創建 Koa 構造函數
之前閲讀 Koa2 的源碼得知, Koa 的服務應用是基於 Node 原生的 HTTP 模塊,對其進行封裝形成的,我們先用原生 Node 實現 HTTP 服務
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200)
res.end('hello world')
})
server.listen(3000, () => {
console.log('監聽3000端口')
})
再看看用 Koa2 實現 HTTP 服務
const Koa = require('Koa')
const app = new Koa()
app.use((ctx, next) => {
ctx.body = 'hello world'
})
app.listen(3000, () => {
console.log('3000請求成功')
})
實現 Koa 的第一步,就是對 原生 HTTP 服務進行封裝,我們按照 Koa 源碼的結構,新建 lib/application.js 文件,代碼如下:
const http = require('http')
class Application {
constructor() {
this.callbackFunc
}
listen(port) {
const server = http.createServer(this.callback())
server.listen(port)
}
use(fn) {
this.callbackFunc = fn
}
callback() {
return (req, res) => this.callbackFunc(req, res)
}
}
module.exports = Application
我們引入手寫的 Koa,並寫個 demo
const Koa = require('./lib/application')
const app = new Koa()
app.use((req, res) => {
res.writeHead(200)
res.end('hello world')
})
app.listen(3000, () => {
console.log('3000請求成功')
})
啓動服務後,在瀏覽器中輸入 http://localhost:3000,內容顯示”Hello,World“
接着我們有兩個方向,一是簡化 res.writeHead(200)、res.end('Hello world') ;二是做塞入多箇中間件。要想做第一個點需要先寫 context,response,request 文件。做第二點其實做到後面也需要依賴 context,所以我們先做簡化原生 response、request,以及將它集成到 context(ctx)對象上
Step 2:構建 request、response、context 對象
request、response、context 對象分別對應 request.js、response.js、context.js,request.js 處理請求體,response.js 處理響應體,context 集成了 request 和 response
// request
let url = require('url')
module.exports = {
get query() {
return url.parse(this.req.url, true).query
},
}
// response
module.exporrs = {
get body() {
return this._body
},
set body(data) {
this._body = data
},
get status() {
return this.res.statusCode
},
set status(statusCode) {
if (typeof statusCode !== 'number') {
throw new Error('statusCode must be a number')
}
this.res.statusCode = statusCode
},
}
這裏我們在 request 中只做了 query 處理,在 response 中只做了 body、status 的處理。無論是 request 還是 response,我們都使用了 ES6 的 get、set,簡單來説,get/set 就是能對一個 key 進行取值和賦值
現在我們已經實現了 request、response,獲取了 request、response 對象和它們的封裝方法,接下來我們來寫 context。我們在源碼分析時曾經説過,context 繼承了 request 和 response 對象的參數,既有請求體中的方法,又有響應體中的方法,例如既能 ctx.query 查詢請求體中 url 上的參數,又能通過 ctx.body 返回數據。
module.exports = {
get query() {
return this.request.query
},
get body() {
return this.response.body
},
set body(data) {
this.response.body = data
},
get status() {
return this.response.status
},
set status(statusCode) {
this.response.status = statusCode
},
}
在源碼中使用了 delegate,把 context 中的 context.request、context.response 上的方法代理到了 context 上,即 context.request.query === context.query; context.response.body === context.body。而 context.request,context.response 則是在 application 中掛載
總結一下:request.js 負責簡化請求體的代碼,response.js 負責簡化響應體的代碼,context.js 把請求體和響應體集成在一個對象上,並且都在 application 上生成,修改 application.js 文件,添加代碼如下:
const http = require('http');
const context = require('context')
const request = require('request')
const response = require('response')
class Application {
constructor() {
this.callbackFunc
this.context = context
this.request = request
this.response = response
}
...
createConext(req, res) {
const ctx = Object.create(this.context)
ctx.request = Object.create(this.request)
ctx.response = Object.create(this.response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
...
}
因為 context、request、response 在其他方法中要用到,所以我們在構造器中就把他們分別賦值為 this.context、this.request、this.response 。我們實現了上下文 ctx ,現在我們回到之前的問題,簡寫 res.writeHead(200)、res.end('Hello world')
我們要想把 res.writeHead(200)、res.end('Hello world') 簡化為 ctx.body = 'Hello world',該怎麼做呢?
res.writeHead(200)、res.end('Hello world') 是原生的, ctx.body = 'Hello world' 是 Koa 的使用方法,我們要對 ctx.body = 'Hello world' 做解析並轉換為 res.writeHead(200)、res.end('Hello world') 。好在 ctx 已經通過 createContext 獲取,那麼再創建一個方法來封裝 res.end,用 ctx.body 來表示
responseBody(ctx) {
let context = ctx.body
if (typeof context === 'string') {
ctx.res.end(context)
} else if (typeof context === 'object') {
ctx.res.end(JSON.stringify(context))
}
}
最後我們修改 callback 方法
// callback() {
// return (req, res) => this.callbackFunc(req, res)
// }
callback() {
return (req, res) => {
// 把原生 req,res 封裝為 ctx
const ctx = this.createContext(req, res)
// 執行 use 中的函數, ctx.body 賦值
this.callbackFunc(ctx)
// 封裝 res.end,用 ctx.body 表示
return this.responseBody(ctx)
}
}
PS:具體代碼:請看倉庫中的 Step 2
Step 3:中間件機制和洋葱模型
我們知道, Koa2 中最重要的功能是中間件,它的表現形式是可以用多個 use,每一個 use 方法中的函數就是一箇中間件,通過第二個參數 next 來表示傳遞給下一個中間件,例如
app.use(async (ctx, next) => {
console.log(1)
await next()
console.log(6)
})
app.use(async (ctx, next) => {
console.log(2)
await next()
console.log(5)
})
app.use(async (ctx, next) => {
console.log(3)
ctx.body = 'hello world'
console.log(4)
})
// 結果 123456
所以,我們的中間件是個數組,其次,通過 next ,執行和暫停執行。一 next ,就暫停本中間件的執行,去執行下一個中間件。
Koa 的洋葱模型在 Koa1 中是用 generator + co.js 實現的, Koa2 則使用了 async/await + Promise 去實現。這次我們也是用 async/await + Promise 來實現
在源碼分析時,我們就説了 Koa2 的中間件合成是獨立成一個庫,即 koa-compose,它的核心代碼如下:
function compose(middleware) {
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch(i) {
if (i <= index)
return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
具體解讀可以去源碼分析上查看,這裏我們不做探究
這裏貼兩種解決方案,其實都是遞歸它
componse() {
return async (ctx) => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext)
}
}
let len = this.middlewares.length
let next = async () => {
return Promise.resolve()
}
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i]
next = createNext(currentMiddleware, next)
}
await next()
}
}
還有一種就是源碼,關於 compose 函數,筆者還不能很好的寫出個所以然,讀者們請自行理解
Step 4:錯誤捕獲與監聽機制
中間件中的錯誤代碼如何捕獲,因為中間件返回的是 Promise 實例,所以我們只需要 catch 錯誤處理就好,添加 onerror 方法
onerror(err, ctx) {
if (err.code === 'ENOENT') {
ctx.status = 404
} else {
ctx.status = 500
}
let msg = ctx.message || 'Internal error'
ctx.res.end(msg)
this.emit('error', err)
}
callback() {
return (req, res) => {
const ctx = this.createContext(req, res)
const respond = () => this.responseBody(ctx)
+ const onerror = (err) => this.onerror(err, ctx)
let fn = this.componse()
+ return fn(ctx).then(respond).catch(onerror)
}
}
我們現在只是對中間件部分做了錯誤捕獲,但是如果其他地方寫錯了代碼,怎麼知道以及通知給開發者,Node 提供了一個原生模塊——events,我們的 Application 類繼承它就能獲取到監聽功能,這樣,當服務器上有錯誤發生時就能全部捕獲
總結
我們先讀了 Koa2 的源碼,知道後其數據結構及使用方式後,再漸進式手寫了一個,這裏特別感謝第一名小蝌蚪的 KOA2 框架原理解析和實現,他的這篇文章是我寫 Koa2 文章的依據。説回 Koa2,它的功能特別簡單,就是對原生 req,res 做了處理,讓開發者能更容易地寫代碼;除此之外,引入中間件概念,這就像插件,引入即可使用,不需要時能減少代碼,輕量大概就是 Koa2 的關鍵字吧
GitHub 地址:https://github.com/johanazhu/...
參考資料
- KOA2 框架原理解析和實現