前端伪进度条实现(仿antd message使用react组件静态方法)

文章目录

  • 背景
  • 实现方法
    • (一) 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%, 然后等待 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);
            
        }
    }

效果是这样的:

进度条组件消失视频

(六) 背景遮罩

遮罩思路:

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

  2. #self-dev-outer 中继承父组件的宽高, 利用背景色透明度来加遮罩;

前端伪进度条实现(仿antd message使用react组件静态方法)_第1张图片

最终实现及代码

整个工程压缩包已上传资源.

demo中涉及到的几个文件的结构:

前端伪进度条实现(仿antd message使用react组件静态方法)_第2张图片

组件代码

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;

组件渲染两次

写到一半打印的时候发现, 组件渲染了两次, 但是我只调用了一次, 截图如下:

前端伪进度条实现(仿antd message使用react组件静态方法)_第3张图片

于是上网搜索, 发现是react严格模式的问题, 把它注释掉就好了.
原贴: 【React】- 组件生命周期连续渲染两次问题

前端伪进度条实现(仿antd message使用react组件静态方法)_第4张图片
前端伪进度条实现(仿antd message使用react组件静态方法)_第5张图片

你可能感兴趣的:(前端,前端,react.js,javascript)