react classname多个_基于 React, 如何实现全局提示?

9e882e8fbaac366fd800472ce0806a5f.png

在 Web 项目当中,一个全局的提示组件可能是一个普遍的需求。

当用户做了一些操作,提示组件可以给用户相应的提醒。比如在页面上,用户做了增删操作,需要提示增加/删除成功。

比如像下面这样:

c78a58b49ca8b57785d8d520c7c0ba48.png

需求分析

若需要用 React 实现一个全局提示组件,这里我们称它为 message,参考 Ant Design , 全局提示组件向外暴露的不是一个组件,而是一个 API, 调用如下:

import message from './Message'

message.info('提示信息')

这样一来,只需要在需要提示的地方引入 message, 直接调用它的方法就可以弹出提示信息。

如上面的代码所示,全局提示组件并没有输出一个组件,这意味着需要在 message.info 方法调用之前,把相关的组件渲染到页面上,为展示的信息提供一个容器。在调用 message.info 方法之后,再把提示的内容渲染到页面上。

代码编写

如上面的分析,message 需要的组件是一个容器组件 MessageContainer, 以及展示提示信息的组件 Message, 每当调用了提示的方法,就往容器组件里面新增一个 Message 组件,来展示提示内容。

所以,代码目录结构如下:

├─Message
│      index.tsx
│      message.tsx
│      _style.scss

容器组件

挂载

容器组件需要预先渲染到页面上,这个渲染的动作就在 message 的入口文件 index.ts.

message 被某个组件引入,这个文件就会执行,将容器组件 MessageContainer 渲染到页面上;并且,就算有多个组件引入了 message, 入口文件的代码也只会执行一次,不会造成冲突。

渲染的代码如下:

let el = document.querySelector('#message-wrapper')
if (!el) {
  el = document.createElement('div')
  el.className = 'message-wrapper'
  el.id = 'message-wrapper'
  document.body.append(el)
}

ReactDOM.render(
  <MessageContainer />,
  el
)

在上面的代码中,我们把 MessageContainer 挂载到页面上。

提示信息队列

容器组件负责加载包含提示信息的 Message 组件。

一般来说,提示信息会有一个时长,比如弹出 3 秒后自动关闭,并且当 3 秒内再次触发提示,页面上会有两条提示信息,如下所示:

e29da6484e8c48c2a718a57b4a1920fc.png

而页面内不可能同时放得下一万条提示信息,所以需要对提示信息需要有一个数目上限,我们在这里暂时把它定为 10 条。

总结一下,就是调用 message.info 方法,MessageContainer 内渲染一个携带提示信息的 Message 组件,并且在 3 秒后把该 Message 组件移除;假如 3 秒内又有一个提示信息,再添加一个 Message 组件,并且 3 秒后移除。当页面内的提示信息大于十条,就删除第一条提示信息,移除第一个 Message 组件。

添加信息

关于 MessageContainer ,添加信息的代码如下:

import Message from './message'

let add: (notice: Notice) => void

export const MessageContainer = () => {
  const [notices, setNotices] = useState<Notice[]>([])

  add = (notice: Notice) => {
    setNotices((prevNotices) => [...prevNotices, notice])
  }

  return (
    <div className="message-container">
      {
        notices.map(({ text, key, type }) => (
            <Message key={key} type={type} text={text} />
        ))
      }
    </div>
  )
}

上面的代码就是 MessageContainer 组件的一部分,这部分负责添加信息,这些代码同样放在 message 的入口文件 index.ts.

注意到,上面的 add 函数,并不是在 MessageContainer 内部声明的,因为这个函数需要被外部调用,来改变 MessageContainer 的内部状态。

const [notices, setNotices] = useState<Notice[]>([]) 初始化了 notices, 这里的 notices 就是提示信息的队列。

addMessageContainer 内部完成赋值,接受一个 notice 作为参数,并把这个 notice 添加到 notices 队列当中。这里 setNotices的参数是一个匿名函数,而不是一个值,因为需要拿到先前的 notices 队列来更新 notices, 假如用下面这样的写法,并不一定能正确更新 notices:

add = (notice: Notice) => {
    setNotices([...notices, notice])
}

