阅读 286

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添加到消息队列中。
这个组件还有很多不足的地方,从点击回调,到主动关闭手动关闭,还有很多地方可以优化


作者:源心锁
链接:https://juejin.cn/post/7018908720784474120


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