关于加载状态的思考和尝试

2022-10-05 16:13:53 浏览数 (1)

在web项目开发中我们离不开网络加载,特别是移动设备网络未知情况很多。为了避免网络加载出现的白屏或者数据未展示完全的情况,我们常用loading或者骨架屏来进行体验上的优化。骨架屏相对于loading提供了更好的视觉效果和用户体验,但两者其根本上都不外乎是对加载状态的管理,当项目越来越大设计一个合适的且优雅的loading则需要考虑到更多的因素。下面内容主要围绕移动端

以react为例,最简单的loading大概是这样的,定义state状态,通过切换state状态来改变加载UI。

代码语言:javascript复制
const App = () => {
    const [loading, setLoad] = useState(false);
    
    useEffect(() => {
        setLoad(true);
        //...
        // 异步请求结束
        setLoad(false);
    }, []) 
    
    return loading ? <div>loading...</div> : <div>正文</div>
}

但以上方式存在三个问题:

  • 短暂的loading会导致页面出现闪烁的
  • 丑陋的三元表达式
  • 同样的逻辑页面过多后会导致重复的样板代码

那我们应该如何去设计一个loading来解决上面的问题呢?

短暂的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

React框架本身也考虑到这个点所以提出了Suspense,Suspense改变了我们思考加载状态的方式,即我们不应该将fetching component或data source耦合,而是应该更多的关注UI本身。Suspense可以让组件在渲染之前等待,即解决了组件和加载状态本身的抽离。如官方示例:

代码语言:javascript复制
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组件不仅仅是这些内容),支持指令和组件方式,避免重复封装

代码语言:javascript复制
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;
代码语言:javascript复制
.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转化为csssvgpnggif,很好用。

0 人点赞