这篇文章将会带你从 0 到 1 用 React 完成一个 Message 消息提示组件,知识点包括
React.createRoot()
这个 API 的使用,以及如何使用原生 JS 来生成uuid
;实际项目开发中,改变一个元素的样式经常是通过动态切换 class 类名,所以,我还会带你封装一个自定义 HooksuseCssClassManager
来优雅的实现组件样式的切换,其余还有很多技术细节,请耐心往下看吧😎
明确需求
Message 消息提示,常见于弹出一个消息提示告知用户,比如,用户登录的时候,如果遇到网络异常,最好的交互方式就是弹出一个 Message 提示,告诉用户当前无法登陆的原因。这个组件对于前端开发者来说应该都不陌生,相信大部分人都用过 Element UI,其中的 Message 组件就很经典
Message 可以分为不同的类型,用来表达不同的状态,比如“操作成功”就是一个和谐轻松的绿框框,而“操作失败”就弹一个醒目刺眼的红框框。
Message 通常不用固定的写在你的模版或 JSX 中,一般都是提供一个函数,例如Message.success('register successful')
,随用随调,这就意味着使用 Message 需要手动挂载 React 组件到 DOM 中。
我们的目标是实现一个 Message 组件,用户每调用一次就能够在屏幕上弹出一个文字提示框,3s 之后自动消失,为了美观,可以给 Message 加上动画效果;同时为了不留下冗余的 DOM 节点,需要在 Message 小时之后自动移除其 DOM 节点。
我们最终要完成的 API 调用形式是 Message.[type]('xxxx')
,这里直接仿照了 Element UI 的 API 形式;提供四种不同样式的 Message ,分别是:info 普通消息
、success 成功消息
、warn 警示消息
、error 错误消息
,通过 type
属性来定义,下面是 Message
对象的结构:
interface propsMessage {
info: Function;
warn: Function;
error: Function;
success: Function;
}
Message 对象提供的这 4 个函数分别渲染出不同类型的消息,所以,我们接下来定义 Function
的入参和返回值:
// 即上面代码中的 Function 类型
// message 是用户自定义的消息内容
type messageInput = (message: string) => void;
interface propsMessage {
info: messageInput;
warn: messageInput;
error: messageInput;
success: messageInput;
}
现在,我们有了一个 API Message.success('hello world')
,调用后屏幕上就会弹出消息提示,接下来的重点是对 Message 的 4 个子方法的逻辑实现。
说到在 React 中手动生成 DOM 节点,首先想到的就是 ReactDOM.render()
这个 API ,首先来回顾一下官方文档中的定义:
其函数签名为render(element, container[, callback])
,我们只需要用到前两个参数就够了。element
可以是任何 DOM 节点,也可以是 React 节点,而 container
是一个父容器节点,本文中的父容器就是document.body
,ReactDOM.render()
可以将 element
作为自节点渲染到 container
中。
下面来说个重要的事情,假如你经过一顿操作,使用 ReactDOM.render()
实现了最终效果,那你的 Message 在未来的 React 新版本中将不再可用 ☹️,为啥呢?因为控制台有一个报错
上面说“ReactDOM.render is no longer supported in React 18. Use createRoot instead”,意思就是 React 18 不再支持这个 API 了,在未来版本中将会被废弃,请使用createRoot
来替代。我就是傻夫夫的用 React.render
实现了核心逻辑,然后发现这个报错,哭笑不得 🥲,没办法,未来将废弃的 API 肯定不能再用啦。
本文就不叙述之前用 render
实现的细节啦,直接使用新的 React.createRoot()
。现在,我们来梳理一下具体的步骤:
- 根据传入的
message
以及所调用方法的type
,准备好一个Message
组件 - 手动渲染这个组件到
body
节点下 - 3s 后自动清除这个 DOM 节点
实现 Message 组件
首先是准备一个 Message 组件,这是渲染到页面上的 DOM ,内容和结构其实很简单;因为Message
这个变量名已经被占用,作为最终的导出对象的名称,所以这里我们给真正的 Message 组件命名为BaseMessage
// 根据 type 和 message 返回不同样式类型的 UI
function BaseMessage(props) {
const { type, message } = props;
return (
<p
ref={refMessage}
className="wdu-message"
>
{message}
</p>
);
}
样式切换
前面说到,有四种不同类型的 Message 需要实现,所以我们可以定义 4 种不同的 css 类,然后动态的应用到组件上
// less 代码
.wdu-raw-plain {
color: #7e7e7e;
background-color: #efefef;
border: 2px solid #bababa;
}
.wdu-raw-danger {
color: #db1a1a;
background-color: #ffbdbd;
border: 2px solid #eb5757;
}
.wdu-raw-success {
color: #0d8a0d;
background-color: #c9f3c9;
border: 2px solid #50b250;
}
.wdu-raw-warn {
color: #9f9f00;
background-color: #ffe89c;
border: 2px solid #dddd00;
}
.wdu-message {
&__info {
.wdu-raw-plain();
}
&__error {
color: white;
.wdu-raw-danger();
}
&__success {
color: white;
.wdu-raw-success();
}
&__warning {
.wdu-raw-warn();
}
}
function BaseMessage(props: any) {
const { type, message} = props;
return (
<p
ref={refMessage}
className={`wdu-message wdu-message__${type}`}
>
{message}
</p>
);
}
这样我们就能够根据传入的type
来实现不同样式的 Message 了
显示与隐藏
为了效果美观,我们来给 Message 显示与隐藏都加上 CSS 动画
.wdu-message {
&__visible {
opacity: 0;
animation: visible 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) 0s 1 forwards;
}
&__hidden {
opacity: 1;
animation: hidden 0.5s cubic-bezier(0.250, 0.460, 0.450, 0.940) 0s 1 forwards;
}
}
@keyframes visible {
from {
opacity: 0;
top: -30px;
}
to {
opacity: 1;
top: 0;
}
}
@keyframes hidden {
from {
opacity: 1;
top: 0;
}
to {
opacity: 0;
top: -30px;
}
}
然后,定义一个状态visible
,控制组件的显示与隐藏,使用 useEffect
监听visible
的变化,动态应用我们编写好的动画即可
function BaseMessage(props: any) {
const { type, message} = props;
const refMessage = useRef<any>();
const classMap = {
base: "",
visible: "wdu-message__visible",
hidden: "wdu-message__hidden",
};
const { addClassName, classList } = useCssClassManager(classMap);
useEffect(() => {
visible ? addClassName("visible") : addClassName("hidden");;
}, [visible]);
return (
<p
ref={refMessage}
className={`wdu-message wdu-message__${type} ${classList}`}
>
{message}
</p>
);
}
上面的代码突然变复杂了,不过不急,我们一段段来看
const classMap = {
base: "",
visible: "wdu-message__visible",
hidden: "wdu-message__hidden",
};
const { addClassName, classList } = useCssClassManager(classMap);
classMap
是一个类名集合,其中包含组件样式的三种状态,即“初始”、“显示”、“隐藏”,通过类名来表示
为了避免在标签上加太多表达式以及写出面条式的代码,这里封装了一个 hooks useCssClassManager
,它可以动态地管理我们所提供的 classMap
interface cssClass {
base: string; // 初始状态的样式类名
[key: string]: string;
}
function useCssClassManager(cssClassMap: cssClass) {
const [classMap, setClassMap] = useState<cssClass>({
base: cssClassMap.base,
});
// 动态类名列表
const [classList, setClassList] = useState("");
// 移除指定类名
const removeClassName = (classKey: string) => {
setClassMap((prev) => {
const template = { ...prev };
delete template[classKey];
return template;
});
};
// 添加指定类名
const addClassName = (classKey: string) => {
setClassMap((prev) => ({ ...prev, [classKey]: cssClassMap[classKey] }));
};
useEffect(() => {
setClassList(Object.values(classMap).join(" "));
}, [classMap]);
return {
removeClassName,
addClassName,
classList,
};
}
到这里,我们就实现了一个基本的 Message 组件了,但是还有很多工作要做。
手动渲染组件
第二步,手动渲染组件到 body 元素下。这里有一个问题:如何渲染多个 Message ?
我们的需求是:每调用一次 Message 方法就要渲染出一个消息框;大家应该都知道,在 React 中UI = component(state)
,我首先想到的就是用队列来存储每条 Message 要用到的数据,也就是state
;假如我们给队列新增 5 个数据项,那么页面上就会渲染 5 个 Message,然后我们删掉队列中的某个数据项,那么页面上相应地也要移除那一个 Message——核心思想就是通过数据的增删来实现响应式的更新 UI
为了实现更清晰的逻辑,可以将数据和 UI 分开来处理。
管理数据项
我们来实现addMessage
方法,用于向队列中添加每条消息的数据项:
interface messageOption {
type: messageType;
message: string;
}
interface messageQueueItem extends messageOption {
id: string;
}
const MESSAGE_QUEUE: Array<messageQueueItem> = [];
function addMessage(params: messageOption) {
const id = uuid();
MESSAGE_QUEUE.push({ ...params,id });
}
MESSAGE_QUEUE
队列中的每条数据都由type、message
组成,为了区分不同的消息,通过uuid
这个方法来生成一个独一无二的id
然后赋予每条数据。
github 有一个非常好用的 id 生成工具——nanoid,它非常的短小精悍,推荐在实际开发中使用
但此处我不想引入外部依赖🧐,所以我们来手动实现一个uuid
函数,我首先想到的方法是:用正则表达式加上随机数来生成,但再细一想,这个实现起来巨麻烦,你要考虑会不会重复的情况,以及怎样编写合理的正则表达式;所以,还是去 MDN 查一波文档,看看有没有什么 API 能方便的实现这个功能,让我可以偷偷懒 😛。
还真让我找到了
简单来说,Web Crypto API
用于生成随机密码,常用加密货币领域,看到“随机”和“密码”两个字你想到了什么?没错,这不就是我们需要的功能嘛!
window.crypto.getRandomValues
可以获取符合密码学要求的安全的随机值,传入参数的数组被随机值填充,语法是cryptoObj.getRandomValues(typedArray)
。typedArray
就是Javascript 中的类型数组,如果你玩儿过 Web 3D ,那你一定对类型数组滚瓜烂熟,它可以用来装载字节流直接传输给 GPU 进行图形计算。限于篇幅,这里不做详细介绍,你可以直接去 MDN 了解它的用法,我们只需要将new Unit8Array(length)
作为参数传递给getRandomValues
即可得到 crypto 生成的随机字符串,将它用作 Message 数据项的 id
。
下面是 uuid
方法的实现,关于要生成的随机数的长度,8位无符号整型(Uint8Array)已经足够
function uuid() {
const uuid = window.crypto.getRandomValues(new Uint8Array(8));
return uuid.toString().split(",").join("");
}
// uuid(8) "1914612525112710383190"
uuid(8)
将会返回一个最长 24 位的随机数值,这几乎满足我们的要求了。
接下来以同样的方式实现删除数据项的方法removeMessage
,非常简单
function removeMessage(id: string) {
// 根据 id 找到元素,然后移除这个元素
const position = MESSAGE_QUEUE.findIndex((item) => item.id === id);
MESSAGE_QUEUE.splice(position, 1);
renderMessage([...MESSAGE_QUEUE]);
}
创建容器
现在,我们已经完成数据项的准备工作,可以实现增删,接下来就是要把数据队列中的每一项传入BaseMessage
组件,然后渲染出多个BaseMessage
组件即可。
使用 React.createRoot()
将组件挂载到页面上,先来看下这个新增 API 的用法
createRoot
接受container
参数作为父容器,然后返回这个父容器的引用root
,接下来,使用root.render
来将element
渲染到父容器中,注意,它会完全替换掉父容器中的内容,也就是说,如果你将document.body
作为父容器,然后调用root.render(element)
,那么body
中的所有内容将被替换为element
这一个元素,这是不行的。
所以我们还需要在**body**
下新增一个子元素,作为挂载 Message 的父容器,而不能直接以body
作为父容器;实现方法createContainer
来创建容器:
const CONTAINER_ID = "wdu-message__container";
function createContainer() {
// 检查页面上是否已存在容器
let container = document.getElementById(CONTAINER_ID);
// 创建一个容器,添加到 body 上
if (!container) {
container = document.createElement("div");
container.setAttribute("id", CONTAINER_ID);
document.body.appendChild(container);
}
return container;
}
渲染组件
挂载 Message 的容器已经准备好,接下来实现方法renderMessage
用来渲染组件:
import { createRoot } from "react-dom/client";
function renderMessage(messageQueue: Array<any>) {
// 获取容器
const container = createContainer();
// 准备 Message 组件
const MessageComponents = messageQueue.map((props) => {
return <BaseMessage {...props} key={props.id} />;
});
// 挂载组件
const containerRoot = createRoot(container);
containerRoot.render(MessageComponents);
}
自动移除组件
最后一步很简单,我们只需要在 Message 组件挂载后,创建一个setTimeout
定时器,3s 后在数据队列中删掉当前消息的数据项,然后再次调用renderMessage
重新渲染视图即可。
这一部分的逻辑由组件自己管理,具体实现如下:
function BaseMessage(props: any) {
const { type, message, id } = props;
// 省略一些非重点逻辑
// ....
// 组件 DOM 节点
const refMessage = useRef<any>();
// 处理消息的隐藏
const clear = () => removeMessage(id);
const handleHidden = () => {
if (refMessage.current) {
// 在隐藏动画结束后,移除当前组件的数据项
refMessage.current.addEventListener("animationend", clear, {
once: true,
});
}
addClassName("hidden");
};
// 在组件挂载后,应用显示动画,3s 后执行组件隐藏的逻辑
useEffect(() => {
addClassName("visible");
setTimeout(() => {
handleHidden();
}, 3000);
}, []);
return (
<p
ref={refMessage}
className={`wdu-message wdu-message__${type} ${classList}`}
>
{message}
</p>
);
}
成果检验
到此,我们基本上已经完成了全部的代码实现,先给大家看一下最终效果
戳这里去👉演示页面
下面贴一下核心代码:
- 类型声明
type messageType = "info" | "warning" | "error" | "success";
type messageInput = (message: string) => void;
interface messageOption {
type: messageType;
message: string;
}
interface messageQueueItem extends messageOption {
id: string;
}
interface propsMessage {
info: messageInput;
warn: messageInput;
error: messageInput;
success: messageInput;
}
export type { propsMessage, messageOption, messageQueueItem };
- 自定义 Hooks
interface cssClass {
base: string; // 初始状态的样式类名
[key: string]: string;
}
function useCssClassManager(cssClassMap: cssClass) {
const [classMap, setClassMap] = useState<cssClass>({
base: cssClassMap.base,
});
// 动态类名列表
const [classList, setClassList] = useState("");
// 移除指定类名
const removeClassName = (classKey: string) => {
setClassMap((prev) => {
const template = { ...prev };
delete template[classKey];
return template;
});
};
// 添加指定类名
const addClassName = (classKey: string) => {
setClassMap((prev) => ({ ...prev, [classKey]: cssClassMap[classKey] }));
};
useEffect(() => {
setClassList(Object.values(classMap).join(" "));
}, [classMap]);
return {
removeClassName,
addClassName,
classList,
};
}
- 工具函数
function uuid() {
const uuid = window.crypto.getRandomValues(new Uint8Array(8));
return uuid.toString().split(",").join("");
}
- 组件实现
import { useEffect, useRef } from "react";
import { propsMessage, messageOption, messageQueueItem } from "./type";
import { createRoot } from "react-dom/client";
import { uuid } from "@util";
import { useCssClassManager } from "@base/hooks";
import "./message.less";
const CONTAINER_ID = "wdu-message__container";
const MESSAGE_QUEUE: Array<messageQueueItem> = [];
// 创建容器
function createContainer() {
let container = document.getElementById(CONTAINER_ID);
if (!container) {
container = document.createElement("div");
container.setAttribute("id", CONTAINER_ID);
document.body.appendChild(container);
}
return container;
}
// 新增消息
function addMessage(params: messageOption) {
const id = uuid(8);
MESSAGE_QUEUE.push({ ...params, id });
renderMessage([...MESSAGE_QUEUE]);
}
// 移除消息
function removeMessage(id: string) {
const position = MESSAGE_QUEUE.findIndex((item) => item.id === id);
MESSAGE_QUEUE.splice(position, 1);
renderMessage([...MESSAGE_QUEUE]);
}
// Message 组件
function BaseMessage(props: any) {
const { type, message, id } = props;
const refMessage = useRef<any>();
const classMap = {
base: "",
visible: "wdu-message__visible",
hidden: "wdu-message__hidden",
};
const { addClassName, classList } = useCssClassManager(classMap);
const clear = () => removeMessage(id);
const handleHidden = () => {
if (refMessage.current) {
refMessage.current.addEventListener("animationend", clear, {
once: true,
});
}
addClassName("hidden");
};
useEffect(() => {
addClassName("visible");
setTimeout(() => {
handleHidden();
}, 3000);
}, []);
return (
<p
ref={refMessage}
className={`wdu-message wdu-message__${type} ${classList}`}
>
{message}
</p>
);
}
// 组件渲染
let containerRoot: any;
function renderMessage(messageQueue: Array<any>) {
const container = createContainer();
if (!containerRoot) {
containerRoot = createRoot(container);
}
const MessageComponents = messageQueue.map((props) => {
return <BaseMessage {...props} key={props.id} />;
});
containerRoot.render(MessageComponents);
}
// 对外导出的 api
const Message: propsMessage = {
info: (message: string) => addMessage({ type: "info", message }),
warn: (message: string) => addMessage({ type: "warning", message }),
error: (message: string) => addMessage({ type: "error", message }),
success: (message: string) => addMessage({ type: "success", message }),
};
export default Message;
总结
本篇文章向大家介绍了如果一步步地编写一个 Message 消息提示组件,涉及到的知识点或技巧方法有:
- React.createRoot 的使用
- React 自定义 hooks 的封装
- window.crypto API 的简单使用
- 动态切换类名的实现
- 如何清晰的分离逻辑,写出单一职责的方法
这是我个人前端项目 wood-ui-react 中的子组件,完整代码请访问 wood-ui-react ,如果觉得不错,可以 start 支持下哦 😜
暂无评论内容