之前講過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/...