提升用户体验之局部过渡

1,417 阅读5分钟

目录

  • 前言
  • 业务场景
  • 解决问题的思路
  • 开始解决问题
  • 效果
  • END

前言

提升用户体验是前端工程师必不可少的工作之一。

过渡效果每个项目都必不可少,优化过渡效果变的尤为的重要。

业务场景

这边来说一下发生这一切之前的场景,我们看一张过渡Loading图片。

这是大多数的做法,全局Loading,

一般是在请求发起及返回处做是否loading识别。

这样做的好处

  • 限制用户操作(在一个请求pending过程中无法触发其他操作)
  • 数据全面渲染后统一展示。

这样做的坏处

  • 如果当前页面请求数量过多,或者接口速度过慢,会让用户等待很长时间。
  • 如果某个接口报错,用户体验极差。

所以我们需要一个模块级别的局部loading解决方案。

假定页面中有五个模块,五个模块分别从五个接口获取数据,先返回的先渲染,未返回的处于loading状态。取消掉全局loading

基于这样的业务场景。

解决问题的思路

首先一般的直接做法是在请求方法的回掉函数的前后文中做loading变量加以控制,类似

想象一下这样做的坏处,如果当前页面模块过多,那么你需要维护的Loading变量就会增加(无论是放在状态管理中还是组件中)。

你需要不断判断业务状况来操作loading状态。看一个反例。

这是个真实的反面案例。。。 所以我们需要痛定思痛,让这样的事情不再发生,并且像更好的方向发展。

经过再三思考之后,我的思路是这样的

  • 通过loading队列的方式维护状态
  • 通过订阅发布模式来通知状态
  • 通过hoc注入组件的方式获取到订阅的状态。

流程图如下

接下来我们来一步一步完成它。

开始解决问题

  • 创建 loading.js
class Loading {
    constructor(props) {}
}

export default new Loading();

在发起请求的时候在config中带上自定义业务过渡的关键字loadKey

我这里用axios举例子。

import { get } from 'axios';

getCardInfoOne = get('/info/card/one', {}, { loadKey: 'unquieKeyOne' })

这里面的 ‘unquieKeyOne’ 是可以识别的唯一关键字,后面需要获取到这个关键字的状态,并加入到loading队列中去。要起的唯一一点。

接下来分别在request 拦截 以及 response 拦截处 获取关键字,做loading队列推入、弹出逻辑。

  • service.js
    
const instance = axios.create()    

// 请求拦截器
instance.interceptors.request.use(config => {
  const { loadKey } = config
  loadKey && loading.show(loadKey);

  return config
}, error => {
  Message.error('服务器开小差了,请稍后再试')
  return Promise.reject(error)
})

// 响应拦截器
instance.interceptors.response.use(response => {
  const { loadKey } = response.config
  loadKey && loading.hide(loadKey);

  const { data = {} } = response

  if (data.dm_error === 4000001 || data.dm_error === 4000003) {
    $user.logout()
  } else if (data.dm_error !== 0) {
    Message.error(data.error_msg || '接口响应异常,请联系管理员')
  }
  return response
}, error => {
  Message.error('服务器开小差了,请稍后再试')
  return Promise.reject(error)
})
  • loading.js
class Loading {
  constructor(props) {
    this.loadQueue = []; // core queue
  }

  show = key => {
    this.loadQueue.push(key);
  }

  hide = key => {
    const _loadKeys = [...this.loadQueue];

    if (_loadKeys.length === 0) return false;

    const _next = _loadKeys.filter(x => x !== key);

    this.loadQueue = _next;
  }
}

export default new Loading();

这样我们就有了一个动态的loading队列,下图发送了四个请求,我们打印loadQueue看一下效果。

我们有了loading queue之后,每次队列变化我们都需要把数据通知出去。

接下来我们通过发布订阅模式把消息推送出去。

  • loading.js
import React from 'react';

class Loading {
  constructor(props) {
    this.loadQueue = []; // core arr
    this.subscribers = []; // 订阅者们。
  }

  ...

  hide = key => {
    ...
    this.publish(key);
  }

  publish = (_key) => {
    const _subscribers = this.subscribers.map(subscriber => {
      const { key, reback, opObj } = subscriber;

      const hasKey = key.includes(_key);

      if (!hasKey) return subscriber;

      const _opObj = { ...opObj };
      _opObj[_key] = false;

      reback(_opObj);

      return { ...subscriber, opObj: _opObj };
    });

    // sync
    this.subscribers = _subscribers;
  }

  _subscribe = (keysArr, reback) => {
    if (!Array.isArray(keysArr)) return {};

    let opObj = {};
    keysArr.forEach(k => {
      opObj[k] = true;
    })

    let _obj = { key: keysArr.join(','), reback, opObj };

    this.subscribers.push(_obj)

    _obj = null;
  }
}

export default new Loading();

这样我们就可以通过订阅的方式动态的获取到业务模块需要到loadingKey的状态。类似如下

如果我们只做到这里的话,看起来与原来的写法复杂度上没怎么降低,我们还是需要来维护额外的组件state状态。所以还不是我们想要的效果。

所以接下来通过高阶组件属性代理的方式注入组件订阅的状态即可。

class Loading {
  ...

  hoc = subscribers => WrappedComponent => {
    const _this = this;
    return class extends React.Component {
      state = {
        ladingProps: _this.mapArrToObj(subscribers),
      }

      componentDidMount() {
        this.subscribe()
      }

      subscribe = () => {
        _this._subscribe(subscribers, (subInfo) => {
          this.setState({ ladingProps: subInfo });
        });
      }

      render() {
        const { ladingProps } = this.state;
        return <WrappedComponent { ...this.props } { ...ladingProps } />;
      }
    }
  }

  mapArrToObj = (_arr) => {
    const _obj = {};
    _arr.forEach(k => {
      _obj[k] = true;
    });
    return _obj;
  }
}

export default new Loading();

这样我们在组件中即可通过装饰器或者注入的方式获取到所需要的loadingKey状态了

@loading.hoc(['unquieKeyOne'])

还是附上一个截图吧

这样我们就通过了这样的一系列思路完成了对于,局部过渡的状态管理的数据设计。

  • 相比普通的结构减少了大量组件状态维护成本
  • 减少了大量过渡状态的业务代码
  • 统一过渡状态,便于扩展
  • 降低代码复杂度,减少代码耦合。

效果

由于我做的业务不方便截图或者录像到此,我们来看一下一个效果的DEMO。

相比与全局过渡限制用户操作,这样的过渡效果极大的提升了用户体验,并且如果接口不稳定的话,我们的页面也可以正常展示某些部分。

可能到这里有些人会有疑问,如果不限制用户操作,那么用户快速点击菜单或者其他操作怎么办?

上面的这个问题很明显通过节流防抖是防不住的。这是 我们就需要扩展我们的service,加入时序控制.

上述业务场景只需要把未返回的接口拦截掉即可。

END

项目的时序控制方案我的另一个同事正在整理,等到他整理完后我把链接发在文章评论中。

过年到现在大家都经历了很多事情,最近生活节奏又重新归为稳定,疫情快过去了,小命貌似没啥威胁了。之后就老样子,文章日常拖着写...

鞠躬,拜拜