前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >关于加载状态的思考和尝试

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

作者头像
gary12138
发布2022-10-05 16:13:53
4910
发布2022-10-05 16:13:53
举报
文章被收录于专栏:前端随笔

在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转化为css\svg\png\gif,很好用。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-03-25 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 短暂的loading会导致页面出现闪烁的
  • 丑陋的三元表达式和重复的样板代码
    • 指令式
      • 组件式
        • React Suspense
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档