智游云行项目难点解析(1):从基础使用到真实应用--骨架屏详解

108 阅读7分钟

前言

大家好,最近作者花了几天时间做了个React的综合前端项目--智行出游,收获颇多,准备花几篇文章来分享下项目中遇到的难点,也借此来给大家分享下相关的知识

项目部分截图

6a713250fe8b739af8aae3b0a66e242.jpg

这篇文章是项目难点解析的第一篇:骨架屏,我会结合骨架屏的基本用法和应用到项目的角度来详细拆解:

1.如何基本使用骨架屏

2.如何在你的项目中使用骨架屏

项目骨架屏图片

image.png


骨架屏的基本使用

如果你想要使用骨架屏的话,从实战经验来说,我建议你直接使用React-vant中的Skeleton 骨架屏组件(别的组件库里的骨架屏也是一样的用法),简单好用,这篇文章我们以实战使用为主,就直接用组件来演示了

(前置工作: 创建一个react项目,安装react-vant依赖)

1. 创建一个骨架屏组件

按下面的方式,引入react-vant中的组件,你就可以直接在jsx中使用该组件了,

import { Skeleton } from 'react-vant'

const BasicSkeleton = () => {
    return (
        <div>
            {/* 标题骨架 */}
            <Skeleton title row={2} />
            
            {/* 头像骨架 */}
            <Skeleton avatar size="40px" />
            
            {/* 普通骨架 */}
            <Skeleton />

        </div>
        
    )
}

export default BasicSkeleton;

2.查看效果

我们将这个骨架屏组件放在App.jsx中,然后跑通项目就能看到我们刚刚的骨架组件屏组件

image.png

可以看到,这就是它的效果,下面来介绍一些骨架屏的定制性,让我们能够像搭积木一样自己随意定制一个骨架屏。

3.设置属性 自定义骨架屏

我们可以通过为Skeleton组件设置一些属性来切换当前骨架屏的页面效果

import { Skeleton } from 'react-vant'

const BasicSkeleton = () => {
    return (
        <div>
            {/* 标题骨架 :将段落行数从2改为了1,设置段落占位高度为50*/}
            <Skeleton title row={1} rowHeight={50}/> 
            
            {/* 头像骨架类  将头像占位图大小设置为60px 段落占位高度为50*/}
            <Skeleton avatar rowHeight={50} avatarSize="60px"/>
            
            {/* 删除了第三个骨架类*/}
        </div>

        
    )
}

export default BasicSkeleton;

让我们看看变化吧:

image.png

这里只是展示了部分主要的可选属性,react-vant的骨架屏还有很多属性可以被设置,可以让我们十分方便地定制化一个骨架屏

下面是react-vant官方文档的骨架屏属性列表:

image.png


真实开发经验:项目中使用骨架屏

下面就来讲讲,我如何将骨架屏应用到我的项目中的吧!

使用骨架屏的目的

首先说说项目中为什么要使用骨架屏吧!

当用户第一次访问我的页面时:

项目加载图片

image.png

可以看到,用户并不能马上看到所有的资源全部显示完全,事实上通过查看网络请求我们发现:

image.png

很多图片信息需要好几秒才能拿到,很慢对吧。

所以我的项目不像平常我们使用各大App,它们往往在打开时几十毫秒内就能拿到所有的图片, 这是因为我的项目部署的服务器是很小的一个服务器(2核4G),所以它的性能并没有那么强,图片比较多的话,网络请求对我的小服务器的压力就会较大,所以就会很慢很慢才能给用户展示完整的数据

所以我们需要设置骨架屏,优化用户体验!

我们使用的各大App能够在几十毫秒内加载完所有的图片一方面是刚刚提到的因为它们的服务器比较强大,另一方面是因为,我们当前的网速较好,倘若当前的网速不好的话,那么同样你在访问各大App时仍然需要像我项目这样等待图片加载完成

