介紹
在我們日常開發中,越來越多看到了中介軟體
這個詞,例如Koa,redux等。這裡就大概記錄一下Koa和redux中介軟體的實現方式,可以從中看到中介軟體的實現方式都是大同小異,基本都是實現了洋蔥模型。
對於中介軟體我們需要了解的是
- 中介軟體是如何儲存的
- 中介軟體是如何執行的
正文
Koa
作為TJ大神的作品,真不愧是號稱基於 Node.js 平臺的下一代 web 開發框架
,其中對於中介軟體
的實現,generator/yield
,還是await/async
,對於回撥地獄的處理,都是給後來的開發者很大的影響。
Koa 1的中介軟體
儲存
/**
* https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
*/
...
var app = Application.prototype;
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.proxy = false;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
...
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are not allowed,
// so we have to make sure that `fn` is a generator function
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
複製程式碼
可以在這裡看到我們通過app.use
加入的中介軟體,儲存在一個middleware
的陣列中。
執行
/**
* https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
*/
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
// 刪除了一些警告程式碼
app.callback = function(){
...
var fn = this.experimental
? compose_es7(this.middleware)
: co.wrap(compose(this.middleware));
var self = this;
...
return function handleRequest(req, res){
var ctx = self.createContext(req, res);
self.handleRequest(ctx, fn);
}
};
app.handleRequest = function(ctx, fnMiddleware){
ctx.res.statusCode = 404;
onFinished(ctx.res, ctx.onerror);
fnMiddleware.call(ctx).then(function handleResponse() {
respond.call(ctx);
}).catch(ctx.onerror);
};
複製程式碼
可以在這裡看到middleware
陣列經過一些處理,生成了fn
,然後通過fnMiddleware.call(ctx)
傳入ctx
來處理,然後就將ctx
傳給了respond
,所以這裡的fnMiddleware
就是我們需要去了解的內容。
這裡首先判斷是否是this.experimental
來獲取是否使用了async/await
,這個我們在Koa1
中不做詳細介紹。我們主要是來看一下co.wrap(compose(this.middleware))
。
讓我們先來看一下compose()
/**
* 這裡使用了Koa1@1.6.0 package.json中的Koa-compose的版本
* https://github.com/Koajs/compose/blob/2.3.0/index.js
*/
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
}
}
function *noop(){}
複製程式碼
co.wrap(compose(this.middleware))
就變成了如下的樣子
co.wrap(function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
return yield *next;
})
複製程式碼
我們可以看到這裡對middleware
進行了倒序遍歷。next = middleware[i].call(this, next);
可以寫為類似下面這個程式碼結構
function *middleware1() {
...
yield function *next1() {
...
yield function *next2() {
...
...
...
}
...
}
...
}
複製程式碼
然後next = middleware[i].call(this, next);
其實每一個next
就是一個middleware
,所以也就可以變成
function *middleware1() {
...
yield function *middleware2() {
...
yield function *middleware() {
...
...
...
}
...
}
...
}
複製程式碼
然後我們就獲得了下面這個程式碼
co.wrap(function *(next){
next = function *middleware1() {
...
yield function *middleware2() {
...
yield (function *middleware3() {
...
yield function *() {
// noop
// NO next yield !
}
...
}
...
}
...
}
return yield *next;
})
複製程式碼
至此我們來看一眼洋蔥模型, 是不是和我們上面的程式碼結構很想。
現在我們有了洋蔥模型式的中間節程式碼,接下來就是執行它。接下來就是co.wrap
,這裡我們就不詳細說明了,co
框架就是一個通過Promise
來讓generator
自執行的框架,實現了類似async/await
的功能(其實應該說async/await
的實現方式就是Promise
和generator
)。
這裡提一個最後
yield *next
,是讓code
可以少執行一些,因為如果使用yield next
,會返回一個迭代器,然後co
來執行這個迭代器,而yield *
則是相當於將generator
裡面的內容寫在當前函式中,詳細可以見yield*
關於Koa1可以看我的早一些寫的另一篇Koa中介軟體(middleware)實現探索
Koa 2的中介軟體
儲存
/**
* https://github.com/Koajs/Koa/blob/1.6.0/lib/application.js
*/
...
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.env = process.env.NODE_ENV || 'development';
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
...
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/Koajs/Koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
複製程式碼
Koa2
對於middleware
的儲存和Koa1
基本一模一樣,儲存在一個陣列中。
執行
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
/**
* Handle request in callback.
*
* @api private
*/
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
複製程式碼
這裡主要就是兩行程式碼
const fn = compose(this.middleware);
// fnMiddleware === fn
fnMiddleware(ctx).then(handleResponse).catch(onerror);
複製程式碼
Koa2
的程式碼似乎比Koa1
要簡介一些了,在預設使用await/async
之後,少了co
的使用。
從fnMiddleware(ctx).then(handleResponse).catch(onerror);
我們可以知道fnMiddleware
返回了一個Promise
,然後執行了這個Promise
,所以我們主要知道compose
做了什麼就好。
/**
* https://github.com/Koajs/compose/blob/4.0.0/index.js
*/
function compose (middleware) {
...
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, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
複製程式碼
看起來這段程式碼比Koa1
的compose
稍微複雜了些,其實差不多,主要的程式碼其實也就兩個
function compose (middleware) {
...
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
let fn = middleware[i]
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
}
}
}
複製程式碼
相比於Koa1
遍歷middleware
陣列,Koa2
改為了遞迴。同上面一樣,我們可以將函式寫為如下結構
async function middleware1() {
...
await (async function middleware2() {
...
await (async function middleware3() {
...
});
...
});
...
}
複製程式碼
因為async
函式的自執行,所以直接執行該函式就可以了。
可以看到Koa1
與Koa2
的中介軟體的實現方式基本是一樣的,只是一個是基於generator/yield
, 一個是基於async/await
。
Redux
相比於Koa
的中介軟體的具體實現,Redux
相對稍複雜一些。
本人對於Redux基本沒有使用,只是寫過一些簡單的demo,看過一部分的原始碼,如有錯誤,請指正
儲存
我們在使用Redux的時候可能會這麼寫
// 好高階的函式啊
const logger = store => next => action => {
console.group(action.type)
console.info('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
console.groupEnd(action.type)
return result
}
let store = createStore(
todoApp,
applyMiddleware(
logger
)
)
複製程式碼
我們可以很方便的找到applyMiddleware
的原始碼。
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
複製程式碼
Redux
沒有單獨儲存middleware
的地方,但是通過展開符的...middlewares
,我們也可以知道至少一開始的middlewares
是一個陣列的形式。
執行
執行的程式碼,還是上面那段程式碼片段。
我們可以看到applyMiddleware()
中,對傳入的middlewares
做了簡單的封裝,目的是為了讓每個middleware
在執行的時候可以拿到當前的一些環境和一些必要的介面函式。也就是上面那個高階函式logger
所需要的三個引數store
,next
,action
。
一開始是middlewares.map(middleware => middleware(middlewareAPI))
,而middlewareAPI
傳入了getState
和dispatch
介面(dispatch
介面暫時沒有用)。這一步就實現了上面高階函式logger
所需要的引數store
。
然後是我們看到好多次的compose
函式,我們找到compose
函式的實現。
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
複製程式碼
我們看到compose
對傳入的中介軟體函式,通過Array.reduce
函式處理了一下。最終的函式應該大概類似下面這個格式
// 加入函式名next方便後面理解
function chain(...args) {
return () => {
return a(function next(...args) {
return b(function next(...args) {
return c(...args);
})
})
}
}
複製程式碼
這裡已經再次出現了我們熟悉的洋蔥模型。同時將下一個元件已引數(next
)的形式傳入當前的中介軟體,這裡就完成了上面的高階函式logger
所需要的第二個引數next
,在中介軟體內部呼叫next
函式就可以繼續中間節的流程。
最後傳入了store.dispatch
也就是高階函式logger
所需要的第二個引數action
,這個就不用多數了,就是將我們剛剛得到的洋蔥格式的函式呼叫一下,通過閉包使得每個中間節都可以拿到store.dispatch
。
總結
至此,Redux
和Koa
的中介軟體的介紹就差不多了,兩者都是以陣列的形式儲存了中介軟體,執行的時候都是建立了一個類似洋蔥模型的函式結構,也都是將一個包裹下一個中介軟體的函式當做next
,傳入當前中介軟體,使得當前中介軟體可以通過呼叫next
來執行洋蔥模型,同時在next
執行的前後都可以寫邏輯程式碼。不同的是Koa1
是通過遍歷生成的,Koa2
是通過遞迴來生成的,redux
是通過reduce
來生成的(和Koa1
的遍歷類似)。
所以中介軟體其實都基本類似,所以好好的理解了一種中介軟體的實現方式,其他的學起來就很快了(只是表示前端這一塊哦)。