设计模式之优雅的显示接口出错后的页面

74 阅读4分钟

最近在做需求过程中遇到了一个比较罕见的问题,前端通过JSB在调用客户端提供的一个接口时,调用的结果一直处于pending状态,既不成功也不失败,导致整个页面一直处于loading状态,但是这个接口对整个页面来讲又是一个次要接口,有它没它对整个页面来讲区别不大,那么由于一个不重要的接口导致整个页面挂掉,显然这个页面设计的有问题,不那么健壮。

要解决这个问题有两个方案

  1. 给接口加一个延时,例如3秒后如果接口还没返回结果就忽略这个接口。
  2. 重构页面结构,增加页面的健壮性。

有问题的代码如下,下面我们来对两个方案进行分析

// 抽象后的伪代码
getQuery(): Promise<Query> {
    return Promise.all([
      fetch1(),
      fetch2(),
      fetch3(),
      JSB.query().catch(() => {
        return { value: undefined }
      });
    ])

有问题的代码是 JSB.query(),就是调用客户端一直处于pending状态的接口,由于用的是 Promise.all(),所以只要有一个接口出问题,所有接口都没返回值。

给接口加超时时间

将有问题处的代码用如下代码替换

Promise.race([
    new Promise((reslove) => {
        setTimeout(() => {
            reslove({ value: undefined });
        }, 3000)
    }),
    JSB.query().catch(() => {
        return { value: undefined }
    });
])

JSB.query() 方法在 3 秒内没有返回结果时,则不再等待其返回结果,reslove 出 { value: undefined }

用上述方法固然可以解决需求中出现的问题,但是治标不治本,没有从根本上解决问题,理论上来说所有的接口都可能出现这个问题,那总不能给所有的接口都去加这么一个超时的逻辑吧,对于调用后端的接口还好说,axios 库可以加一个 timeout 属性解决问题,但是前端通过JSB还调用了大量的客户端的异步接口,给每一个接口加超时时间处理显然不是一个好的解决方案。

重构页面增强页面健壮性

在前端工程中我发现了很多页面获取接口数据的形式是这样的

// 伪代码如下
onload = (): Promise<PageData> => {
    return Promise.all([
      fetch1(),
      fetch2(),
      fetch3(),
      ...
    ])
 }

在父组件中用 Promise.all() 一次性将页面初始化时依赖的所有接口数据请求回来,将数据处理后分别传给各个子组件,这几乎成了很多页面的一种开发模式或者说开发模版,这样导致页面的所有接口都耦合在一起,强依赖后端接口的正确性,一个接口出错整个页面都挂掉,这不是一个好的体验。

装饰器模式+单利模式+观察者模式

为了解决接口耦合和强依赖后端接口正确性的问题,采用了三种设计模式来组成页面的开发模式,三种设计模式分别是:装饰器模式、单利模式、观察者模式。

代码思路如下:

  1. 首先定义一个依赖注入函数,对类进行单实例化
const actions = new Map();

export function getAction<T>(Context: Constructable<T>): T {
  if (!actions.has(Context)) {
    actions.set(Context, new Context());
  }

  return actions.get(Context);
}

export function Inject(Context: Constructable<{}>) {
  return (target: object, name: string): void => {
    target[name] = getAction(Context);
  };
}
  1. 定一个数据获取类,用于数据的获取,且这个类继承事件监听类,用于发射获取到的数据。在这个类里面调用接口获取数据,然后将获取到的数据发射出去。
export class DataStream extends BaseEvent {

  public queryData(): void {
    fetch(url).then((res) => {
        this.emit(EventName, res);
    })
    .catch((err: unknown) => {
        this.emit(EventErrorName, err);
     });
  }
}
  1. 在父组件里注入数据获取类,并调用获取数据的函数
@Inject(DataStream)
private ds: DataStream;

componentDidMount(): void {
    this.ds.queryData();
}
  1. 在各个子组件里注入数据获取类,并进行数据的监听,子组件在未监听到数据之前可以显示一个初始化的内容,等监听到数据后再更新页面。
@Inject(DataStream)
private ds: DataStream;

componentDidMount(): void {
    this.ds.subscribe(EventName, (res) => {
        this.update();
    });
}

总结

采用这种编码方式之后每个子组件依赖的接口都是相互独立的,互不影响,完全解藕,在依赖的接口返回数据之前可以显示为空、或初始数据、或loading状态,初始化显示的内容比较自由。且可以作为一种开发模式或开发模版,适用于其他页面的开发,方便在团队内推广。