所以各大App也几乎都设置了骨架屏,让用户看到骨架屏,有一种“数据即将呼之欲出”的感觉,这样的话,用户能够更乐于等待,所以:骨架屏必不可少!!!


如何设计一个骨架屏

有了使用骨架屏的目的之后,就让我们先来设计一个骨架屏。

上面我们介绍了如何基础使用骨架屏,我们要在这个基础上根据我们的项目的页面特点,来为这个页面定制化骨架屏!

原页面(我们需要根据这个页面的大概结构设计骨架屏也类似于这个页面) image.png

让我们在components文件夹中创建一个mySkeleton文件,在里面设计我们的骨架屏(待会要将它导出给首页页面组件使用):

根据页面的结构设计:

首先给大家看到的是我的Home页面的结构,我们的设计当然要基于这个结构来进行,这里也包含了骨架屏的逻辑代码,这是最后一部分我要给大家详谈的。

 <div>
            {loading && <AnimatedSkeleton />}
            {!loading && (
                <>
                    <div className={styles.header}>
                        <Header />
                    </div>
                    <div className={styles.adver}>
                        <Adver />
                    </div>
                    <div className={styles.swiper}>
                        <Swiper />
                    </div>
                    <div className={styles.recommand}>
                        <Recommand />
                    </div>
                </>
            )}
        </div>

所以就有了骨架屏的结构:下面直接给出骨架屏源码

