背景
有个需要写h5活动页的临时需求,开始时需求范围比较小,直接拿原生HTML+JavaScript
去写的,后面需求蔓延,里面涉及了一些不同场景下的弹窗,弹窗中还涉及到收集用户信息,于是产生不如还是用框架来写,然后再构建一个静态包的形式,这样无论是改样式还是需求变更都更好维护一些。这里选择的是React + typescript + less
。
过程
由于有第一版方案是原生的html
,也没有使用UI框架,所以弹窗也是拿原生js
来写的,在移植到React
的过程中,想要把弹窗这部分逻辑抽成公共的hook
。
众所周知,React18
废弃了react-dom
的render
方法,对于页面初始化来说,变化不大,调用createRoot
就好,但是对于这种动态插入DOM
就要做些改动了。之前一直用UI框架直接梭哈,也没太留意升级后的实现,这也算是一种尝试。
但先明确我的需求是,类似实现一个alert
功能,可以直接通过函数调用 的形式来插入一段DOM
,像这样
js
showAlert({ content: "这是一段alert" })
当然,它需要一个关闭按钮,点击按钮就可以关闭这个弹窗。
查阅官网,首先就是createPortal
方法,
正如介绍所说,它可以把JSX
即时渲染到DOM
中,来看一下它的使用,
但让人失望的是,它需要把createPortal
的返回值直接放到JSX
中,这显然无法达成实现函数调用来插入的效果。
换个思路,createPortal
是生成一个ReactNode
,那不如再生成一个根节点createRoot
,然后通过render
来渲染这个createPortal
生成的ReactNode
,
js
const generate = (children: ReactNode) => {
const wrap = document.createElement("div");
const root = createRoot(wrap);
// 插入DOM
document.body.appendChild(wrap);
// 渲染组件
root.render(createPortal(children, wrap));
};
果然可行!那么接下来就是如何销毁这个ReactNode
的问题。
中间尝试了一些方案,想尽可能让外部无感知的去销毁掉,但由于想通过hook
的形式调用,那如果在某个组件中多次执行了generate
,想去定点销毁某个弹窗就会变得格外困难,于是采用生成key
的方式。
js
const useExtraRender = (props: extraRenderProps = {}) => {
// 记录一下当前的所有生成的root
const rootMap = useRef<Map<string, () => void>>(new Map());
const generate = (children: ReactNode) => {
// 生成一个唯一标识
const key = generateUUID();
const wrap = document.createElement("div");
const root = createRoot(wrap);
// 插入DOM
document.body.appendChild(wrap);
// 渲染组件
root.render(createPortal(children, wrap));
// 注册销毁函数
const destroy = () => {
root.unmount();
document.body.removeChild(wrap);
// 解除引用
rootMap.current.delete(key);
};
rootMap.current.set(key, destroy);
// key作为generate的返回值
return key;
};
const destroy = (key: string) => {
const cb = rootMap.current.get(key);
cb && cb();
};
return {
generate,
destroy,
};
}
至此,想要的效果实现了90%,还差通过弹窗内部的关闭按钮去关闭弹窗功能,这里我选择使用cloneElement
的形式把destory
包装进去。最好来看一下实现吧,我拆成了2个hook
,一个用于管理这种渲染,一个封装Alert
。
useExtraRender.tsx
js
import { ReactElement, cloneElement, useRef } from "react";
import { createPortal } from "react-dom";
import { createRoot } from "react-dom/client";
import { generateUUID } from "@src/utils";
interface extraRenderProps {
generateContainer?: () => HTMLDivElement;
}
const generateDiv = () => {
const wrapper = document.createElement("div");
document.body.appendChild(wrapper);
return wrapper;
};
const useExtraRender = (props: extraRenderProps = {}) => {
const { generateContainer = generateDiv } = props;
const rootMap = useRef<Map<string, () => void>>(new Map());
const generate = (children: ReactElement) => {
const wrap = generateContainer();
const root = createRoot(wrap);
const key = generateUUID();
// 注册销毁函数
const destroy = () => {
root.unmount();
document.body.removeChild(wrap);
// 解除引用
rootMap.current.delete(key);
};
// 渲染组件
root.render(
createPortal(
cloneElement(children, {
destroy,
}),
wrap
)
);
rootMap.current.set(key, destroy);
return key;
};
const destroy = (key: string) => {
console.log("destroy!!");
const cb = rootMap.current.get(key);
cb && cb();
};
return {
generate,
destroy,
};
};
export default useExtraRender;
useExtraRender.tsx
js
import { ReactNode } from "react";
import useExtraRender from "../useExtraRender";
import styles from "./index.module.less";
interface alertArgs {
width?: number | string;
}
interface alertContentProps {
content: ReactNode;
maskClosable?: boolean;
destroy?: () => void;
onClose?: (from?: string) => void;
}
const useAlert = (props: alertArgs = {}) => {
const { width = "70vw" } = props;
const { generate } = useExtraRender();
const AlertContent = (props: alertContentProps) => {
const { content, onClose, destroy, maskClosable = false } = props;
const handleClose = (from: string) => {
onClose && onClose(from);
destroy && destroy();
};
return (
<div
className={styles.mask}
onClick={() => maskClosable && handleClose("mask")}
>
<div
className={styles.box}
style={{
width,
}}
onClick={(e) => e.stopPropagation()}
>
<div className={styles.wrap}>{content}</div>
<button
className={styles.closeBtn}
onClick={(e) => {
e.stopPropagation();
handleClose("button");
}}
style={{ marginTop: "10px" }}
></button>
</div>
</div>
);
};
const show = (alertProps: alertContentProps) => {
generate(<AlertContent {...alertProps} />);
};
return {
show,
};
};
export default useAlert;
只是一个简单的尝试,足够覆盖需求的场景,如果一些复杂的操作当然还是建议使用UI框架,因为通常具有更好的性能调优。