藏好自己(内部实现), 做好清理(内存垃圾)
-- 『三体』
洋葱模型
重登逻辑设计
背景说明
-
静默登录: 在小程序中, 调用 wx.login() 并与后端通讯拿到登录凭证的过程是对用户无感知的.
-
静默重登: 由于登录时静默的, 因此接口登录失败时, 也可以在无报错的情况下重新登录后再次执行网络请求.
需求整理
-
重新登录后要能够再次触发请求
-
多个接口同时登录失败, 执行登录的方法只应当触发一次, 并对这些接口都再次触发请求.
-
业务层调用 api.xxx() 对重登无感知, 保持 .then(() => ...) 的写法仍能获取到数据.
设计方案
-
因为未登录或登录超时情况下都会执行静默重登, 因此可以使用 boolean 标记是否已登录
-
多个请求同时登录失败, 需要对登录方法加锁, 我们可以直接使用 "登录的 promise 是否存在"
基于中间件的伪代码
为了将中间件抽象出来, 我们将平台依赖/业务依赖的
-
login - 执行登录
-
checkLoginError - 判断错误是否为登录报错
作为高阶函数入参
fetcher 作为 callback 表示真正执行的网络请求
TypeScript
export const createLoginMiddleware = ({ login, checkLoginError, retryNum = 3 }: CreateOption) => {
let loggedIn = false;
let loadingPromise: Promise<unknown> | undefined;
return defineMiddleware(async (option, fetcher) => {
async function loginWrapper() {
if (loggedIn) return;
if (!loadingPromise) {
loadingPromise = login?.();
}
await loadingPromise;
loggedIn = true;
loadingPromise = undefined;
}
async function retryFn(n) {
try {
await loginWrapper();
return await fetcher();
} catch (e) {
if (n > 0 && checkLoginError?.(e)) {
loggedIn = false;
return retryFn(n - 1);
} else {
throw e;
}
}
}
return retryFn(retryNum);
});
};
洋葱模型原理
自然而然的 callback
如果 fetch 前需要做其它的处理, 我们将
return defineMiddleware(async (option, fetcher) => {
改成
return defineMiddleware(async (option, ``next``) => {
就是我们所说的中间件了
假设除了上述中间件, 我们还有 middlewareX 和 middlewareY, 要使用上述中间件, 最简单的用法:
TypeScript
const option;
const response =
createLoginMiddleware({
login,
checkLoginError
})(option, () => {
return middlewareX(option, () => {
return middlewareY(option, () => {
... return axios.get(...);
})
})
})
JS 经典: 回调地狱 |
---|
洋葱模型的核心: compose
TypeScript
'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)
}
}
}
}
使用 compose 解决 callback
TypeScript
class LoginError extends Error {}
const run = compose([
createLoginMiddleware({ login, checkLoginError })
middlewareX,
middlewareY,
处理报错中间件 // 判断后端返回的报错是否为登录报错, 并 throw LoginError
// 这样错误类型就可以内聚在使用层
]);
const response = run(option, () => {
return fetch(...);
});
已知问题:
koa 的 compose 实现, 对于中间件中的 next 方法只允许调用一次, 并不适用于上述重登逻辑. 我们只需要简单修改代码就可以了.
告诉大家一个小秘密: 上面的中间件伪代码就是真实可运行的.
用伪代码来描述设计思路, 将需要隐藏的细节定义为函数, 最后再通过入参实现函数本身.
竞品对比
axios 中间件处理
TypeScript
axios.interceptors.request.use((config) => {
console.log('interceptors.request1');
return config;
}, (error) => {
return Promise.reject(error);
});
axios.interceptors.request.use(...);
axios.interceptors.request.use(...);
axios.interceptors.response.use(...);
axios.interceptors.response.use((config) => {
console.log('interceptors.request2');
return config;
}, (error) => {
return Promise.reject(error);
});
axios 的中间件是将 request 和 response 分开处理的.
通过伪代码的实现, 需要再进行一次逻辑封装, 才能避免散落在多个 interceptors 中.
而一个中间件的逻辑需要足够的内聚, 减少其它同学的理解成本, 提高可维护性.
作为分享后习题, 请大家基于 axios 中间件实现重登机制.