React:像message.success()一样实现Message通用容器及Message组件

我在开发不紧急的时候喜欢自己实现一些轮子;而这次要做的就是做一个非常常用的组件Message

对于Message这样的组件,在各个页面都有可能使用到。但我们是不希望在各个页面都必须引入一个容器的,也不希望还必须做专门的组件挂载,这样的组件能用,但不是很能用。

我希望造出一个如下一般只需要在js中进行简单的调用函数即可的开箱即用组件,当然最终也要允许用户自定义配置。

message.success("成功")
message.error("fail")

最终实现效果

1.gif

组件设计

  • 无需在调用的时候手动挂载组件
  • 一个通用的容器
  • 支持通过options配置组件消息内容、关闭延迟等属性
  • 完善友好的动画显隐
  • more…

组件实现

自动挂载组件

说到自动挂载,我的思路是在导入Message的时候自动运行挂载组件的代码,那我的想法是立即执行函数

(function initModalContainer() {
  let ele = document.getElementById("source-modal-contain");
  if (!ele) {
    //如果不存在容器,则进行创建
    let sourceModalContainer = document.createElement("div");
    sourceModalContainer.id = "source-modal-contain";
    document.body.append(sourceModalContainer);
    ele = document.getElementById("source-modal-contain");
    //通过ReactDOM将容器挂载在真实dom上
    ReactDOM.render(<ModalContainer />, ele);
  }
})();

ps:我在设计Message的时候,确定挂载这块其实有相当的通用性,最外层容器中完全可以塞入Modal组件这样的弹窗。所以这里你会看到容器名是ModalContainer

Container外层容器及ContainItem容器子项UI实现

不要熬夜不要熬夜hhh,我在写容器组件的时候,是凌晨2点,第二天看到很难想象我是用activeIDList这种方式来控制当前仍活动消息的。

实现思路:

  • 写一个nodeList用来装消息,这样我们卸载消息只需要在list中删掉对应消息就行。
  • 在node节点外层包一层组件,我们控制这层组件实现动画

这里其实我遇见了一个问题:

  • 由于hook函数中的useState是异步操作,而且不像setState一样提供了回调,那么当我们进行多次类似message.success()的调用时,会因为异步导致意料之外的作用,所以我最终是通过class组件实现。我的原本思路是调用子组件中的控制方法来进行隐藏操作,大晚上写着写着特么的就写歪了
    ModalContainer
class ModalContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      nodeList: [],
      activeIDList: [],
    };
  }
  render() {
    return (
      <div className={`${styles["modal-container"]}`}>
        {this.state.nodeList.map((item, index) => (
          <ModalItem
            key={item.id}
            show={this.state.activeIDList.indexOf(item.id) !== -1}
            config={item.config}
          >
            {item.node}
          </ModalItem>
        ))}
      </div>
    );
  }
}

对应的css

.modal-container{
    width: 100vw;
    position: fixed;
    z-index: 5000;
    left: 0;
    top: 0;
    display: flex;
    flex-direction: column;
}

上边这个是ModalContainer的基本框架,为每一个ModalItem传入一个配置,在外层通过this.state.activeIDList控制show(这又是一个坑,所以我们千万不要熬夜写代码)

因为我们修改show等于修改ModalItemprops.show,直接用来控制ModalItem的话会不可避免的引起组件的重新渲染,所以我这里的这样实现ModalItem 的:

ModalItem

function ModalItem(props) {
  const [show, setShow] = useState(props.show);
  useEffect(() => {
    if (props.show === false) setShow(false)
  }, [props.show]);
  return (
    <div
      className={`flexCenter ${styles["modal-item"]} ${
        show ? "" : styles["modal-item-hidden"]
      } `}
      style={{ "--duration--": props.config.duration + "ms" }}
    >
      {props.children}
    </div>
  );
}

对应的css

.modal-item{
    margin: .375rem;
    transform: translateY(-0.12rem);
    opacity: .2;
    animation: modalItemShow var(--duration--) ease forwards;
}
@keyframes modalItemShow {
    to{transform: translateY(0);opacity: 1;}
}
.modal-item-hidden{
    animation: modalItemHidden var(--duration--) ease forwards;
}
@keyframes modalItemHidden {
    to{transform: translateY(-2.5rem);opacity: 0;}
}

对组件控制进行实现

很明显,我们要对nodeListactiveList进行操控,不过暴露在外部的不应该是直接修改nodeList和activeList的能力,我的想法是实现addChildremoveChild;

首先在最外层准备一个对象

const modalControl = {
  addChild: null,
  removeChild: null,
};

接着在ModalContainer的constructor函数中实现这两个函数;

addChild()

addChild函数要做的是在nodeList中添加一个节点,节点如下

{
   node: item, //这是一条消息(也可以说是一个弹窗),ReactComponent
   config,     //这是这条消息的配置信息
   id          //通过时间戳生成的唯一ID
}

最终要在拆入完全完成,在setState之后将本条消息在nodeList中的index传出(这里有没有必要改成唯一id,值得思考)

const addChild = async (item, config) => {
      let nodeNew = [...this.state.nodeList];
      let id = new Date().getTime();
      nodeNew.push({
        node: item,
        config,
        id
      });
      let newActiveIDList = [...this.state.activeIDList, id];
      //给activeIDList添加这个id
      return new Promise((resolve) => {
        this.setState(
          {
            activeIDList: newActiveIDList,
            nodeList: nodeNew,
          },
          () => {
            resolve(nodeNew.length - 1);
          }
        );
      });
    };

