在web项目开发中我们离不开网络加载,特别是移动设备网络未知情况很多。为了避免网络加载出现的白屏或者数据未展示完全的情况,我们常用loading或者骨架屏来进行体验上的优化。骨架屏相对于loading提供了更好的视觉效果和用户体验,但两者其根本上都不外乎是对加载状态的管理,当项目越来越大设计一个合适的且优雅的loading则需要考虑到更多的因素。下面内容主要围绕移动端
以react为例,最简单的loading大概是这样的,定义state状态,通过切换state状态来改变加载UI。
const App = () => {
const [loading, setLoad] = useState(false);
useEffect(() => {
setLoad(true);
//...
// 异步请求结束
setLoad(false);
}, [])
return loading ? <div>loading...</div> : <div>正文</div>
}
但以上方式存在三个问题:
那我们应该如何去设计一个loading来解决上面的问题呢?
通过使用延迟loading消失的时间,如:不管请求合适请求成功,都延迟500ms再消失loading。这样也就避免了闪烁的问题,但是在网络条件好的情况部分接口大概200ms就能获取到,这样做反而加大了用户等待时间,在此基础上我们可以再定义一个规则,假设设定为200ms内能请求到数据的就直接不显示loading,并且大于200ms小于500ms时,loading显示500ms,避免临界情况如请求时间为201ms时同样会出现闪烁情况,这样折中去优化。时间界定可以根据自身项目去定义。
通过封装通用组件/逻辑解决此问题,其中使用两种手段进行解决。一种是指令式、一种是组件方式。
优点:使用足够简单,代码简洁
缺点:灵活性较差,只能满足于loading,骨架屏需求相对难以应付。
优点:灵活性高,定制化强,能同时满足loading和骨架屏
缺点:使用上相对指令式要繁琐
两个方式都能解决以上部分问题,选择适合自己项目的方式就是最好的方式。如果使用指令式,我们可以通过把loading方法封装到http请求中,这样就可以把loading的逻辑隐藏在内部,专注于业务。如果使用组件式可以通过封装一个类似antd spin组件+state/redux的方式(dva-loading)。如果单单使用指令方式就没办法利用骨架屏提升体验,而组件的方式确实足够灵活也能处理骨架屏的问题,但是却没有完全消除重复繁琐的代码状态处理,是否有办法消除组件式的重复繁琐的使用方式呢,这才是我想要解决的问题。
React框架本身也考虑到这个点所以提出了Suspense,Suspense改变了我们思考加载状态的方式,即我们不应该将fetching component或data source耦合,而是应该更多的关注UI本身。Suspense可以让组件在渲染之前等待,即解决了组件和加载状态本身的抽离。如官方示例:
const resource = fetchProfileData();
function ProfilePage() {
return (
<Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails />
<Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline />
</Suspense>
</Suspense>
);
}
function ProfileDetails() {
// Try to read user info, although it might not have loaded yet
const user = resource.user.read();
return <h1>{user.name}</h1>;
}
function ProfileTimeline() {
// Try to read posts, although they might not have loaded yet
const posts = resource.posts.read();
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.text}</li>
))}
</ul>
);
}
Suspense让业务组件本身不再需要关注加载状态,我们也不用每次请求去切换状态,看似Suspense完美解决了我们加载状态的问题,但是在使用的时候发现,Suspense只是解决了“初始化”问题,如果一个表单进行提交需要loading时,Suspense并不能再次满足我们,现在Suspense用于获取数据还在实验性阶段,未来会变成什么样还是未知,但是确实是一个很棒的方式。
虽然只使用Suspense不能解决我们的问题,但我们可以针对上面的所有方案进行中和呢,根据自身业务,初始化时使用Suspense方式管理loading/骨架屏,而在用户操作时,一般情况是不想要用户再操作其他的内容(如:表单提交、下单),这时我们可以使用指令式loading,把loading直接封装在Http请求中,通过参数来判断是否使用loading。
现在整体的思路已经清晰及Suspense+指令调用组合,Suspense+骨架屏的方式管理初始化状态,指令调用管理操作时状态。这里我们需要对指令式loading组件进行封装并最终达到使用Loading.show()/Loading.hide()来实现加载的显示与隐藏。
这里做了一个Loading组件的简单实现(仅供思路参考,完善的loading组件不仅仅是这些内容),支持指令和组件方式,避免重复封装
import React, { PureComponent } from 'react';
import ReactDOM from 'react-dom';
import styles from './styles.module.css';
interface LoadingProps {
color?: string;
}
class Loading extends PureComponent<LoadingProps> {
static defaultProps = {
color: '#ffffff'
}
private static timer: NodeJS.Timeout | null;
private static startTime: number;
static container: HTMLDivElement | null;
static show() {
Loading.startTime = new Date().getTime();
Loading.timer = setTimeout(() => {
if (!Loading.container) {
const div = document.createElement('div');
document.body.appendChild(div);
Loading.container = div;
}
ReactDOM.render(<Loading />, Loading.container);
}, 200)
}
static hide() {
Loading.clearTime();
const diffTime = new Date().getTime() - Loading.startTime;
const delayTime = diffTime > 200 && diffTime < 500 ? 300 : diffTime;
Loading.timer = setTimeout(() => {
if (Loading.container) {
document.body.removeChild(Loading.container);
Loading.container = null;
}
}, delayTime <= 200 ? 0 : delayTime);
}
private static clearTime() {
Loading.timer && clearTimeout(Loading.timer);
Loading.timer = null;
}
componentWillUnmount() {
Loading.clearTime();
}
render() {
const { color } = this.props;
const style = { background: color };
return (
<div className={styles['loading']}>
<div className={styles['loading-container']}>
<div className={styles['loading-spinner']}>
<div className={styles['loading-content']}>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
<div style={style}></div>
</div>
</div>
</div>
</div>
)
}
};
export default Loading;
.loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0);
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
@keyframes loading-content {
0% {
opacity: 1
}
100% {
opacity: 0
}
}
.loading-content div {
left: 47px;
top: 24px;
position: absolute;
animation: loading-content linear 1s infinite;
background: #ffffff;
width: 6px;
height: 12px;
border-radius: 3px / 6px;
transform-origin: 3px 26px;
box-sizing: content-box;
}
.loading-content div:nth-child(1) {
transform: rotate(0deg);
animation-delay: -0.9166666666666666s;
background: #ffffff;
}
.loading-content div:nth-child(2) {
transform: rotate(30deg);
animation-delay: -0.8333333333333334s;
background: #ffffff;
}
.loading-content div:nth-child(3) {
transform: rotate(60deg);
animation-delay: -0.75s;
background: #ffffff;
}
.loading-content div:nth-child(4) {
transform: rotate(90deg);
animation-delay: -0.6666666666666666s;
background: #ffffff;
}
.loading-content div:nth-child(5) {
transform: rotate(120deg);
animation-delay: -0.5833333333333334s;
background: #ffffff;
}
.loading-content div:nth-child(6) {
transform: rotate(150deg);
animation-delay: -0.5s;
background: #ffffff;
}
.loading-content div:nth-child(7) {
transform: rotate(180deg);
animation-delay: -0.4166666666666667s;
background: #ffffff;
}
.loading-content div:nth-child(8) {
transform: rotate(210deg);
animation-delay: -0.3333333333333333s;
background: #ffffff;
}
.loading-content div:nth-child(9) {
transform: rotate(240deg);
animation-delay: -0.25s;
background: #ffffff;
}
.loading-content div:nth-child(10) {
transform: rotate(270deg);
animation-delay: -0.16666666666666666s;
background: #ffffff;
}
.loading-content div:nth-child(11) {
transform: rotate(300deg);
animation-delay: -0.08333333333333333s;
background: #ffffff;
}
.loading-content div:nth-child(12) {
transform: rotate(330deg);
animation-delay: 0s;
background: #ffffff;
}
.loading-spinner {
width: 80px;
height: 80px;
display: inline-block;
overflow: hidden;
background: rgba(0, 0, 0, .8);
border-radius: 10px;
}
.loading-content {
width: 100%;
height: 100%;
position: relative;
transform: translateZ(0) scale(.8);
backface-visibility: hidden;
transform-origin: 0 0;
}
关于Http请求库封装,现流行的有很多如:Fetch、Axios或swr、react-query、useReuqest这类hook请求方式,所以可根据自身项目选型进行二次封装,只需在请求前先Loading.show(),请求完毕后Loading.hide()即可,且支持loading选项可配。
或许最终的解决方案并不适合你的项目,但希望通过这些内容,能让你从中对这不起眼的加载状态引发新的思考,如有不同的想法评论区互相交流。总之针对自身业务选择最适合的方式即是最好的。顺便安利一个loading在线制作平台,LOADING.IO,可以把loading转化为css\svg\png\gif,很好用。