官網代碼示例css
1.新建一個項目,命名爲koa2-test
node
2.在命令行中,進入koa2-test
,執行npm init -y
npm
npm init -y
複製代碼
3.將此項目中的package.json
中的"main":"index.js"
替換爲"main":"app.js"
json
npm install koa --save
複製代碼
4.建立app.js
文件,將以下代碼複製到app.js
中,(一共有三個中間件),如下代碼爲官網示例代碼api
const Koa = require('koa');
const app = new Koa();
// logger 記錄日誌
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time 處理請求時間
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
複製代碼
5.在命令行中啓動(啓動命令以下)數組
node app.js
複製代碼
6.瀏覽器訪問 localhost:3000promise
命令行中打印以下:瀏覽器
代碼執行流程解析:bash
1.先註冊3箇中間件,再監聽3000
端口。app
2.在第一個logger中間件執行到await next();
時,下面的代碼先不執行,繼續執行下一個中間件x-response-time
,直到遇到ctx.body
,再開始逆向執行await next();
下的內容,最終打印出響應所需的時間。 此流程即爲洋蔥圈模型:(此圖爲網上搜索獲得)
將代碼進行以下修改,加入註釋
const Koa = require('koa');
const app = new Koa();
// logger 記錄日誌
app.use(async (ctx, next) => {
console.log("第一層洋蔥---開始")
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
console.log("第一層洋蔥---結束")
});
// x-response-time 處理請求時間
app.use(async (ctx, next) => {
console.log("第二層洋蔥---開始")
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log("第二層洋蔥---結束")
});
// response
app.use(async ctx => {
console.log("第三層洋蔥---開始")
ctx.body = 'Hello World';
console.log("第三層洋蔥---結束")
});
app.listen(3000);
複製代碼
請求在命令行打印以下:這樣就清楚的解釋了洋蔥圈模型(由外向內執行,再由內向外執行)
此行爲用洋蔥來形容很是的形象,先由外向內執行一個一個的next,等執行到最中心,再由內向外一層層執行。
從上面的例子中咱們能夠進行分析,中間件是如何進行實現的? 猜想至少應該有兩個步驟:
1.app.use
用來註冊中間件,並進行收集
2.實現next
機制:經過上一個next
觸發下一個next
// 引入http
const http = require('http')
// 組合中間件
function compose(middlewareList) {
return function (ctx) {
// 中間件調用的邏輯
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
// 執行中間件,並封裝爲Promise,格式兼容
fn(ctx, dispatch.bind(null, i + 1)) // Promise
)
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
// 定義構造函數
class Koa2 {
constructor () {
// 中間件數組
this.middlewareList = []
}
use(fn) {
this.middlewareList.push(fn)
return this
}
// 將req和res組合爲ctx
createContext(req, res) {
const ctx = {
req,
res
}
ctx.query = req.query
return ctx
}
handleRequest(ctx, fn) {
return fn(ctx)
}
callback() {
const fn = compose(this.middlewareList)
return (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}
// 建立服務並監聽 ...args傳入多個參數
listen(...args) {
const server = http.createServer(this.callback())
server.listen(..args)
}
}
module.exports = Koa2
複製代碼
結構爲:在class Koa2
中有 use createContext callback listen
等方法!經過use
方法來收集中間件。 compose
爲組合中間件的方法,從而實現next()
,其中Promise.resolve()
是爲了防止,在使用app.use()
時沒有使用async
包裹,就返回的不是promise
函數,Promise.resolve()
包裹後就一直返回promise
。
將fn(ctx, dispatch.bind(null, i + 1))
包裹在Promise.resolve()
中,fn
爲async
函數
Promise.resolve(value)方法返回一個以給定值解析後的Promise
對象。若是該值(指代value
)爲promise
,返回這個promise
;若是value值爲promise
,返回這個promise
。此函數將類promise
對象的多層嵌套展平。
在MDN
的解釋中,若是Promise.resolve(value)
中的value
值爲promise
,則返回這個promise
。在KOA2
中,中間件爲async await
包裹的異步函數,而async await
是promise
的語法糖。所以即便用Promise.resolve(value)
把中間件進行了包裹,也會不想影響結果,並且避免了中間件沒有使用async await
時的報錯。
在這篇文章中也有說起。
compose
爲組合中間件的方法,其實也就不難看出,整個中間件的核心功能就在compose
,此方法將中間件push
到middlewareList
中。
所以重點在於在compose
中進行遞歸,在監聽到request
請求的時候,將上下文對象ctx
傳入其中,最終使全部中間件按照洋蔥圈
模型執行。
當middlewares
數組合成到最後一箇中間件的時候,則直接返回,此時遞歸則結束。
Promise.resolve()
複製代碼
遞歸的返回值爲何要通過Promise.resolve()
的包裹呢?由於涉及到async、await
等相關的異步操做。若在使用app.use()
時未用async
包裹則會發生錯誤。
咱們最終的目的是返回一個可接收上下文參數ctx
的函數,所以須要對dispatch
進行進一步的包裝,就造成了咱們最終的compose
,dispatch
執行的過程是一個遞歸的過程。
function compose(middlewareList) {
reutrn function(ctx) {
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
fn(ctx, dispatch.bind(null, i+1))
)
} catch (err) {
return Promise.reject(err)
}
}
reutrn dispatch(0)
}
}
複製代碼
KOA源碼結構以下圖:
lib
文件夾下放着四個
KOA2
核心文件,
application.js、context.js、request.js、response.js
koa-compose
源碼以下
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
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)
}
}
}
}
複製代碼