因为当频繁触发 add 的时候,很有可能会跳过其中的几次更新,其中缘由,可参考 useState 函数式更新。

MessageContainer 返回的即是它的渲染内容,根据提示信息队列来渲染 Message 组件。Message 组件稍后会分析。关于 Notice,由以上MessageContainer 返回内容的代码可见,Notice 实例有 text, type, key 三个属性,其结构如下:

export interface Notice {
  text: string; // 提示消息文本
  key: string; // 该条信息的 uuid
  type: MessageType; // 提示信息的类型
}

删除信息

上面说到,当一条信息出现超过 3 秒,或者信息队列的长度超过 10, 都会删除信息。添加了删除逻辑的 MessageContainer 代码如下:

import Message from './message'

let add: (notice: Notice) => void

export const MessageContainer = () => {
  const [notices, setNotices] = useState<Notice[]>([])
  const timeout = 3 * 1000
  const maxCount = 10

  const remove = (notice: Notice) => {
    const { key } = notice

    setNotices((prevNotices) => (
      prevNotices.filter(({ key: itemKey }) => key !== itemKey)
    ))
  }

  add = (notice: Notice) => {
    setNotices((prevNotices) => [...prevNotices, notice])

    setTimeout(() => {
      remove(notice)
    }, timeout)
  }

  useEffect(() => {
    if (notices.length > maxCount) {
      const [firstNotice] = notices
      remove(firstNotice)
    }
  }, [notices])

  return (
    <div className="message-container">
      {
        notices.map(({ text, key, type }) => (
            <Message key={key} type={type} text={text} />
        ))
      }
    </div>
  )
}

上面的代码中,变量 timeout 定义了单条信息的时长,maxCount 则是信息数量的上限。

remove 方法中,先取得 noticekey, 这个 key 是单条 notice 的唯一值,可以根据这个值删除信息队列 notices 中的某一条 notice. 删除 notice 与添加 notice 类似,用了函数式更新 state . 这里的删除方法是数组的 filter 方法。

add 方法中,可以看到,多出了一个定时器,在 timeout 时间之后,将删除该条信息的代码放入执行队列。

当信息超过 10 条,将删除第一条信息,这里利用 useEffect 实现。useEffect 的依赖项就是提示信息队列 notices , 当 notices 发生变化,就会执行 uesEffect 中的回调函数。当 notices 的长度大于 10,将会调用 remove 方法移除第一条提示信息。提取第一条信息的代码是const [firstNotice] = notices , 这里利用了数组的解构赋值。

Message 组件

Message 组件只是一个纯展示的组件,负责展示提示信息文本。除了文本,提示信息一般还会有各种类型,这里用图标来表示,如下所示:

8abba922312b4a3e170bb85fe2834edd.png

不同类型的提示,对应不同的图标,在视觉上给出更加直观的表述。

可见 Message 内部有一个代表提示类型的图标,还有提示信息的文本内容,所以 Message 组件有接受两个属性,分别是 typetext .

涉及到图标渲染,这里利用的图标库是 react-fontawesome, 并且在编写 message 之前,已经简单封装了一个 Icon 组件,当然,这两点不是特别重要,只是一个前情提要,方便以下代码的阅读。

Message 的代码如下:

import React, { FC, ReactElement } from 'react'
import { IconProp } from '@fortawesome/fontawesome-svg-core'
import Icon from '../Icon/icon'

export type MessageType = 'info' | 'success' | 'danger' | 'warning'

export interface MessageProps {
  text: string;
  type: MessageType
}

const Message: FC<MessageProps> = (props: MessageProps) => {
  const { text, type } = props

  const renderIcon = (messageType: MessageType): ReactElement => {
    let messageIcon: IconProp

    switch (messageType) {
      case 'success':
        messageIcon = 'check-circle'
        break
      case 'danger':
        messageIcon = 'times-circle'
        break
      case 'warning':
        messageIcon = 'exclamation-circle'
        break
      case 'info':
      default:
        messageIcon = 'info-circle'
        break
    }

    return <Icon icon={messageIcon} theme={messageType} />
  }

  return (
    <div className="message">
      <div className="message-content">
        <div className="icon">
          {renderIcon(type)}
        </div>
        <div className="text">
          {text}
        </div>
      </div>
    </div>
  )
}

