<静默重登>为例, 介绍<洋葱模型中间件>

藏好自己(内部实现), 做好清理(内存垃圾)

-- 『三体』

洋葱模型

重登逻辑设计

背景说明

  • 静默登录: 在小程序中, 调用 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

github.com/koajs/compo...

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 中间件实现重登机制.

相关推荐
new出一个对象2 小时前
uniapp接入BMapGL百度地图
javascript·百度·uni-app
你挚爱的强哥3 小时前
✅✅✅【Vue.js】sd.js基于jQuery Ajax最新原生完整版for凯哥API版本
javascript·vue.js·jquery
前端Hardy3 小时前
纯HTML&CSS实现3D旋转地球
前端·javascript·css·3d·html
susu10830189113 小时前
vue3中父div设置display flex,2个子div重叠
前端·javascript·vue.js
小镇程序员6 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
疯狂的沙粒6 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪7 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背7 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M7 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc7 小时前
学习electron
javascript·学习·electron