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

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

-- 『三体』

洋葱模型

重登逻辑设计

背景说明

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

相关推荐
老码沉思录1 小时前
写给初学者的React Native 全栈开发实战班
javascript·react native·react.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
Dread_lxy7 小时前
vue 依赖注入(Provide、Inject )和混入(mixins)
前端·javascript·vue.js
奔跑草-8 小时前
【前端】深入浅出 - TypeScript 的详细讲解
前端·javascript·react.js·typescript
羡与8 小时前
echarts-gl 3D柱状图配置
前端·javascript·echarts
前端郭德纲8 小时前
浏览器是加载ES6模块的?
javascript·算法
JerryXZR8 小时前
JavaScript核心编程 - 原型链 作用域 与 执行上下文
开发语言·javascript·原型模式