我在开发不紧急的时候喜欢自己实现一些轮子;而这次要做的就是做一个非常常用的组件Message
对于Message这样的组件,在各个页面都有可能使用到。但我们是不希望在各个页面都必须引入一个容器的,也不希望还必须做专门的组件挂载,这样的组件能用,但不是很能用。
我希望造出一个如下一般只需要在js中进行简单的调用函数即可的开箱即用组件,当然最终也要允许用户自定义配置。
message.success("成功")
message.error("fail")
最终实现效果
组件设计
- 无需在调用的时候手动挂载组件
- 一个通用的容器
- 支持通过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) => (<ModalItemkey={
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
等于修改ModalItem
的props.show
,直接用来控制ModalItem的话会不可避免的引起组件的重新渲染,所以我这里的这样实现ModalItem
的:
ModalItem
function ModalItem(props) {
const [show, setShow] = useState(props.show);useEffect(() => {
if (props.show === false) setShow(false)}, [props.show]);return (<divclassName={
`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;}
}
对组件控制进行实现
很明显,我们要对nodeList
和activeList
进行操控,不过暴露在外部的不应该是直接修改nodeList和activeList的能力,我的想法是实现addChild
和removeChild
;
首先在最外层准备一个对象
const modalControl = {
addChild: null,removeChild: null,
};
接着在ModalContainer的constructor
函数中实现这两个函数;
addChild()
addChild函数要做的是在nodeList中添加一个节点,节点如下
{
node: item, //这是一条消息(也可以说是一个弹窗),ReactComponentconfig, //这是这条消息的配置信息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添加这个idreturn 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) => (<ModalItemkey={
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)}
}
组件使用
我们上边构建了messageSuccess
和messageSuccessConfig
,使用Message组件也和我们一开始想的一样简单
messageSuccess("成功!")let test=messageSuccessConfig({
delay:2400})test("success!")
最终都能实现
总结
因为抽象出了container,想实现Modal、Dialog都会很容易;我们当然也可以自己写一个提示框,通过addChild
添加到消息队列中。
这个组件还有很多不足的地方,从点击回调,到主动关闭手动关闭,还有很多地方可以优化