骨架屏是APP和移动端h5必备的东西,以下,我提供2个骨架屏的设计思路:
一、如果是SPA单页应用,比如react、vue、react-native、angular
一般在index.html中会有一个根节点
<div id="root"></div>
第一个方案就是在这个div中写死静态的html+css骨架屏代码,这也是我所推荐的,灵活性虽然不足,但是用户体验非常好,以react来举例,在数据没有被react-dom接管之前(接管后会替换掉根节点内的内容),立刻就可以被浏览器渲染展示,而不用等js执行
下面是完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="theme-color" content="#000000">
<!-- 启用浏览器的极速模式(webkit) -->
<meta name="renderer" content="webkit">
<!-- 默认宽度为设备宽度,缩放大小为 1,禁止缩放 -->
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover">
<!-- 忽略页面中的数字识别为电话,忽略email识别 -->
<meta name="format-detection" content="telphone=no, email=no"/>
<link rel="shortcut icon" href="<%= htmlWebpackPlugin.files.publicPath %>favicon.png">
<link rel="manifest" href="<%= htmlWebpackPlugin.files.publicPath %>manifest.json">
<link rel="stylesheet" href="<%= htmlWebpackPlugin.files.publicPath %>fonts_cms/iconfont.css">
<title>公司信息-xxxx</title>
<style>
/* 配置 rem */
html { font-size: calc(100vw / 26.8); }
body { margin: 8px 0 }
</style>
<style>
.skeleton_wrap {
margin-top: 76px;
padding: 0 15px 1.5rem 15px;
}
.linearGradient {
position: absolute;
z-index: 100;
top: 3.144rem;
width: 100%;
height: calc(100vh - 44px);
background-image: linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -o-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -moz-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -webkit-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -ms-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
}
.container {
padding: 1.3rem;
/* margin-top: 0.857rem; */
}
.wrapper {
/* height: 100vh; */
width: 100%;
margin-bottom: 2.144rem;
/* background-color: green; */
}
.title_100 {
width: 100%;
height: 1.858rem;
margin-bottom: 1.07rem;
background-color: rgba(0, 0, 0, 0.1);
}
.title_45 {
width: 45%;
height: 1.858rem;
margin-bottom: 1.07rem;
background-color: rgba(0, 0, 0, 0.1);
}
.paragraph_100 {
width: 100%;
/* height: 1rem;
margin-bottom: 1.07rem; */
background-color: rgba(0, 0, 0, 0.1);
height: 10px;
margin-bottom: 10px;
}
.paragraph_85 {
width: 85%;
/* height: 1rem;
margin-bottom: 1.07rem; */
background-color: rgba(0, 0, 0, 0.1);
height: 10px;
margin-bottom: 10px;
}
.paragraph_40 {
width: 40%;
/* height: 1rem;
margin-bottom: 1.07rem; */
background-color: rgba(0, 0, 0, 0.1);
height: 10px;
margin-bottom: 10px;
}
.imageContent_wrap {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.imageContent_circle {
display: inline-block;
/* width: 3.573rem;
height: 3.573rem; */
width: 30px;
height: 30px;
margin-right: 1.07rem;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.1);
}
.imageContent_column {
display: inline-block;
width: 50%;
/* height: 3.573rem; */
height: 30px;
}
.imageContent_content {
width: 80%;
/* height: 1rem; */
/* margin-bottom: 1.07rem; */
background-color: rgba(0, 0, 0, 0.1);
height: 10px;
margin-bottom: 10px;
}
.imageContent_content_no_margin {
margin-bottom: 0rem;
}
.animOpacityAni {
animation: opacityAni 1s 0s infinite;
-webkit-animation: opacityAni 1s 0s infinite;
-moz-animation: opacityAni 1s 0s infinite;
}
@keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
@-webkit-keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
@-moz-keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
</style>
</head>
<body>
<div id="root">
<div class="skeleton_wrap">
<!-- 标题部分 -->
<div class="wrapper animOpacityAni">
<div class="title_100"></div>
<div class="title_45"></div>
</div>
<!-- 图片+标题 -->
<div
class="wrapper animOpacityAni imageContent_wrap"
>
<div class="imageContent_circle"></div>
<div class="imageContent_column">
<div
class="imageContent_content"
></div>
<div
class="imageContent_content imageContent_content_no_margin"
></div>
</div>
</div>
<!-- 短段落 -->
<div>
<div class="paragraph_100"></div>
<div class="paragraph_100"></div>
<div class="paragraph_85"></div>
</div>
<!-- 中段落 -->
<div>
<div class="paragraph_100"></div>
<div class="paragraph_85"></div>
<div class="paragraph_100"></div>
<div class="paragraph_85"></div>
</div>
<!-- 长段落 -->
<div>
<div class="paragraph_100"></div>
<div class="paragraph_85"></div>
<div class="paragraph_100"></div>
<div class="paragraph_85"></div>
<div class="paragraph_40"></div>
</div>
</div>
</div>
</body>
</html>
二、接下来是第二种方案
以组件的思想进行组件封装📦,但是在详情数据请求回来,react执行js进行切换内容时,会有一定的时间花销,会有一段时间的白屏,性能不够好
首先是最终效果图(取截图,真实效果是会忽隐忽现)
调用各部分骨架的Skeleton.js
import React from 'react';
import moment from 'moment';
import styles from './skeleton.css';
import Title from './Title';
import Paragraph from './Paragraph';
import ImageContent from './ImageContent';
export default class extends React.Component {
state = {};
componentDidMount() {}
render() {
const largeSize = 30;
const Size = 10;
return (
<div>
<div className={styles.linearGradient}></div>
<div className={styles.container}>
<Title />
<ImageContent
circleStyle={{ width: largeSize, height: largeSize }}
descStyle={{ height: largeSize }}
columnItemStyle={{ height: Size, marginBottom: 10 }}
/>
<Paragraph type="small" rowStyle={{ height: Size, marginBottom: Size }} />
<Paragraph
type="middle"
rowStyle={{ height: Size, marginBottom: Size }}
/>
<Paragraph type="large" rowStyle={{ height: Size, marginBottom: Size }} />
</div>
</div>
);
}
}
skeleton.css
.linearGradient {
position: absolute;
z-index: 100;
top: 44px;
width: 100%;
height: calc(100vh - 44px);
background-image: linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -o-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -moz-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -webkit-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
background-image: -ms-linear-gradient(
top,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 1) 80%,
rgba(255, 255, 255, 1) 100%
);
}
.container {
padding: 20px;
margin-top: 12px;
}
.wrapper {
/* height: 100vh; */
width: 100%;
margin-bottom: 30px;
/* background-color: green; */
}
.title_100 {
width: 100%;
height: 26px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.title_45 {
width: 45%;
height: 26px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.paragraph_100 {
width: 100%;
height: 14px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.paragraph_85 {
width: 85%;
height: 14px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.paragraph_40 {
width: 40%;
height: 14px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.imageContent_wrap {
flex: 1;
flex-direction: row;
justify-content: space-between;
align-items: flex-start;
}
.imageContent_circle {
display: inline-block;
width: 50px;
height: 50px;
margin-right: 15px;
border-radius: 50%;
background-color: rgba(0, 0, 0, 0.1);
}
.imageContent_column {
display: inline-block;
width: 50%;
height: 50px;
}
.imageContent_content {
width: 80%;
height: 14px;
margin-bottom: 15px;
background-color: rgba(0, 0, 0, 0.1);
}
.imageContent_content_no_margin {
margin-bottom: 0px;
}
.animOpacityAni {
animation: opacityAni 1s 0s infinite;
-webkit-animation: opacityAni 1s 0s infinite;
-moz-animation: opacityAni 1s 0s infinite;
}
@keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
@-webkit-keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
@-moz-keyframes opacityAni {
0% {
opacity: 0.2;
}
50% {
opacity: 0.8;
}
100% {
opacity: 0.2;
}
}
Title.js
import React from 'react';
import moment from 'moment';
import styles from './skeleton.css';
export default class extends React.Component {
state = {};
componentDidMount() {}
render() {
return (
<div className={`${styles.wrapper} ${styles.animOpacityAni}`}>
<div className={styles.title_100}></div>
<div className={styles.title_45}></div>
</div>
);
}
}
Paragraph.js
import React from 'react';
import moment from 'moment';
import styles from './skeleton.css';
export default class extends React.Component {
state = {};
componentDidMount() {}
judgeType() {
const { type } = this.props;
switch (type) {
case 'small':
return this.renderSmall();
break;
case 'middle':
return this.renderMiddle();
break;
case 'large':
return this.renderLarge();
break;
}
}
renderSmall() {
const { rowStyle } = this.props;
return (
<div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_85} style={rowStyle}></div>
</div>
);
}
renderMiddle() {
const { rowStyle } = this.props;
return (
<div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_85} style={rowStyle}></div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_85} style={rowStyle}></div>
</div>
);
}
renderLarge() {
const { rowStyle } = this.props;
return (
<div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_85} style={rowStyle}></div>
<div className={styles.paragraph_100} style={rowStyle}></div>
<div className={styles.paragraph_85} style={rowStyle}></div>
<div className={styles.paragraph_40} style={rowStyle}></div>
</div>
);
}
render() {
const renderContent = this.judgeType();
return (
<div className={`${styles.wrapper} ${styles.animOpacityAni}`}>
{renderContent}
</div>
);
}
}
ImageContent.js
import React from 'react';
import moment from 'moment';
import styles from './skeleton.css';
export default class extends React.Component {
state = {};
componentDidMount() {}
render() {
const { circleStyle, descStyle, columnItemStyle } = this.props;
return (
<div
className={`${styles.wrapper} ${styles.animOpacityAni} ${styles.imageContent_wrap}`}
>
<div className={styles.imageContent_circle} style={circleStyle}></div>
<div className={styles.imageContent_column} style={descStyle}>
<div
className={styles.imageContent_content}
style={columnItemStyle}
></div>
<div
className={`${styles.imageContent_content} ${styles.imageContent_content_no_margin}`}
style={columnItemStyle}
></div>
</div>
</div>
);
}
}
如何使用
render() {
const { navTitle, barVisible, data } = this.state;
const { detail } = this.props;
return (
<React.Fragment>
{!!data ? (
<div>
<App
{...this.props}
detail={data || detail}
barVisible={barVisible}
setNavTitle={navTitle => this.setState({ navTitle })}
hideFontBar={() => this.toggleBarVisible(false)}
/>
</div>
) : (
<Skeleton />
)}
</React.Fragment>
);
}