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

吐槽君 分类:javascript

在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来解决上面的问题呢?

短暂的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可以让组件在渲染之前等待,即解决了组件和加载状态本身的抽离。如官方示例:

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

.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,很好用。

往期回顾

  • 逐步拆解React组件--Swipe轮播图
  • 逐步拆解React组件—Lazyload懒加载

最后

觉得有用?喜欢就收藏,顺便点个赞吧,你的支持是我最大的鼓励!觉得没用?评论区交流您的想法,虚心接受您的指导。

回复

我来回复
  • 暂无回复内容