背景
洋葱圈模型是前端一个经典概念,起源于Koa,那么底层如何实现?介绍一下我看过的三种不同实现方法。
首先介绍一道面试题
let middleware = []
middleware.push((next) => {
console.log(1)
next()
console.log(1.1)
})
middleware.push((next) => {
console.log(2)
next()
console.log(2.1)
})
middleware.push((next) => {
console.log(3)
next()
console.log(3.1)
})
let fn = compose(middleware)
fn()
期望输出结果当然是
1 2 3 3.1 2.1 1.1
分析一下题目,这里的compose起到了组合函数的作用,举个例子A、B、C三个函数经过 componse([A,B,C]),期望最后执行顺序变成A(B(C))
KOA
源码很简洁:github.com/koajs/compo…
/**
* 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)
}
}
}
}
使用了递归的思想,dispatch.bind(null, i + 1) 返回第i+1个函数,实现了 middleware[i](middleware[i+1])的效果,并一层一层递归。
如果用来解题的话,我会这么写
function compose(middlewares) {
return function(){
const dispatch = (index)=>{
if(index === middlewares.length){
return
}
return middlewares[index](dispatch.bind(null,index+1))
}
return dispatch(0)
}
}
Redux
Redux的插件系统也是洋葱圈模型,而与koa相比,插件写法不同。
const logger = store => next => action => {
console.log('dispatching', action)
let result = next(action)
console.log('next state', store.getState())
return result
}
这里是一个柯里化写法,结合applyMiddleware组装函数去理解。通过遍历middlewares,不断更新dispatch函数,最后返回一个dispatch。这样每次dispatch(action),就都会通过middlewares的处理。
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
return Object.assign({}, store, { dispatch })
}
具体可以参考 理解 Middleware
那么如果这道题修改一下
function M1(store) {
return function(next) {
return function() {
console.log(1)
next()
console.log(1.1)
};
};
}
function M2(store) {
return function(next) {
return function() {
console.log(2)
next()
console.log(2.1)
};
};
}
function M3(store) {
return function(next) {
return function() {
console.log(3)
next()
console.log(3.1)
};
};
}
compose可以这么写
function compose( middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = ()=>{}
middlewares.forEach(middleware => (dispatch = middleware(store)(dispatch)))
return dispatch
}
Taro
Taro网路请求的拦截器也实现了一个洋葱圈模型,具体可看Taro.addInterceptor(interceptor)
koa和redux的实现都是用一个compose函数进行组合,返回一个组合后的函数。taro则更像击鼓传花,在插件里去触发下一个插件,并将处理后的requestParams传给下一个插件。
首先看一下插件写法
- 入参是一个chain对象,相当于ctx。
- chain.proceed将requestParams传递给下一个插件。
export function timeoutInterceptor (chain) {
const requestParams = chain.requestParams
let p
const res = new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
timeout = null
reject(new Error('网络链接超时,请稍后再试!'))
}, (requestParams && requestParams.timeout) || 60000)
p = chain.proceed(requestParams)
p
.then(res => {
if (!timeout) return
clearTimeout(timeout)
resolve(res)
})
.catch(err => {
timeout && clearTimeout(timeout)
reject(err)
})
})
if (!isUndefined(p) && isFunction(p.abort)) res.abort = p.abort
return res
}
再看一下chain的实现,Chain对象的interceptors是一个插件列表,proceed()会根据index找到下一个插件。
import { isFunction } from '../utils'
export default class Chain {
constructor (requestParams, interceptors, index) {
this.index = index || 0
this.requestParams = requestParams
this.interceptors = interceptors || []
}
proceed (requestParams) {
this.requestParams = requestParams
if (this.index >= this.interceptors.length) {
throw new Error('chain 参数错误, 请勿直接修改 request.chain')
}
const nextInterceptor = this._getNextInterceptor()
const nextChain = this._getNextChain()
const p = nextInterceptor(nextChain)
const res = p.catch(err => Promise.reject(err))
Object.keys(p).forEach(k => isFunction(p[k]) && (res[k] = p[k]))
return res
}
_getNextInterceptor () {
return this.interceptors[this.index]
}
_getNextChain () {
return new Chain(this.requestParams, this.interceptors, this.index + 1)
}
}
总结下来,三种不同的方式对应的是不同场景,了解下来还是蛮有趣的。