在咱們平常開發中,愈來愈多看到了中間件
這個詞,例如Koa,redux等。這裏就大概記錄一下Koa和redux中間件的實現方式,能夠從中看到中間件的實現方式都是大同小異,基本都是實現了洋蔥模型。前端
對於中間件咱們須要瞭解的是git
做爲TJ大神的做品,真不愧是號稱基於 Node.js 平臺的下一代 web 開發框架
,其中對於中間件
的實現,generator/yield
,仍是await/async
,對於回調地獄的處理,都是給後來的開發者很大的影響。github
/**
* 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
的數組中。web
/**
* 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
就是咱們須要去了解的內容。json
這裏首先判斷是不是this.experimental
來獲取是否使用了async/await
,這個咱們在Koa1
中不作詳細介紹。咱們主要是來看一下co.wrap(compose(this.middleware))
。redux
讓咱們先來看一下compose()
api
/**
* 這裏使用了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);
能夠寫爲相似下面這個代碼結構bash
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)實現探索
/**
* 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
。
相比於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
的遍歷相似)。
因此中間件其實都基本相似,因此好好的理解了一種中間件的實現方式,其餘的學起來就很快了(只是表示前端這一塊哦)。