前言
在前端开发中,遮罩层(Mask)是一个常见的 UI 组件。但在某些场景下,我们需要确保遮罩层始终显示在页面上,即使发生了 DOM 操作也不会被意外移除。今天就和大家分享一个基于 React 实现的持久遮罩层方案。
核心功能
- 创建全屏遮罩层
- 防止遮罩层被意外移除或修改
技术实现解析
1. 基础架构设计
这里采用函数式 API 设计,通过 options 参数支持自定义配置,返回销毁方法供外部调用。
- 遮罩层创建与样式处理
- 使用原生 DOM API 创建遮罩容器,并通过
Object.assign合并默认样式和自定义样式。这种方式比 React 的 Portal 方案更灵活,能更好地控制 DOM 结构。 - 使用 React 18 的
createRootAPI 创建根节点,将遮罩组件渲染到容器中。这里巧妙地使用isInitReact标志位来防止初始化过程中的重复渲染。 - DOM 监听与自动恢复
这是整个方案的核心亮点:使用 MutationObserver 监听 DOM 变化,自动恢复被移除或修改的遮罩层。
性能优化与最佳实践
- 防抖处理:
通过延时重置 isInitReact 标志位,避免初始化阶段的重复渲染。
- 资源释放:
return {
destroy: () => {
observer.disconnect();
observer = null;
originRoot?.unmount();
originRoot = null;
},
};
提供 destroy 方法,确保资源正确释放,防止内存泄漏。
使用注意事项
- 需要注意遮罩层的 z-index 值,确保显示在最上层
- 自定义样式时要谨慎覆盖position等关键属性
- 在组件卸载时调用 destroy 方法清理资源
实战应用示例
javascript
Copy
// 创建遮罩
const mask = openMask({
idKey: 'custom-mask',
style: {
backgroundColor: 'rgba(0,0,0,0.7)',
}
});
// 销毁遮罩
mask.destroy();
总结
这个实现方案巧妙地结合了 React 组件系统和原生 DOM API,通过 MutationObserver 实现了遮罩层的持久化显示。虽然实现相对复杂,但在特定场景下(如视频播放、支付场景等)非常实用。
当然,这个方案还可以进一步优化,比如:
- 添加动画效果支持
- 提供更多自定义配置选项
- 优化 MutationObserver 的性能开销
希望这篇文章对大家有所帮助!欢迎在评论区讨论和分享你的想法。
源码
- index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import MaskCmp from './MaskCmp';
export const openMask = (
options: { idKey?: string; style?: React.CSSProperties } = {},
) => {
/** 基础数据初始化 */
const idKey = options.idKey || 'mask-for-video';
let originRoot;
let observer;
let isInitReact = false;
const baseStyle = Object.assign(
{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100%',
height: '100%',
zIndex: 9999,
backgroundColor: 'rgba(0,0,0,0.5)',
},
options.style || {},
);
/** 处理自定义react节点渲染 */
const renderDom = (wrapper) => {
isInitReact = true;
originRoot = createRoot(wrapper);
originRoot.render(<MaskCmp open={true} />);
setTimeout(() => {
isInitReact = false;
}, 1000);
};
/** 创建遮罩层 */
const createMask = () => {
// 如果已经有,则移除旧节点
let tempNode = document.getElementById(idKey);
if (tempNode) {
tempNode.remove();
}
// 创建新节点
const div = document.createElement('div');
// 设置固定定位
div.id = idKey;
Object.assign(div.style, baseStyle);
// 阻止事件
div.onclick = (event) => {
event.preventDefault();
event.stopPropagation();
};
document.body.appendChild(div);
//处理react组件渲染挂载
renderDom(div);
};
/** 启动观察者监听修改 */
const startObserver = () => {
/**实例化观察标签实例 */
observer = new MutationObserver((mutations) => {
if (isInitReact) {
return;
}
let tempNode = document.getElementById(idKey);
if (!tempNode) {
const target = mutations[0].target;
// 如果在body中删除节点,则重新创建,需要排除document.body
if (target !== document.body) {
target?.remove?.();
}
// eslint-disable-next-line @typescript-eslint/no-use-before-define
initMask();
return;
}
mutations.forEach((mutation) => {
// const target = mutation.target;
const isOptCur = mutation.target.id === idKey;
if (isOptCur && mutation.type === 'childList') {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
initMask(); // 当前节点移除
} else if (isOptCur && mutation.type === 'attributes') {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
initMask(); // 处理属性修改
}
});
});
observer.observe(document.body, {
attributes: true /**表示观察节点和子节点属性发生变化,触发回调 */,
childList: true /**观察目标节点和子节点标签增删改的时候触发 */,
subtree: true /**观察目标节点的整个子树 */,
// characterData: true, // 观察文本内容变化
});
};
/** 主逻辑 */
const initMask = () => {
createMask();
startObserver();
};
initMask();
/** 返回销毁函数 */
return {
destroy: () => {
observer.disconnect();
observer = null;
originRoot?.unmount();
originRoot = null;
},
};
};
- MaskCmp.tsx
const MaskCmp = () => {
return (
<div
style={{
border: '1px solid gray',
width: '100%',
height: '100%',
}}
>
<video
style={{ width: '100%' }}
autoPlay={true}
tabIndex={-1}
src="https://media.w3.org/2010/05/sintel/trailer.mp4"
></video>
</div>
);
};
export default MaskCmp;