文章目录

  • 背景
  • 实现方法
    • (一) react组件静态方法
    • (二) 通过静态方法改变组件的状态
    • (三) 指定进度条的步幅规则
    • (四) 成功和失败的状态改变
      • 1. 成功
      • 2. 失败
    • (五) 组件消失
    • (六) 背景遮罩
  • 最终实现及代码
    • 组件代码
    • 模拟调用进度条组件的代码
  • 可能遇到的问题
    • 静态方法调不到/报错
    • 组件渲染两次

背景

react项目中, 有些页面加载很慢, 为了提升用户体验, 需要在前端做一个伪进度条, 并且把这个伪进度条封装成一个组件, 提供给多个项目使用.

这个伪进度条有以下特点:

  1. 基于antd组件进行二次开发. 利用 antd 的 Spin, Progress 组件开发, 减少工作量.
  2. 在进度条走到 99% 的时候会卡住, 等拿到响应的时候完成.
  3. 考虑到要把这个伪进度条封装成组件, 所以最好是用类似 message.info() 这种方法调用, 不然不好对响应之后的组件状态进行改变. 因此要用到react组件的静态方法.
  4. 我用的函数组件 (当然用类组件也是可以的).

实现方法

(一) 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>{/*  */}</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 元素外层需使用一个标签进行包裹,如
  • 第二个参数是插入该模板的目标位置。
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(// // // // )}// 静态调用方式SelfDevProgress.success = (message) => {console.log(message,"success")SelfDevProgressAPI("success")}

(三) 指定进度条的步幅规则

这个可以自由指定, 但是要注意几点:

1. 最好不要出现小数的步幅, 小数出现在进度条上有点不太好看;
2. 在最后的时候要有卡顿, 比如说从90%的时候开始走的变慢, 或者卡在99%的位置;

以下是我指定的一些规则:

进度条时间(duration)规则定时器时间间隔步幅(s)
//100ms默认为2
duration > 10 秒10s走到99%, 然后等待100ms1
10 >= duration > 55s走到98%, 这时候将步幅改为 1, 走到99%等待100ms2
5 >= duration > 22s走到95%, 这时候将步幅改为 1, 走到99%等待100ms5
duration <=21s走到90%, 这时候将步幅改为 1, 走到99%等待100ms10
 /* 进度条展示规则 如果没有设置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);}}

效果是这样的:

进度条组件消失视频

(六) 背景遮罩

遮罩思路:

  1. #selfDevProgress-container 中利用 position 属性定义好位置, 以及设置好宽高 (因为这个进度条组件是相对于整个页面的, 所以这个div应该覆盖整个可视页面);

  2. #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 = 1break;case duration > 5 && duration <= 10:s = 2break;case duration > 2 && duration <= 5:s = 5break;case duration <= 2:s = 10break;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 = 0setGetChangePer(per)let t = setInterval(() => {per += sif(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 = 0switch (type) {case "start":per = 0break;case "success":per = 100break;case "fail":per = 50break;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】- 组件生命周期连续渲染两次问题