实现带主题的 react 通用骨架屏

marvin

marvin

实现带主题的 react 通用骨架屏

假定我们的页面左侧由一个文章列表, 右侧有一个用户信息的侧边栏, 现在需要为这些生成一些骨架屏用来代替加载状态组件, 我们可以通过定义一个自定义的 Skeleton 组件来实现, 通过给 Skeleton 组件传入不同的类型来显示不同的元素的骨架样式

基础的骨架屏组件

首先需要定义一个 Skeleton 组件, 通过 type 来定义 class, 不同的 type 定义不同的样式:

// SkeletonElement.js
import React from 'react'

const SkeletonElement = ({ type }) => {
	const classes = `skeleton ${type}`;

	return (
		<div className={classes}></div>
	)
}

export default SkeletonElement;

为 SkeletonElement 组件定义样式

不同的元素例如文字, 标题, 头像等它们的宽高和形状都不一样, 需要单独定义:

.skeleton {
	background-color: #ddd;
	margin: 10px 0;
	border-radius: 4px;

	&.text {
		width: 100%;
		height: 12px;
	}

	&.title {
		width: 50%;
		height: 20px;
		margin-bottom: 15px;
	}

	&.avatar {
		width: 100px;
		height: 100px;
		border-radius: 50%;
	}

	&.thumbnail {
		width: 100px;
		height: 100px;
	}
}

有了基础的组件, 接下来可以定义文章列表的部分了

构建文章骨架屏

文章由标题和多行文本组成, 这里用三个 text 类型表示, 为了定义文章列表的样式, 我们在 SkeletonElement 的基础上再定义一个 SkeletonArticle 组件用来包装样式:

import SkeletonElement from './SkeletonElement';

const SkeletonArticle = () => {
	return (
		<div className="skeleton-wrapper">
			<div className="skeleton-article">
				<SkeletonElement type="title" />
				<SkeletonElement type="text" />
				<SkeletonElement type="text" />
				<SkeletonElement type="text" />
			</div>
		</div>
}

组件定义好了, 接下来就需要定义样式

文章骨架屏的样式

这里为文章列表单独定义样式, 其中 position 和 overflow 是为了 shimmer 动画设置的, 可以让动画定位和不超出宽度:

.skeleton-wrapper {
	margin: 20px auto;
	padding: 10px 15px;
	border-radius: 4px;
	position: relative;
	overflow: hidden;
}

文章部分完成了, 接下来看用户信息部分

构建用户信息骨架屏

用户信息由用户头像, 标题, 多行描述文本组成, 这里用两行 text 类型表示:

import React from 'react'

const SkeletonProfile = () => {
	return (
		<div className="skeleton-wrapper">
			<div className="skeleton-profile">
				<div>
					<SkeletonElement type="avatar" />
				</div>
				<div>
					<SkeletonElement type="title" />
					<SkeletonElement type="text" />
					<SkeletonElement type="text" />
				</div>
			</div>
		</div>
	)
}

组件完成,我们还需要将头像和文本部分对齐

用户信息骨架屏的样式

这里将整个容器设置为 grid, 居中对齐, 头像部分占一份, 文本部分占两份

.skeleton-profile {
	display: grid;
	grid-template-columns: 1fr 2fr;
	gap: 30px;
	align-items: center;
}

为文章骨架屏添加主题

为了支持主题颜色修改, 这里需要修改 SkeletonArticle 组件, 传入 theme 参数

const SkeletonArticle = ({ theme }) => {
	return (
		<div className={`skeleton-wrapper ${themeClass}`}>
			{/* ... */}
		</div>
	)
}

对于不同的 theme 参数分别定义背景色

为文章骨架屏添加主题颜色

这里支持白色和黑色主题, 其中黑色主题的 SkeletonElement 元素颜色更深:

.skeleton-wrapper.light {
	background-color: #f2f2f2;
}

.skeleton-wrapper.dark {
	background-color: #444;
}

.skeleton-wrapper.dark .skeleton {
	background-color: #777;
}

为骨架屏添加动画

我们的骨架屏还缺少动画, 这里修改 SkeletonArticle 组件, 新增 Shimmer 组件, 作为动画层覆盖在 SkeletonElement 组件之上:

import Shimmer from './Shimmer'

const SkeletonArticle = ({ theme }) => {
	return (
		<div className={`skeleton-wrapper ${themeClass}`}>
			{/* ... */}
			<Shimmer />
		</div>
	)
}

Shimmer 定义如下:

import React from 'react'

const Shimmer = () => {
	return (
		<div className="shimmer-wrapper">
			<div className="shimmer"></div>
		</div>
	)
}

export default Shimmer;

定义 Shimmer 的样式

shimmer 组件由 shimmer-wrapper 和 shimmer 类组成, 下面分别设置样式:

.shimmer-wrapper {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	animation: loading 2.5s infinite;
}

.shimmer {
	width: 50%;
	height: 100%;
	background-color: rgba(255, 255, 255, .8);
	transform: skewX(-20deg);
	box-shadow: 0 0 30px 30px rgba(255, 255, 255, .05);
}

.dark .shimmer {
	background-color: rgba(255, 255, 255, .05);
}

@keyframes loading {
	0% {
		transform: translateX(-150%);
	}

	50% {
		transform: translateX(-60%);
	}

	100% {
		transform: translateX(150);
	}
}

这里让 shimmer-wrapper 绝对定位, 并定义了从左到右的平移动画 loading 应用到 wrapper 上, 内部 shimmer 通过 transform 将 shimmer 斜着放置, 最后添加了一个阴影使背景更加明显.

写在最后

以上就实现了一个通用骨架屏, 但是如果有更多的页面和组件, 还是需要在此基础上封装不同的 Skeleton.