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

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

-- 『三体』

洋葱模型

重登逻辑设计

背景说明

  • 静默登录: 在小程序中, 调用 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 中间件实现重登机制.

相关推荐
爱学习的程序媛25 分钟前
《深入浅出Node.js》核心知识点梳理
javascript·node.js
Robet2 小时前
TS和JS成员变量修饰符
javascript·typescript
方法重载2 小时前
前端性能优化之“代码分割与懒加载”)
javascript
我叫张小白。2 小时前
Vue3 响应式数据:让数据拥有“生命力“
前端·javascript·vue.js·vue3
laocooon5238578862 小时前
vue3 本文实现了一个Vue3折叠面板组件
开发语言·前端·javascript
科普瑞传感仪器2 小时前
从轴孔装配到屏幕贴合:六维力感知的机器人柔性对位应用详解
前端·javascript·数据库·人工智能·机器人·自动化·无人机
n***F8752 小时前
SpringMVC 请求参数接收
前端·javascript·算法
TechMasterPlus3 小时前
VScode如何调试javascript文件
javascript·ide·vscode
牧码岛4 小时前
Web前端之canvas实现图片融合与清晰度介绍、合并
前端·javascript·css·html·web·canvas·web前端
灵犀坠4 小时前
前端面试八股复习心得
开发语言·前端·javascript