const homeSkeleton = () => {
 return (
        <div className={styles.container}>
            {/* 搜索栏骨架屏 */}
            <div className={styles.searchSection}>
                <div className={styles.searchBar}>
                    <Skeleton title row={1} />
                </div>
            </div>
            
            {/* 网格图标骨架屏 */}
            <div className={styles.gridSection}>
                <div className={styles.gridContainer}>
                    {Array.from({ length: 15 }).map((_, index) => (
                        <div key={index} className={styles.gridItem}>
                            <div className={styles.iconSkeleton}>
                                <Skeleton avatar size="60px" />
                            </div>
                        </div>
                    ))}
                </div>
            </div>
            
            {/* 广告区域骨架屏 */}
            <div className={styles.adverSection}>
                <div className={styles.adverGrid}>
                    {Array.from({ length: 4 }).map((_, index) => (
                        <div key={index} className={styles.adverItem}>
                            <div className={styles.adverTitle}>
                                <Skeleton title row={1} />
                            </div>
                            <div className={styles.adverImage}>
                                <Skeleton image width="100%" height="80px" />
                            </div>
                        </div>
                    ))}
                </div>
            </div>
            
            {/* 轮播图骨架屏 */}
            <div className={styles.swiperSection}>
                <div className={styles.swiperContainer}>
                    <Skeleton image width="100%" height="200px" />
                </div>
            </div>
            
            {/* 推荐列表骨架屏 */}
            <div className={styles.recommandSection}>
                <div className={styles.recommandTitle}>
                    <Skeleton image width="100%" height="60px" />
                </div>
                <div className={styles.cardGrid}>
                    {Array.from({ length: 6 }).map((_, index) => (
                        <div key={index} className={styles.cardItem}>
                            <div className={styles.cardImage}>
                                <Skeleton image width="100%" height="180px" />
                            </div>
                            <div className={styles.cardContent}>
                                <div className={styles.cardTitle}>
                                    <Skeleton title row={2} />
                                </div>
                                <div className={styles.cardLocation}>
                                    <Skeleton title row={1} />
                                </div>
                                <div className={styles.cardFooter}>
                                    <div className={styles.cardPrice}>
                                        <Skeleton title row={1} />
                                    </div>
                                    <div className={styles.cardStats}>
                                        <Skeleton title row={1} />
                                    </div>
                                </div>
                            </div>
                        </div>
                    ))}
                </div>
            </div>
        </div>
       }

可以看到这里:

  • 大量使用了Skeleton组件
  • 自己也定义了很多盒子,待会我们按照Home原页面的样式给这些盒子定义样式
  • 骨架屏的结构和Home的结构十分相似

接下来就是根据Home的样式,来书写骨架屏的css了,比如Home的头部search部分的颜色是浅蓝色,那么骨架屏的searchSection也需要设置成浅蓝色,这样就能达到我们开始给大家演示的效果了


骨架屏的应用

好了,现在骨架屏也设计完了,那么骨架屏到底该如何放呢?

先讲讲它的放置时机:

  1. 页面初始加载时:当用户首次访问页面或刷新时
  2. 内容更新时:当页面部分内容需要重新加载或更新时
  3. 路由切换时:在单页应用(SPA)中切换路由时
  4. 数据获取时:当发起API请求等待响应时

一般来讲是会为骨架屏设置一个loading,控制着骨架屏是否显示

当上面提到的事情做完时(异步任务或等待网络请求),就将骨架屏设置为true

function MyComponent() {
  const [isLoading, setIsLoading] = useState(true);
  const [data, setData] = useState(null);

  useEffect(() => {
    这里举的是异步请求的例子 实际上任何需要等待的请求/网络事件都可以利用这种方式
    fetchData().then(data => {
      setData(data);
      setIsLoading(false);
    });
  }, []);

  return (
    <div>
      {isLoading ? (
        <Skeleton />
      ) : (
        <ActualContent data={data} />
      )}
    </div>
  );
}

我的项目中的应用

在我的项目中,fetch请求什么之类的都特别块,我上线后,用户访问时,在几百毫秒内拿到数据,但是由于我的项目中包含着许许多多的图片,所以图片加载地很慢,就像上面看到的加载页面图片一样。

于是我的需求就是,先设置一个骨架屏,做图片预加载,待图片加载完了,再让用户看到真实的内容

下面是我实现的相关代码:

 const imagesToPreload = [
        // Adver 图片
        Adver1, Adver2, Adver3, Adver4, RecommandAdv,
        // Swiper 图片
        Swiper1, Swiper2, Swiper3, Swiper4, Swiper5, Swiper6, Swiper7,
        // Recommand 图片
        南昌之星, 南昌梅岭
    ]

    // 预加载图片函数
    const preloadImages = async (images) => {
        console.log('开始预加载Home页面图片,共', images.length, '张')
        
        const preloadPromises = images.map((imageSrc, index) => {
            return new Promise((resolve) => {
                if (preloadedImagesRef.current.has(imageSrc)) {
                    resolve()
                    return
                }
                
                const img = new Image()
                img.onload = () => {
                    preloadedImagesRef.current.add(imageSrc)
                    console.log(`图片 ${index + 1}/${images.length} 预加载完成:`, imageSrc)
                    resolve()
                }
                img.onerror = () => {
                    console.warn(`图片 ${index + 1}/${images.length} 预加载失败:`, imageSrc)
                    resolve() // 即使失败也完成,不阻塞其他图片
                }
                img.src = imageSrc
            })
        })
        
        try {
            await Promise.all(preloadPromises)
            console.log('Home页面所有图片预加载完成!')
        } catch (error) {
            console.error('图片预加载过程中出现错误:', error)
        }
    }

    useEffect(() => {
        // 启动预加载和骨架屏计时器
        const startPreload = async () => {
            // 立即开始预加载图片
            await preloadImages(imagesToPreload)
            
            setLoading(false)


            return () => clearTimeout(timer) 
        }

        startPreload()
    }, [])

可以看到,使用了异步事件确保所有图片加载完成之后,才会显示页面。就能明显提升用户体验啦!


总结

这篇文章为你讲解了如何使用骨架屏,结合我的项目实例,模拟在真实开发时骨架屏的使用场景,你也可以学习后为你的项目设置不同的骨架屏,相信我,使用后,你的项目的用户体验将会提到大大地提升!!!

创作不易:如果内容对你有帮助的话,可以帮忙点个赞呀