中间件是啥? 听起来很高大上实际就是一个插件模式,原有系统开了个口,允许你插入一段自己的逻辑
插件的好处就是与系统解耦,扩展性强。
好的组件或者系统设计都有插件功能的支持,比如webpack、vue、redux等等
中间件通常会是一小段代码,它可以接受一个请求,对其进行处理,每个中间件只处理一件事,完成后将其传递给下一层或最终处理。这样就做到了程序的解耦。
下面我们以axios、vuex、redux、koa来探究下他们设计的插件支持
主要研究两个问题:
1、插件在一个系统里是怎么注册的,怎么存储的?
2、在执行过程中怎么插件是组合得到调用的?
axios
基本使用
给全部的请求加上token,我们使用axios的拦截器:
拦截器的注册:
request.use(successfunc, errfunc)
response.use(successfunc, errfunc)
这个api是不是似曾相似?
没错,promise.then
接受的同样也是这两个参数。promise.then(resolvefunc, rejectfunc)
axios 内部正是利用了 promise 的这个机制,把 use 传入的两个函数作为一个intercetpor
,每一个intercetpor
都有resolved
和rejected
两个方法。
// 把
axios.interceptors.response.use(func1, func2)
// 在内部存储为
{
resolvedQueue: func1,
rejectedQueue: func2
}
那么是不是 在请求之前 遍历调用resolvedQueue的函数一次遍历执行,请求之后遍历rejectedQueue执行即可呢
这里引入了promise链,保证了异步的顺序执行
然后 万事俱备只欠东风,利用promise实现异步串行调用
总结:在拦截器里 接受的参数,都是逐层向下传递的,从设计角度就是库把所有数据都完全信任地交给了插件,插件需要显示的传递值。(回调里 return 处理好的data)
vuex
用法
引入插件
实现原理也是 将subscribeAction传入的函数 分论别类地放在不同的回调集合,分别在vuex内部的dispatch触发前后调用before、after回调
总结下 vuex 和 axios 类型,差不多套路都是收集插件集合,然后在合适的时机调用
回答下这两个问题:
1、插件在一个系统里是怎么注册的,怎么存储的?(一个队列存储,在合适的时机 统一遍历执行回调队列的函数)
2、在执行过程中怎么插件是组合得到调用的?(数据的传递考虑 基于对象引用地址 or 函数返回值得到更新
数据更新: axios 依赖于显式返回处理后的data; vuex 插件如果要更新state实现侵入式改动的话,
直接更改state会报警,在插件中不允许直接修改状态——类似于组件,
只能通过提交 mutation 来触发变化。通过提交 mutation,插件可以用来同步数据源到 store
调用: axios构建promise链,利用链实现异步的串行控制;vuex类似
redux
在Redux中,Middlerwares主要的作用就是处理Action,Redux中的Action必须是一个plain object。但是为了实现异步的Action或其他功能,这个Action可能就是一个函数,或者是一个promise对象。这是就需要中间件帮助来处理这种特殊的Action了。
也就是说,Redux中的Middleware会对特定类型action做一定的转换,所以最后传给reducer的action一定是标准的plain object。
针对Action的特征,Reudx Middleware可以采取不同的操作:
- 可以选择传递给下一个中间件,如:next(action)
- 可以选择跳过某些中间件,如:dispatch(action)
- 或者更直接了当的结束传递,如:return。
要理解 redux 中的中间件机制,需要先理解一个方法:compose
function compose(...funcs: Function[]) {
return funcs.reduce((a, b) => (...args: any) => a(b(...args)));
}
compose(fn1, fn2, fn3) (…args) = > fn1(fn2(fn3(…args)))
高阶聚合函数,相当于把 fn3 先执行,然后把结果传给 fn2 再执行,再把结果交给 fn1 去执行。
redux中间件的核心可以用一句话解释:
把 dispatch 这个方法不断用高阶函数包装,最后返回一个强化过后的 dispatch
**
const typeLogMiddleware = dispatch => {
// 返回的其实还是一个结构相同的dispatch,接受的参数也相同
// 只是把原始的dispatch包在里面了而已。
return ({ type, ...args }) => {
console.log(`type is ${type}`);
return dispatch({ type, ...args });
};
};
考虑无侵入的修改 比如每次改变数据打印log
// […middlewares] = [middle1, middle2]
// compose(…middlewares) = compose(middle1, middle2)(args) = middle1(middle2(args))
// 最终函数返回值就是最终传递值
// args 传递的是 dispatch 这个函数
// 代表 初始值是一个函数,经过compose处理,得到一个加强的函数
koa
和 redux 的中间件机制有点类似,本质上都是高阶函数的嵌套,外层的中间件嵌套着内层的中间件,这种机制的好处是可以自己控制中间件的能力
插件顺序是
1、最外层 管控全局错误
2、第二层 日志中间件, 在next之后 拿到的是 业务层处理后的数据
3、第三层 核心服务中间件
总结
本质也是 基于数据的传递,依赖的底层知识点有:
1、数据是共享地址的,函数内拿到的是引用地址
2、函数在js里是第一等公民,函数参数、返回值都可以是函数
所以有了 高阶函数,以及外界传入回调,库里决定啥时候调用回调这样的处理方式实现 开放性
3、一些闭包的知识
其余的都是一些技巧:
1、vuex 基于 回调,在特定时期调用 回调集
2、axios 库里内部管理了回调,高级的地方在于 引入了promise,将回调处理成了链式集合,
以此管理异步、同步的问题
3、koa、redux, 使用了高阶函数 compose
更完善的,比如 vue的生命周期,webpack 等,作为一个完整的插件体系来说,需要库设计开放更多的钩子函数
对于注入的插件的处理,可能会需要更完善的 异步控制,比如webpack的tapable核心依赖库提供丰富的钩子类型:同步钩子、异步并行钩子、异步串行钩子等
思考
有没有啥办法,不让库(源代码)提供接口,自己就能实现注入、拦截的功能呢?
即假如面对这样的问题:
有一个被业务广泛使用的函数(比如common的log),
我们是否能够在既不更改调用它的业务代码,也不更改该函数源码的前提下,
在其(执行前后)注入一段我们自定义的逻辑呢?
下面给出一两个可以思考的demo点
demo1、劫持console.log 做点啥 在每一个console.log前后做点啥
(比如在服务器的每个console.log实现日志收集)
demo2、在 Vue 中收集甚至定制所有的 this.$emit 信息
import Vue from 'vue'
Vue.prototype.$emit = withHookBefore(Vue.prototype.$emit, (name, payload) => {
// 在此发挥黑魔法
console.log('emitting', name, payload)
})
function withHookBefore (originalFn, hookFn) {
return function () {
hookFn.apply(this, arguments)
return originalFn.apply(this, arguments)
}
}
暂时想到的点:
way1、基于原型链劫持,高阶函数 装扮原函数
way2、数据响应劫持 proxy \ Object.defineProperties