Koa2是一個基于Node實現(xiàn)的Web框架,特點是優(yōu)雅、簡潔、健壯、體積小、表現(xiàn)力強。它所有的功能通過插件的形式來實現(xiàn)。
本文主要介紹如何自己實現(xiàn)一個簡單的Koa,通過這種方式來深入理解Koa原理,尤其是中間件部分的理解。Koa的具體實現(xiàn)可以看的koa的源碼。
// koa 的簡單使用
const Koa = require('koa')
const app = new Koa()
app.use(async ctx => {
ctx.body = 'Hello World';
})
app.listen(3000)
通過上面的代碼,如果要實現(xiàn)koa,我們需要實現(xiàn)三個模塊,分別是http的封裝,ctx對象的構建,中間件機制的實現(xiàn),當然koa還實現(xiàn)了錯誤捕獲和錯誤處理。
封裝http模塊
通過閱讀Koa2的源碼可知Koa是通過封裝原生的node http模塊。
// server.js
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200)
res.end('hello world')
})
server.listen(3000, () => {
console.log('server running on port 3000')
})
以上是使用Node.js創(chuàng)建一個HTTP服務的代碼片段,關鍵是使用http模塊中的createServer()方法,接下來我們對上面這面這部分過程進行一個封裝,首先創(chuàng)建application.js,并創(chuàng)建一個Application類用于創(chuàng)建Koa實例。通過創(chuàng)建use()方法來注冊中間件和回調(diào)函數(shù)。并通過listen()方法開啟服務監(jiān)聽實例,并傳入use()方法注冊的回調(diào)函數(shù),如下代碼所示:
// application.js
let http = require('http')
class Application {
constructor () {
this.callback = () => {}
}
listen(...args) {
const server = http.createServer((req, res) => {
this.callback(req, res)
})
server.listen(...args)
}
use(callback){
this.callback = callback
}
}
module.exports = Application
接下來創(chuàng)建一個server.js,引入application.js進行測試
// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()
app.use((req, res) => {
res.writeHead(200)
res.end('hello world')
})
app.listen(3000, () => {
console.log('server running on port 3000')
})
啟動后,在瀏覽器中輸入localhost:3000就能看到顯示"hello world"。這樣就完成http server的簡單封裝了。
構造ctx對象
Koa 的 Context 把 Node 的 Request 對象和 Response 對象封裝到單個對象中,并且暴露給中間件等回調(diào)函數(shù)。比如獲取 url,封裝之前通過req.url的方式獲取,封裝之后只需要ctx.url就可以獲取。因此我們需要達到以下效果:
app.use(async ctx => {
ctx // 這是 Context
ctx.request // 這是 koa Request
ctx.response // 這是 koa Response
});
JavaScript 的 getter 和 setter
在此之前,需要了解 setter 和 getter 屬性,通過 setter 和 getter 屬性,我們可以自定義屬性的特性。
// test.js
let person = {
_name: 'old name',
get name () {
return this._name
},
set name (val) {
console.log('new name is: ' + val)
this._name = val
}
}
console.log(person.name)
person.name = 'new name'
console.log(person.name)
// 輸出:
// old name
// new name is: new name
// new name
上面的代碼在每次給name屬性賦值的時會打印new name is: new name,添加了console.log這個行為,當然還可以做許多別的操作
構造 context
因此,我們可以使用 getter 和 setter 來構造 context,如下所示:
const http = require('http')
// 獲取 request 的 url
let request = {
get url() {
return this.req.url
}
}
let response = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
let context = {
get url() {
return this.request.url
},
get body() {
return this.response.body
},
set body(val) {
this.response.body = val
}
}
class Application {
constructor() {
// this.callback = () => {}
// 把 context、request 和 response 掛載到 Application 里面
this.context = context
this.request = request
this.response = response
}
use(callback) {
this.callback = callback
}
// 改造 listen
listen(...args) {
// 可能是一個 異步函數(shù) 因此需要 async
const server = http.createServer(async (req, res) => {
let ctx = this.createCtx(req, res)
// 此時就可以直接給callback一個 ctx
await this.callback(ctx)
// this.callback(req, res)
// 此時的 ctx.body 是可以直接獲取的
/**
* get body() {
* return this.response.body
* }
*/
ctx.res.end(ctx.body)
})
server.listen(...args)
}
// 把原生的 req 和 res 掛載到 ctx 上
createCtx(req, res) {
// 模擬 req 和 res
let ctx = Object.create(this.context) // 生成 context 對象,里面掛載 body 和 url
ctx.request = Object.create(this.request) // 把 request 掛載到 ctx 上
ctx.response = Object.create(this.response) // 把 response 掛載到 ctx 上
// 把原生的 req 和 res 都掛載到 request 和 response 以及 ctx 上
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
return ctx
}
}
這時,我們就可以通過 ctx 來獲取 url 了
// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()
// 此時可以使用 ctx
app.use(async (ctx) => {
ctx.body = 'ctx url: ' + ctx.url
})
app.listen(3000, () => {
console.log('server running on port 3000')
})
// 在瀏覽器輸入 localhost:3000/path
// 瀏覽器顯示 ctx url: /path
Koa中間件及洋蔥圈模型的理解與實現(xiàn)

