說到中間件,很多開發者都會想到 Koa.js,其中間件設計無疑是前端中間件思想的典型代表之一,
最近重新溫習這部分內容,按奈不住想要和各位看官聊聊其中絕妙!
Koa用起來非常方便——比之express,它“完美中間件”的設計讓功能之間看起來非常簡潔!筆者在專案中就曾這樣使用過:
const Koa=require('koa')
const app=new Koa()
const Router=require('koa-router')
const router=new Router()
const cors=require('koa2-cors')
const koaBody=require('koa-body')
const ENV='test-mpin2'
app.use(cors({
origin:['http://localhost:9528'], // 也可以寫為:['*']
credentials:true
}))
app.use(koaBody({
multipart:true
}))
app.use(async(ctx,next)=>{
console.log('訪問全域中間件')
ctx.state.env=ENV // 全域快取
await next()
})
const playlist=require('./controller/playlist.js')
router.use('/playlist',playlist.routes())
const blog=require('./controller/blog.js')
router.use('/blog',blog.routes())
app.use(router.routes()).use(router.allowedMethods())
app.listen(3000,()=>{
console.log('服務已開啟')
})
它將路由router抽離出去作為單獨的中間件使用,則app只負責全域處理,還比如:
// 最外層中間件,可以用于兜底 Koa 全域錯誤
app.use(async (ctx, next) => {
try {
// 執行下一個中間件
await next();
} catch (error) {
console.log(`[koa error]: ${error.message}`)
}
});
// 第二層中間件,可以用于日志記錄
app.use(async (ctx, next) => {
const { req } = ctx;
console.log(`req is ${JSON.stringify(req)}`);
await next();
console.log(`res is ${JSON.stringify(ctx.res)}`);
});
簡單實作一個Koa吧!
如上代碼,我們看 Koa 實體,通過use方法注冊和串聯中間件,其原始碼的簡單實作可以表述為:
use(fn) {
this.middleware.push(fn);
return this;
}
我們將中間件存盤到this.middleware陣列中,那么中間件是如何被執行的呢?參考下面原始碼:
// 通過 createServer 方法啟動一個 Node.js 服務
listen(...args) {
const server = http.createServer(this.callback());
server.listen(...args);
}
Koa 框架通過 http 模塊的 createServer 方法創建一個 Node.js 服務,并傳入 this.callback() 方法, callback原始碼簡單實作如下:
callback(){
const fn=compose(this.middlewareList)
return (req,res)=>{
const ctx=createContext(req,res)
return this.handleRequest(ctx,fn)
}
}
handleRequest(ctx, fn) {
const onerror = err => ctx.onerror(err);
// 將 ctx 物件傳遞給中間件函式 fn
return fn(ctx).catch(onerror);
}
如上代碼,我們將 Koa 一個中間件組合和執行流程梳理為以下步驟:
- 通過一個方法(我們稱為compose)組合各種中間件,回傳一個中間件組合函式
fn - 請求過來時,會先呼叫
handleRequest方法,該方法完成:- 呼叫
createContext方法,對該次請求封裝出一個ctx物件; - 接著呼叫
this.handleRequest(ctx, fn)處理該次請求,
- 呼叫
其中,核心程序就是使用compose方法組合各種中間件 —— 這是一個單獨的方法,它應該不受Koa其余方法的約束,其原始碼簡單實作為:
// 組合中間件
// 和express中的next函式意義一樣
function compose(middlewareList){
// return function意思是回傳一個函式
return function(ctx,next){
// 各種中間件呼叫的邏輯
function dispatch(i){
const fn=middlewareList[i] || next
if(fn){
try{
// koa中都是async,其回傳的是一個promise(物件)
return Promise.resolve(fn(ctx,function next(){
return dispatch(i+1)
}))
}catch(err){
return Promise.reject(err)
}
}else{
return Promise.resolve()
}
}
return dispatch(0)
}
}
其功能可以表示為這樣(非原始碼):
async function middleware1() {
//...
await (async function middleware2() {
//...
await (async function middleware3() {
//...
});
//...
});
//...
}
到這里我們其實可以“初窺”其原理,有兩點:
- Koa 的中間件機制被社區形象地總結為洋蔥模型;
所謂洋蔥模型,就是指每一個 Koa 中間件都是一層洋蔥圈,它即可以掌管請求進入,也可以掌管回應回傳,換句話說:外層的中間件可以影響內層的請求和回應階段,內層的中間件只能影響外層的回應階段,
- dispatch(n)對應第 n 個中間件的執行,在使用中即第 n 個中間件可以通過await next()來“插入”執行下一個中間件,同時在最后一個中間件執行完成后,依然有恢復執行的能力,即:通過洋蔥模型,await next()控制呼叫后面的中間件,直到全域沒有可執行的中間件且堆疊執行完畢,最終“原路回傳”至第一個執行next的中間件,這種方式有個優點,特別是對于日志記錄以及錯誤處理等全域功能需要非常友好,
Koa1 的中間件實作利用了 Generator 函式 + co 庫(一種基于 Promise 的 Generator 函式流程管理工具),來實作協程運行,本質上,Koa v1 中間件和 Koa v2 中間件思想是類似的,只不過 Koa v2 改用了 Async/Await 來替換 Generator 函式 + co 庫,整體實作更加巧妙,代碼更加優雅,—— from《狼書》
經過上述部分原始碼的描述,我們就可以采用es6的方式將其組合起來:
// myKoa.js檔案
const http=require('http')
function compose(){} //見上
class LikeKoa2{
constructor() {
this.middlewareList=[]
}
use(){} //見上
// 把所有的req,res屬性、事件都交給ctx(這里只是簡寫)
createContext(req,res){
const ctx={
req,
res
}
// 比如
ctx.query=req,query
return ctx
}
handleRequest(){} //見上
callback(){} //見上
listen(){} //見上
}
// koa和express的不同之一:
// express在呼叫時直接呼叫函式:const app=express();所以暴露出去new過的物件——具體見下面鏈接中代碼
// 但是koa呼叫時以類的方式:const app=new Koa();所以直接暴露出去
module.exports=LikeKoa2
那use方法和其余方法并不相通,它是如何被執行的呢?執行了createServer后是不是相當于建立了一個通道、掛載了一個監聽函式呢?
這一點恐怕就要到Node的原始碼中一探究竟了…
對比 Koa,聊聊 Express 原理
說起 Node.js 框架,我們一定忘不了 Express —— 不同于 Koa,它繼承了路由、靜態服務器和模板引擎等功能,雖然比之Koa顯得“臃腫”了許多,但看上去比 Koa 更像是一個框架,通過學習 Express 原始碼,筆者簡單的總結了它的作業機制:
- 通過app.use方法注冊中間件,
- 一個中間件可以理解為一個 Layer 物件,其中包含了當前路由匹配的正則資訊以及 handle 方法,
- 所有中間件(Layer 物件)使用stack陣列存盤起來,
- 當一個請求過來時,會從 req 中獲取請求 path,根據 path 從stack中找到匹配的 Layer,具體匹配程序由
router.handle函式實作, router.handle函式通過next()方法遍歷每一個 layer 進行比對:next()方法通過閉包維持了對于 Stack Index 游標的參考,當呼叫next()方法時,就會從下一個中間件開始查找;- 如果比對結果為 true,則呼叫
layer.handle_request方法,layer.handle_request方法中會呼叫next()方法 ,實作中間件的執行,
通過上述內容,我們可以看到,Express 其實是通過 next() 方法維護了遍歷中間件串列的 Index 游標,中間件每次呼叫next()方法時,會通過增加 Index 游標的方式找到下一個中間件并執行,它的功能就像這樣:
((req, res) => {
console.log('第一個中間件');
((req, res) => {
console.log('第二個中間件');
(async(req, res) => {
console.log('第三個中間件');
await sleep(2000)
res.status(200).send('hello')
})(req, res)
console.log('第二個中間件呼叫結束');
})(req, res)
console.log('第一個中間件呼叫結束')
})(req, res)
如上代碼,Express 中間件設計并不是一個洋蔥模型,它是基于回呼實作的線形模型,不利于組合,不利于互操,在設計上并不像 Koa 一樣簡單,而且業務代碼有一定程度的侵擾,甚至會造成不同中間件間的耦合,
express的簡單實作筆者已上傳至騰訊微云,需要者可自行查看&下載:express的簡單實作
CSDN認證博客專家
ECMAScript 6
Node.js
CSS
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/260079.html
標籤:其他