上面的代码,一开始定义了提示信息的类型 MessageType, 以及 Message 组件的 props 类型 MessageProps .

Message 内部的 renderIcon 方法,即是根据提示类型来渲染不同类型,不同颜色的图标。

message API

当容器组件 MessageContainerMessage 组件都准备好,就需要暴露一个 API 给外部调用,来渲染提示信息。

已知 MessageContainer 已经预先渲染到页面中,一开始,它的内部信息队列 notices 是空的。并且, MessageContainer 中添加信息的方法 add 所在作用域并不是在 MessageContainer 内部,我们可以在外部调用这个方法来给 MessageContainer 添加信息。

index.ts 内的代码大致如下:

export interface MessageApi {
  info: (text: string) => void;
  success: (text: string) => void;
  warning: (text: string) => void;
  error: (text: string) => void;
}

export interface Notice {
  text: string; // 提示消息文本
  key: string; // 该条信息 uuid
  type: MessageType; // 提示信息的类型
}

let seed = 0
const now = Date.now()
const getUuid = (): string => {
  const id = seed
  seed += 1
  return `MESSAGE_${now}_${id}`
}

let add: (notice: Notice) => void
export const MessageContainer = () => {
  // 省略
}

const api: MessageApi = {
  info: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'info'
    })
  },
  success: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'success'
    })
  },
  warning: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'warning'
    })
  },
  error: (text) => {
    add({
      text,
      key: getUuid(),
      type: 'danger'
    })
  }
}

export default api

MessageApi 接口规定了 message API 的形状,info, success, warning, error 四个字段代表四个类型不同的方法,调用方式如 message.success('成功信息'), message.info('提示信息') 等。

Notice 接口规定了单条提示信息 notice 的字段。

getUuid 则是获取单条提示信息的 uuid 的方法,在 MessageContainer 中,需要依据这个值,来删除某条提示信息。

接下来就是 add 方法的声明,以及 MessageContainer 组件,add 方法声明在外部,赋值在 MessageContainer 内部,即可实现在 MessageContainer 外部改变其状态。

最后是变量 api, 实现了 MessageApi 接口的各个方法,在 api 实现的方法中,调用 add 来添加信息,改变 MessageContainer 的状态,使得提示信息渲染到页面上。

动画

到此,message 就实现了基本的功能。在提示信息中,如果有动画的过渡,那么信息就不会突然弹出或突然关闭,显得很突兀,而且,添加了动画,也更加美观。

添加了动画之后,调用 message.info('默认提示'), 效果如下:

8f2443f20088e22f7a5d46401c9f8ec2.gif

可见提示信息在出现的时候,有个从上到下的过渡,以及透明度的变化;消失的时候则反之。

这里使用的动画库是 React Transition Group,这个库可以在组件加载卸载过程中,为组件添加相应的 className, 这样一来,就可以对应的 className 编写样式,实现动画的过渡效果。

动画的样式如下:

.slide-in-top-enter {
  opacity: 0;
  transform: translateY(-100%);
}

.slide-in-top-enter-active {
  opacity: 1;
  transform: translate(0);
  transition: transform 200ms ease-out, opacity 200ms ease-in-out;
}

.slide-in-top-exit {
  opacity: 1;
}

.slide-in-top-exit-active {
  opacity: 0;
  transform: translateY(-100%);
  transition: transform 300ms linear 100ms, opacity 300ms ease-in-out;
}

这里结合 React Transition Group 的 CSSTransition 实现了一个“从上往下出现,从下往上消失”的动画。

以上代码中,类名里的 enter, enter-active 后缀, 分别代表组件“开始出现”,”出现过程中“的状态;exitexit-active 后缀分别对应“开始消失”,“消失过程中”的状态。这些后缀都是 CSSTransition 所赋予的。

总结

这就是一个 React 全局提示的简单实现,关键之处就是 MessageContaineradd 方法,它暴露在外部,让外部方法可以修改内部状态。

文中的代码只是大致呈现,完整的代码可参考这里,查看 message 的演示可点击这里 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值