koa的中間件機制是一個洋蔥圈模型,通過use()注冊多個中間件放入數(shù)組中,然后從外層開始往內(nèi)執(zhí)行,遇到next()后進入下一個中間件,當所有中間件執(zhí)行完后,開始返回,依次執(zhí)行中間件中未執(zhí)行的部分,如上圖所示。
在實現(xiàn)之前,我們先來了解一下中間件的原理,根據(jù)中間件的原理可知,要層層遞進執(zhí)行多個函數(shù),比如下面的例子
// test.js
function add (x, y) {
return x + y
}
function double (z) {
return z * 2
}
const res1 = add (1, 2)
const res2 = double (res1)
console.log(res2) // 6
上面的例子中,我們把add()函數(shù)傳入double()中,把函數(shù)作為參數(shù),這樣最終就會先執(zhí)行add()然后執(zhí)行double(),這時我們把這種模式編寫成一個通用的compose()函數(shù)。
// test.js
function add(x, y) {
return x + y
}
function double(z) {
return z * 2
}
// 把需要執(zhí)行的函數(shù)都按順序放到一個數(shù)組里,類似于koa中間件的use()方法
const middleware = [add, double]
let len = middleware.length
// compose 把所有函數(shù)都壓成一個函數(shù)
function compose(middleware) {
return (...args) => {
// step1: 先把第一個函數(shù)拿出來執(zhí)行一下,作為初始值
let res = middleware[0](...args)
// step2: 初始值執(zhí)行完成之后塞給第二個函數(shù)
for (let i = 1; i < len; i++) {
// 從 1 開始遍歷,把所有的函數(shù)都執(zhí)行一下
// 把執(zhí)行的結果傳給下一個函數(shù)
res = middleware[i](res)
}
return res
}
}
const fn = compose(middleware)
const res = fn(1, 2)
console.log(res) // 6
上面的compose()函數(shù)還有一個缺點,它是一個同步的方法,并沒有異步的等待,如果要使用異步,直接使用for循環(huán)是不行的,它不能等待異步執(zhí)行完畢,此外 koa 還對外暴露了next()方法來實現(xiàn)異步等待,它是一個Promise,當執(zhí)行到它時就執(zhí)行下一個中間件。
// test.js
async function fn1(next) {
console.log('fn1')
await next()
console.log('end fn1')
}
async function fn2(next) {
console.log('fn2')
await delay()
await next()
console.log('end fn2')
}
async function fn3(next) {
console.log('fn3')
}
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
}
function compose(middleware) {
// console.log(middleware)
// [ [AsyncFunction: fn1], [AsyncFunction: fn2], [AsyncFunction: fn3] ]
return () => {
// 先執(zhí)行第一個函數(shù)
return dispatch(0)
function dispatch(i) {
let fn = middleware[i]
// 如何不存在直接返回 Promise
if (!fn) {
return Promise.resolve()
}
// step1: 返回一個 Promise,因此單純變成一個 Promise 且 立即執(zhí)行
// step2: 往當前中間件傳入一個next()方法,當這個中間件有執(zhí)行 next 的時候才執(zhí)行下一個中間件
return Promise.resolve(fn(function next() {
// 執(zhí)行下一個中間件
return dispatch(i + 1)
}))
}
}
}
const middleware = [fn1, fn2, fn3]
const finalFn = compose(middleware)
finalFn()
// fn1
// fn2
// 等待兩秒
// fn3
// end fn2
// end fn1
上面已經(jīng)實現(xiàn)一個了一個簡單的中間件示例,接下來再把它整合到 Application 類中
// Application.js
const http = require('http')
let request = {
get url() {
return this.req.url
}
}
let response = {
get body() {
return this._body
},
set body(val) {
this._body = val
}
}
let context = {
get url() {
return this.request.url
},
get body() {
return this.response.body
},
set body(val) {
this.response.body = val
}
}
class Application {
constructor() {
this.context = context
this.request = request
this.response = response
this.middleware = []
}
use(callback) {
// 創(chuàng)建一個 middleware 數(shù)組,通過 push 傳入多個 callback
// 然后通過 compose 控制整個 middleware 執(zhí)行的順序
// 每個 callback 回調(diào)函數(shù)給兩個參數(shù) 第一個是 context 第二個是 next
this.middleware.push(callback)
// this.callback = callback
}
// 直接把 compose 移植過來
compose(middleware) {
// 每個中間件需要一個 context
return function (context) {
return dispatch(0)
function dispatch(i) {
let fn = middleware[i]
if (!fn) {
return Promise.resolve()
}
// 中間件第一個參數(shù)是一個 context,第二個參數(shù)是 next()
return Promise.resolve(fn(context, function next() {
return dispatch(i + 1)
}))
}
}
}
listen(...args) {
const server = http.createServer(async (req, res) => {
let ctx = this.createCtx(req, res)
// await this.callback(ctx)
// 這里不能直接執(zhí)行 callback 而是先獲取經(jīng)過 compose 處理后的中間件集合
const fn = this.compose(this.middleware)
await fn(ctx)
ctx.res.end(ctx.body)
})
server.listen(...args)
}
createCtx(req, res) {
let 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
}
}
module.exports = Application
這時一個精簡的 koa 就實現(xiàn)了,我們來測試它是否好用
// server.js
const MiniKoa = require('./application')
const app = new MiniKoa()
function delay() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
}, 2000)
})
}
app.use(async (ctx, next) => {
ctx.body = '(fn1) '
await next()
ctx.body += '(end fn1) '
})
app.use(async (ctx, next) => {
ctx.body += '(fn2) '
await delay()
await next()
ctx.body += '(end fn2) '
})
app.use(async (ctx, next) => {
ctx.body += '(fn3) '
})
app.listen(3000, () => {
console.log('server running on port 3000')
})
// 瀏覽器輸出:(fn1) (fn2) (fn3) (end fn2) (end fn1)
總結
到此為止,一個簡單的 Koa 就實現(xiàn)了,但是這里還缺少了異常處理,更詳細的實現(xiàn)方式請查看 Koa 源碼,無非也只是一些工具函數(shù)以及一些功能點的細化,其基本原理大概就是如此了。其中的難點是中間件原理,通過這個例子徹底理解中間件原理后,以后再使用起這個框架來,就更加得心應手了。