removeChild()

removeChild()要根据addChild()传出的key先在活动消息列表中删除该消息完成动画,在动画结束后对应地在nodeList中删除这个node

const removeChild = async (key) => {
      let {config,id:nodeID} = this.state.nodeList[key];
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          let newActiveIDList = this.state.activeIDList.filter(item => item !== nodeID);
          this.setState(
            {
              activeIDList: newActiveIDList,
            },
            () => {
              let newNodeList = this.state.nodeList.filter(item => item.id !== nodeID);
              setTimeout(() => {
                this.setState(
                  {
                    nodeList: newNodeList,
                  },
                  () => {
                    resolve();
                  }
                );
              }, config.duration);
            }
          );
        }, config.delay);
      });
    };

最后一步,把这两函数挂到modalControl对象上,最后整个容器的代码如下

import React, { useEffect , useState } from "react";
import ReactDOM from "react-dom";
import styles from "./modal.module.css";
const modalControl = {
  addChild: null,
  removeChild: null,
};
function ModalItem(props) {
  ...
}
class ModalContainer extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      nodeList: [],
      activeIDList: [],
    };
    const addChild = async (item, config) => {
      ...
    };
    const removeChild = async (key) => {
      ...
    };
    modalControl.addChild = addChild;
    modalControl.removeChild = removeChild;
  }
  render() {
    return (
      <div className={`${styles["modal-container"]}`}>
        {this.state.nodeList.map((item, index) => (
          <ModalItem
            key={item.id}
            show={this.state.activeIDList.indexOf(item.id) !== -1}
            config={item.config}
          >
            {item.node}
          </ModalItem>
        ))}
      </div>
    );
  }
}
(function initModalContainer() {
  ...
})();
export { modalControl };

到这里,其实我们已经实现了通用容器,在这个基础上我们可以轻易的做Message组件出来

完成最后的Message

首先写个Message模板,我这里只实现了success的模板

import successSvg from '../images/success.svg';
const svgmap={
   "success":successSvg
}
function MessageTemplate(props){
    return (
        <div className={`${styles[props.type+'-template']} ${styles['template']}`} onClick={()=>{console.log('test')}}>
            <img src={svgmap[props.type]} width="30" height="30" style={{margin:'6px'}}></img>
            <span>{props.content}</span>
        </div>
    )
}

接着实现messageSuccess默认函数,调用这个函数会调用默认模板

const defaultConfig={
    delay:1500,
    duration:360
}
async function messageSuccess(content){
    let key=await modalControl.addChild(
        <MessageTemplate type="success" content={content}/>,
        defaultConfig
    )
    await modalControl.removeChild(key)
}

再实现一个允许自定义配置的messageSuccessConfig函数,调用该函数会得到一个调用自定义config的messageSuccess

function messageSuccessConfig(e){
    let options={
        ...defaultConfig,
        ...e
    };
    return async function(content){
        let key=await modalControl.addChild(
            <MessageTemplate type="success" content={content||options.content}/>,
            options
        )
        await modalControl.removeChild(key)
    }
}

组件使用

我们上边构建了messageSuccessmessageSuccessConfig,使用Message组件也和我们一开始想的一样简单

  messageSuccess("成功!")
  let test=messageSuccessConfig({
    delay:2400
  })
  test("success!")

最终都能实现

2.gif

总结

因为抽象出了container,想实现Modal、Dialog都会很容易;我们当然也可以自己写一个提示框,通过addChild添加到消息队列中。

这个组件还有很多不足的地方,从点击回调,到主动关闭手动关闭,还有很多地方可以优化

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您可以优化代码,使其更具可读性和可维护性。以下是代码的改进建议: 1. 将请求逻辑放在单独的函数中,以提高代码的可读性和重用性。 2. 使用解构赋值来简化代码,并避免不必要的复制操作。 3. 使用可选链操作符(Optional Chaining)来避免在深层嵌套对象中访问不存在的属性时报错。 4. 将请求参数和返回数据的结构拆分为单独的变量,以提高代码的可读性。 5. 将返回的数据对象直接作为请求结果返回,而不是将它们封装在一个新的对象中。 下面是改进后的代码示例: ```javascript const fetchData = async (params) => { const body = { dataQueryTime: selectedDate }; const res = await queryBatteryInformationManagement(body); const { statisticResultVOList, ...total } = res?.data || {}; setSummaryData(total); console.log(total); return { data: statisticResultVOList, success: res?.success, errorCode: res?.errorCode, errorMessage: res?.errorMessage, showType: res?.showType }; }; // 在组件中使用请求函数 // ... <ProTable request={fetchData} /> ``` 通过将请求逻辑提取到单独的函数中,可以提高代码的可读性和重用性。使用解构赋值简化了代码,并避免了不必要的复制操作。通过使用可选链操作符,可以避免在深层嵌套对象中访问不存在的属性时报错。将请求参数和返回数据的结构拆分为单独的变量,提高了代码的可读性。最后,直接返回请求结果对象,而不是封装在一个新的对象中。这些改进将使代码更加清晰和易于理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值