文章目录
- 背景
- 实现方法
- (一) react组件静态方法
- (二) 通过静态方法改变组件的状态
- (三) 指定进度条的步幅规则
- (四) 成功和失败的状态改变
- 1. 成功
- 2. 失败
- (五) 组件消失
- (六) 背景遮罩
- 最终实现及代码
- 组件代码
- 模拟调用进度条组件的代码
- 可能遇到的问题
- 静态方法调不到/报错
- 组件渲染两次
背景
react项目中, 有些页面加载很慢, 为了提升用户体验, 需要在前端做一个伪进度条, 并且把这个伪进度条封装成一个组件, 提供给多个项目使用.
这个伪进度条有以下特点:
- 基于antd组件进行二次开发. 利用 antd 的 Spin, Progress 组件开发, 减少工作量.
- 在进度条走到 99% 的时候会卡住, 等拿到响应的时候完成.
- 考虑到要把这个伪进度条封装成组件, 所以最好是用类似
message.info()
这种方法调用, 不然不好对响应之后的组件状态进行改变. 因此要用到react组件的静态方法. - 我用的函数组件 (当然用类组件也是可以的).
实现方法
(一) react组件静态方法
这种方式感觉平常开发很少用到, 放一个 gpt 给的示例:
React组件中使用静态方法可以有多种方式。下面是一个使用静态方法的React组件示例:
import React from 'react';
class MyComponent extends React.Component {
static myStaticMethod() {
console.log('This is a static method.');
}
render() {
return (
<div>
{/* 组件内容 */}
</div>
);
}
}
// 在其他地方调用静态方法
MyComponent.myStaticMethod();
export default MyComponent;
在上面的代码中,myStaticMethod是一个静态方法,可以直接通过组件类名调用,例如MyComponent.myStaticMethod()。
请注意,在React组件的静态方法中,无法访问组件的实例属性或实例方法,因为静态方法是属于类本身的,而不是实例。
另外,你也可以使用ES6的类静态属性语法来定义静态方法:
import React from 'react';
class MyComponent extends React.Component {
static myStaticMethod = () => {
console.log('This is a static method.');
}
render() {
return (
<div>
{/* 组件内容 */}
</div>
);
}
}
// 在其他地方调用静态方法
MyComponent.myStaticMethod();
export default MyComponent;
以上是在类声明中定义的静态方法,也可以在函数式组件中使用静态方法。下面是一个函数式组件中使用静态方法的示例:
import React from 'react';
function MyFunctionalComponent() {
return (
<div>
{/* 组件内容 */}
</div>
);
}
MyFunctionalComponent.myStaticMethod = () => {
console.log('This is a static method.');
};
// 在其他地方调用静态方法
MyFunctionalComponent.myStaticMethod();
export default MyFunctionalComponent;
我习惯用函数组件, 所以采用了函数组件的静态方法写法.
再加上使用了 antd 的 Spin 和 Progress 组件, 得到了下面的组件代码:
import React,{useState,useEffect,memo} from 'react';
import {Progress,Spin} from 'antd';
import './index.css'
const SelfDevProgress = (props)=> {
return(
<div className='outer'>
<div className='inner'>
<p>系统正在全力加载中, 请稍后...</p>
<Spin />
<Progress percent={70} status={"active"}/>
</div>
</div>
)
}
// 静态调用方式
SelfDevProgress.success = (message) => {
console.log(message,"success")
}
export default SelfDevProgress;
外部调用:
import React from 'react';
import SelfDevProgess from "./modules/selfDevProgess"
const Main = () => {
SelfDevProgess.success("成功提示")
return(
<div>
<p>模块组件页面testtest父页面</p>
{/* <SelfDevProgess time={5}/> */}
</div>
)
}
export default Main;
css 文件:
.outer {
}
.inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 60%;
text-align: center;
}
但这时候, 只能知道静态方法调用成功了, 没法通过静态方法去改变组件的状态, 比如进度条的 percent 和 status , 我尝试在静态方法里调用 SelfDevProgress(percent:100) 这种方式去传值, 会报错.
(二) 通过静态方法改变组件的状态
改变状态需要借助另外一个函数来实现, 写一个 SelfDevProgressAPI
函数, 让它来作为一个中转站.
这个函数的作用是:
1. 允许在静态方法里调用, 接收父组件调用的时候传过来的参数, 并对这些参数进行处理;
2. 创建div, 把之前写的 SelfDevProgess
组件的html元素挂载到这个div上, 让组件的html通过这个div展示出来;
注意:
1. 必须给挂载的div设置一个唯一的id, 如果不给设置一个唯一的id,每次调用都会挂载一个div,这个div会重叠很多次;
2. 这个方法里面是不能用 return 这种方式去展示的, 要用 ReactDom.render( )这种方式挂载; 如果 ReactDom 报错的话, 要引入一下ReactDOM.
import ReactDOM from 'react-dom';
ReactDOM.render 是 React 的最基本方法用于将模板转为 HTML 语言,并插入指定的 DOM 节点。ReactDOM.render(template,targetDOM) 方法接收两个参数:
- 第一个是创建的模板,多个 dom 元素外层需使用一个标签进行包裹,如
<div>
; - 第二个参数是插入该模板的目标位置。
const SelfDevProgressAPI = (type) => {
const per = (type="success") ? 100 : 70
// 创建一个div,把它挂载到body元素的下面(因为这个进度条是相对于整个页面的)
// 如果不给设置一个唯一的id,每次调用都会挂载一个div,这个div会重叠很多次
let container = document.querySelector('#selfDevProgress-container');
if (!container) {
container = document.createElement('div');
container.id = 'selfDevProgress-container';
document.body.appendChild(container);
}
// react17的写法, 把组件渲染到刚刚创建的div上
ReactDOM.render(
<SelfDevProgress percent={per}/>,
container
);
// 注意这个方法里面是不能用return这种方式去展示的
// return(
// <div>
// <SelfDevProgress percent={per}/>
// </div>
// )
}
// 静态调用方式
SelfDevProgress.success = (message) => {
console.log(message,"success")
SelfDevProgressAPI("success")
}
(三) 指定进度条的步幅规则
这个可以自由指定, 但是要注意几点:
1. 最好不要出现小数的步幅, 小数出现在进度条上有点不太好看;
2. 在最后的时候要有卡顿, 比如说从90%的时候开始走的变慢, 或者卡在99%的位置;
以下是我指定的一些规则:
进度条时间(duration) | 规则 | 定时器时间间隔 | 步幅(s) |
---|---|---|---|
/ | / | 100ms | 默认为2 |
duration > 10 秒 | 10s走到99%, 然后等待 | 100ms | 1 |
10 >= duration > 5 | 5s走到98%, 这时候将步幅改为 1, 走到99%等待 | 100ms | 2 |
5 >= duration > 2 | 2s走到95%, 这时候将步幅改为 1, 走到99%等待 | 100ms | 5 |
duration <=2 | 1s走到90%, 这时候将步幅改为 1, 走到99%等待 | 100ms | 10 |
/* 进度条展示规则
如果没有设置duration,就按 5s 处理
1.duration > 10 秒, 10s走到99%,然后等待
2.10 >= duration > 5, 5s走到98%, 这时候将步幅改为 1, 走到99%等待
3.5 >= duration > 2, 2s走到95%, 这时候将步幅改为 1, 走到99%等待
4.duration <=2, 1s走到90%, 这时候将步幅改为 1, 走到99%等待
*/
(四) 成功和失败的状态改变
1. 成功
成功比较简单, 成功状态之前让进度条卡在99%, 成功时需要:
(1) 将进度条状态跳到 100%
(2) 进度条状态改为 success 成功状态 (变为绿色)
(3) 改变 loading 图标的状态
(4) 清除定时器
(5) 进度条整个消失
2. 失败
失败时需要:
(1) 进度条卡在目前的数值
(2) 进度条状态改为 exception 失败状态 (变为红色)
(3) 改变 loading 图标的状态
(4) 清除定时器
(5) 进度条整个消失
这里面第一步将 “进度条卡在目前的数值” 容易遇到问题. 比如: 在调用clearInterval()之后,setInterval()循环仍在运行, 进度条会在变红的情况下继续前进.
解决方法可以查看: 调用clearInterval(), 定时器仍在进行
(五) 组件消失
首先想到的是设置 display:"none"
这种方式, 但是感觉有点太生硬了, 唰的一下就突然消失了, 所以想给加一个过渡.
试了一下将 width 和 height 设为 0 不太好使, 最后的实现方法是: "结束的时候改变透明度, 从1到0, 用 transition 加一个过渡动画, 当透明度为 0 的时候, 再将 display 设为 none.
至于进度条出现的时候要不要采用渐进的方式, 我觉得没有必要, 所以就没有加.
// 控制进度条整个组件是否展示
const isShow = (state) => {
let container = document.querySelector('#selfDevProgress-container');
if(state) {
container.style.display = "block"
container.style.opacity = "1"
} else {
let t1 = setTimeout(() => {
container.style.transition = "opacity 0.5s ease-out"
container.style.opacity = "0"
let t2 = setTimeout(() => {
container.style.display = "none"
clearTimeout(t2)
}, 500);
clearTimeout(t1)
}, 500);
}
}
效果是这样的:
进度条组件消失视频
(六) 背景遮罩
遮罩思路:
-
#selfDevProgress-container
中利用 position 属性定义好位置, 以及设置好宽高 (因为这个进度条组件是相对于整个页面的, 所以这个div应该覆盖整个可视页面); -
#self-dev-outer
中继承父组件的宽高, 利用背景色透明度来加遮罩;
最终实现及代码
整个工程压缩包已上传资源.
demo中涉及到的几个文件的结构:
组件代码
index.jsx:
import React,{useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
import {Progress,Spin} from 'antd';
import './index.css'
const SelfDevProgress = (props)=> {
const {percent,duration,msg,mask} = props;
console.log("mask",mask)
const [getChangePer,setGetChangePer] = useState(percent)
const [perStatus,setPerStatus] = useState("active")
const [timer,setTimer] = useState(null) // 定时器
let s = 2 ; // 进度条步幅,默认为2 (5s)
// let timer = null; // 定时器
/* 进度条展示规则
如果没有设置duration,就按 5s 处理
1.duration > 10 秒, 10s走到99%,然后等待
2.10 >= duration > 5, 5s走到98%, 这时候将步幅改为 1, 走到99%等待
3.5 >= duration > 2, 2s走到95%, 这时候将步幅改为 1, 走到99%等待
4.duration <=2, 1s走到90%, 这时候将步幅改为 1, 走到99%等待
*/
const rule = () => {
switch (true) { // 这个switch里面是不能写duration的,否则会失效
case duration > 10:
s = 1
break;
case duration > 5 && duration <= 10:
s = 2
break;
case duration > 2 && duration <= 5:
s = 5
break;
case duration <= 2:
s = 10
break;
default:
break;
}
}
// 控制进度条整个组件是否展示
const isShow = (state) => {
let container = document.querySelector('#selfDevProgress-container');
if(state) {
container.style.opacity = "1"
container.style.display = "block"
} else {
let t1 = setTimeout(() => {
container.style.transition = "opacity 1s ease-out"
container.style.opacity = "0"
let t2 = setTimeout(() => {
container.style.display = "none"
clearTimeout(t2)
}, 1000);
clearTimeout(t1)
}, 500);
}
}
// 控制进度条组件上面的loading标识
const msgSpin = (state) => {
const spin = document.querySelector('#self-dev-spin')
if(!state) {
spin.style.visibility = "hidden"
} else {
spin.style.visibility = "visible"
}
}
// 调起进度条
const initial = async () => {
// 判断是否有遮罩
let outer = document.querySelector('#self-dev-outer');
if(mask) {
outer.style.backgroundColor = 'rgba(255,255,255,0.7)'
} else {
outer.style.backgroundColor = 'unset'
}
// 获得进度条步幅
await rule()
// 显示进度条
isShow(true)
// 改变进度条状态
msgSpin(true)
setPerStatus("active")
// 进度条步数
let per = 0
setGetChangePer(per)
let t = setInterval(() => {
per += s
if(per === 99) { // 在100之前要卡住停顿
clearInterval(t)
}
if(per + s >= 100) { // 最后一段改变步幅
s = 1
}
setGetChangePer(per)
}, 100);
setTimer(t)
}
// 响应成功
const success = () => {
// 清除定时器
clearInterval(timer)
// 改变进度条状态
setGetChangePer(100)
setPerStatus("success")
// 改变图标
msgSpin(false)
// 进度条消失
isShow(false)
}
// 响应失败
const fail = () => {
// 清除定时器
clearInterval(timer)
// 改变进度条状态
setPerStatus("exception")
// 改变图标
msgSpin(false)
// 进度条消失
isShow(false)
}
useEffect(()=>{
switch (percent) {
case 0:
initial()
break;
case 100 :
success()
break;
case 50:
fail()
break;
default:
break;
}
},[percent])
return(
<div id='self-dev-outer'>
<div className='self-dev-inner'>
<p id='self-dev-msg'>{msg}</p>
<Spin id='self-dev-spin'/>
<Progress percent={getChangePer} status={perStatus}/>
</div>
</div>
)
}
const SelfDevProgressAPI = ({type,mask,msg,duration}) => {
let per = 0
switch (type) {
case "start":
per = 0
break;
case "success":
per = 100
break;
case "fail":
per = 50
break;
default:
break;
}
// 创建一个div,把它挂载到body元素的下面(因为这个进度条是相对于整个页面的)
let container = document.querySelector('#selfDevProgress-container');
if (!container) {
container = document.createElement('div');
container.id = 'selfDevProgress-container';
document.body.appendChild(container);
}
// react 17 写法, 把组件渲染到刚刚创建的div上
ReactDOM.render(
<SelfDevProgress percent={per} duration={duration} msg={msg} mask={mask}/>,
container
);
}
// 静态调用方式
/*
duration: 进度条持续的时间(number)
msg: 成功或失败的文字提示(string)
mask: 是否有遮罩(Boolean)
steps:步骤(array)这个参数暂时没用到
*/
SelfDevProgress.start = (mask,duration,msg) => {
let obj = {
type:"start",
mask: mask===undefined ? false : mask,
msg: msg ? msg : "系统正在全力加载中, 请稍后...",
duration: duration ? duration : 4,
}
SelfDevProgressAPI(obj)
}
SelfDevProgress.success = (msg) => {
let obj = {
type:"success",
msg: msg ? msg : "加载成功",
}
SelfDevProgressAPI(obj)
}
SelfDevProgress.fail = (msg) => {
let obj = {
type:"fail",
msg: msg ? msg : "加载失败",
}
SelfDevProgressAPI(obj)
}
export default SelfDevProgress;
index.css 代码:
#selfDevProgress-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
#self-dev-outer {
width: 100%;
height: 100%;
}
.self-dev-inner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
width: 60%;
text-align: center;
z-index: 99;
}
模拟调用进度条组件的代码
import React from 'react';
import SelfDevProgess from "./modules/selfDevProgess"
import './main.css'
const Main = () => {
let t0 = setTimeout(() => {
SelfDevProgess.start()
clearTimeout(t0)
}, 2000);
let t1 = setTimeout(() => {
// SelfDevProgess.fail()
SelfDevProgess.success()
clearTimeout(t1)
}, 5000);
return(
<div>
<p className='fa'>模块组件页面testtest父页面</p>
</div>
)
}
export default Main;
可能遇到的问题
静态方法调不到/报错
要注意静态方法写的位置, 比如像下面这个 demo 中, 静态方法书写的位置是在 SelfDevProgress
方法外面, export default SelfDevProgress;
之前的.
如果写在 SelfDevProgress
方法内部, 就会报错.
import React,{useState,useEffect,memo} from 'react';
import ReactDOM from 'react-dom';
import {Progress,Spin} from 'antd';
import './index.css'
const SelfDevProgress = (props)=> {
const [percent,setPersent] = useState(props.per ? props.per : 0)
const [perStatus,setPerStatus] = useState("active")
return(
<div className='outer'>
<div className='inner'>
<p>系统正在全力加载中, 请稍后...</p>
<Spin />
<Progress percent={percent} status={perStatus}/>
</div>
</div>
)
}
// 静态调用方法书写的位置
SelfDevProgress.success = (message) => {
console.log(message,"success")
}
export default SelfDevProgress;
组件渲染两次
写到一半打印的时候发现, 组件渲染了两次, 但是我只调用了一次, 截图如下:
于是上网搜索, 发现是react严格模式的问题, 把它注释掉就好了.
原贴: 【React】- 组件生命周期连续渲染两次问题