目录
- 前言
- 业务场景
- 解决问题的思路
- 开始解决问题
- 效果
- 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
项目的时序控制方案我的另一个同事正在整理,等到他整理完后我把链接发在文章评论中。
过年到现在大家都经历了很多事情,最近生活节奏又重新归为稳定,疫情快过去了,小命貌似没啥威胁了。之后就老样子,文章日常拖着写...
鞠躬,拜拜