脱敏节点回显逻辑
脱敏节点从接口获取到界面回显的完整流程
概述
本文档详细解析了脱敏功能从后端接口获取数据到前端界面回显的完整流程,包括数据获取、结构转换、DOM 节点创建、界面渲染和用户交互等各个环节。
核心技术亮点:
- 🔧 replaceRangeWithTuoMing: 脱敏功能的核心转换函数,负责将文本范围替换为脱敏容器
- 🛡️ 冲突检测机制: 使用 Range API 防止重复脱敏,确保操作的幂等性
- ⚡ 原子性操作: 事务性的 DOM 操作,保证数据一致性
- 🎯 精确定位: 基于章节 ID 和偏移量的精确文本定位
- 🔄 状态管理: 完整的脱敏/恢复状态切换机制
1. 数据获取阶段
1.1 接口请求
脱敏数据的获取主要通过 /original/content/detail
接口实现:
// 在 Template 组件中调用
const [getHtmlContentDetail] = useAxios("/original/content/detail", "post");
const getHtmlContent = async () => {
const params = {
proId: finalProId,
ptsId,
hospitalNo,
visitViewParam: {
recordId,
code,
name,
id,
},
selectionId, // 可选参数
};
const res = await getHtmlContentDetail(params);
setOriginalData(res); // 设置原始数据
};
1.2 接口返回的数据结构
// 接口返回的原始数据结构
{
documentId: "doc123",
content: "原始HTML内容",
highLightPositionList: [
{
id: "highlight123", // highLightPositionId - 脱敏记录的唯一标识
startOffset: 100, // 文本起始位置
endOffset: 120, // 文本结束位置
sectionId: "section1", // 章节ID
type: "tuo-min", // 类型标识
value: "敏感文本" // 原始敏感文本内容
}
]
}
1.3 脱敏数据提取和存储
// 从接口返回的数据中提取脱敏信息
const newHtmlDesensitizationDocumentIdList = [];
res?.forEach((resItem) => {
// 确保 highLightPositionList 是数组并且有值
if (
Array.isArray(resItem.highLightPositionList) &&
resItem.highLightPositionList.length > 0
) {
resItem.highLightPositionList.forEach((item) => {
const { id, documentId } = item;
if (id && documentId) {
newHtmlDesensitizationDocumentIdList.push({
highLightPositionId: id,
documentId,
});
}
});
}
});
// 存储到全局状态中,用于后续的状态管理
setHtmlDesensitizationDocumentIdList(newHtmlDesensitizationDocumentIdList);
2. 数据结构转换阶段
2.1 渲染数据准备
在 useNodeRender
的 renderRange
函数中,将接口数据转换为可渲染的格式:
const renderRange = (iframeDocument, data) => {
// 处理脱敏类型的数据
data?.map((item, index) => {
if (item && item.type === "tuo-min") {
try {
const { sectionId, endOffset, startOffset } = item;
// 根据章节ID和位置信息创建文本范围
const iframeRange = createTextRange(
iframeDocument.body.querySelectorAll(
`[prime-section-id="${sectionId}"]`
),
startOffset,
endOffset
);
const rangeRelativeToOrigin = {
startOffset: item.startOffset,
endOffset: item.endOffset,
};
// 执行脱敏渲染
replaceRangeWithTuoMing(
iframeRange,
iframeDocument,
item,
rangeRelativeToOrigin,
item.id
);
} catch (error) {
console.log("脱敏渲染失败:", error);
}
}
});
};
2.2 文本范围创建
// createTextRange 函数负责根据位置信息创建精确的文本范围
const iframeRange = createTextRange(nodeList, startOffset, endOffset);
// 该函数会:
// 1. 遍历指定的DOM节点列表
// 2. 根据startOffset和endOffset计算精确位置
// 3. 处理HTML实体和空白字符
// 4. 返回一个Range对象,用于后续的DOM操作
2.3 核心脱敏转换函数 - replaceRangeWithTuoMing
replaceRangeWithTuoMing
是脱敏功能的核心函数,负责将指定的文本范围替换为脱敏容器。
2.3.1 函数签名和参数
const replaceRangeWithTuoMing = useCallback(
(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
// 函数实现
},
[performInitialDesensitization]
);
参数说明:
参数名 | 类型 | 说明 |
---|---|---|
range |
Range | DOM Range 对象,表示需要脱敏的文本范围 |
iframeDocument |
Document | iframe 的文档对象,用于 DOM 操作 |
rangeItem |
Object | 范围配置项,包含 containerId 等信息 |
rangeRelativeToOrigin |
Object | 相对于原始文档的位置信息 |
highLightPositionId |
String | 脱敏记录的唯一标识 ID |
2.3.2 核心逻辑实现
const replaceRangeWithTuoMing = useCallback(
(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
// 1. 检查冲突:查找文档中所有已存在的脱敏容器
const primeTuoMing = iframeDocument.querySelectorAll(
".prime-tuo-min-container"
);
// 2. 冲突检测:判断当前区间是否与已存在的脱敏区间重叠
const containsTuoMing = Array.from(primeTuoMing).some((item) => {
return range.intersectsNode(item);
});
// 3. 执行脱敏:只有在没有冲突的情况下才执行脱敏操作
if (!containsTuoMing) {
performInitialDesensitization(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
);
}
},
[performInitialDesensitization]
);
2.3.3 工作流程详解
步骤 1:冲突检测
// 获取文档中所有已存在的脱敏容器
const primeTuoMing = iframeDocument.querySelectorAll(
".prime-tuo-min-container"
);
// 使用Range.intersectsNode()方法检测是否有重叠
const containsTuoMing = Array.from(primeTuoMing).some((item) => {
return range.intersectsNode(item);
});
- 目的: 防止重复脱敏,避免在已脱敏的区域再次创建脱敏容器
- 方法: 使用
Range.intersectsNode()
API 检测当前范围是否与已存在的脱敏容器有交集 - 优势: 确保脱敏操作的幂等性,避免 DOM 结构混乱
步骤 2:条件执行
if (!containsTuoMing) {
performInitialDesensitization(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
);
}
- 条件: 只有在没有检测到冲突时才执行脱敏
- 操作: 调用
performInitialDesensitization
函数执行实际的 DOM 替换
2.3.4 performInitialDesensitization 函数详解
这是实际执行脱敏 DOM 操作的核心函数:
const performInitialDesensitization = useCallback(
(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
// 1. 保存原文内容
const fragment = range.cloneContents(); // 克隆原始内容
range.deleteContents(); // 删除原始内容
// 2. 创建脱敏容器的各个组件
const tuoMingNode = createDesensitizedNode(
fragment.textContent,
rangeItem.containerId,
highLightPositionId
);
const restoreNode = createRestoreNode(fragment, rangeItem.containerId);
const toggleBtn = createToggleButton(
highLightPositionId,
rangeRelativeToOrigin.startOffset,
rangeRelativeToOrigin.endOffset
);
// 3. 组装完整的脱敏容器
const container = iframeDocument.createElement("span");
container.className = "prime-tuo-min-container";
container.appendChild(tuoMingNode); // 脱敏显示节点
container.appendChild(restoreNode); // 原文存储节点
container.appendChild(toggleBtn); // 操作按钮
// 4. 设置初始显示状态
tuoMingNode.classList.add("prime-tuo-min-show");
tuoMingNode.classList.remove("prime-tuo-min-hidden");
restoreNode.classList.add("prime-tuo-min-restore-hidden");
restoreNode.classList.remove("prime-tuo-min-restore-show");
toggleBtn.classList.add("prime-tuo-min-undo-btn");
toggleBtn.classList.remove("prime-tuo-min-redo-btn");
// 5. 设置按钮的数据属性
toggleBtn.dataset.state = "desensitized";
toggleBtn.dataset.highLightPositionId = highLightPositionId;
toggleBtn.title = t("es.tuomin.cancel");
toggleBtn.style.transform = "";
// 6. 将容器插入到原始位置
range.insertNode(container);
},
[createDesensitizedNode, createRestoreNode, createToggleButton, t]
);
详细步骤说明:
- 内容保存: 使用
range.cloneContents()
克隆原始内容,然后删除原始内容 - 节点创建: 分别创建脱敏显示节点、原文存储节点和操作按钮
- 容器组装: 创建容器元素并按顺序添加子节点
- 状态设置: 设置初始的 CSS 类名,确保脱敏内容显示,原文隐藏
- 属性配置: 为按钮设置必要的数据属性和样式
- DOM 插入: 将完整的容器插入到原始文本位置
2.3.5 函数调用时机
replaceRangeWithTuoMing
函数在以下场景中被调用:
- 页面初始化: 从接口获取脱敏数据后,批量渲染已存在的脱敏项
- 用户操作: 用户选择文本并点击脱敏按钮时
- 状态恢复: 从其他页面返回时恢复脱敏状态
2.3.6 错误处理和边界情况
// 在renderRange函数中的错误处理
try {
const iframeRange = createTextRange(nodeList, startOffset, endOffset);
const rangeRelativeToOrigin = {
startOffset: rangeItem.startOffset,
endOffset: rangeItem.endOffset,
};
replaceRangeWithTuoMing(
iframeRange,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
rangeItem.id
);
} catch (error) {
console.log("脱敏渲染失败:", error);
// 跳过当前项,继续处理其他脱敏项
}
处理的边界情况:
- Range 创建失败: 当文本位置无效时,跳过该项
- DOM 操作异常: 当 iframe 文档不可用时,避免程序崩溃
- 重复脱敏: 通过冲突检测避免重复操作
- 数据不完整: 当必要参数缺失时,函数会安全退出
3. DOM 节点创建阶段
3.1 脱敏容器的整体结构
脱敏功能通过创建一个复合的 DOM 结构来实现,主要包含三个部分:
const performInitialDesensitization = (
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
// 1. 保存原文内容
const fragment = range.cloneContents();
range.deleteContents();
// 2. 创建各个子节点
const tuoMingNode = createDesensitizedNode(
fragment.textContent,
rangeItem.containerId,
highLightPositionId
);
const restoreNode = createRestoreNode(fragment, rangeItem.containerId);
const toggleBtn = createToggleButton(
highLightPositionId,
rangeRelativeToOrigin.startOffset,
rangeRelativeToOrigin.endOffset
);
// 3. 组装完整容器
const container = iframeDocument.createElement("span");
container.className = "prime-tuo-min-container";
container.appendChild(tuoMingNode); // 脱敏显示节点
container.appendChild(restoreNode); // 原文存储节点
container.appendChild(toggleBtn); // 操作按钮
// 4. 设置初始显示状态
tuoMingNode.classList.add("prime-tuo-min-show");
restoreNode.classList.add("prime-tuo-min-restore-hidden");
toggleBtn.classList.add("prime-tuo-min-undo-btn");
// 5. 插入到DOM中替换原始文本
range.insertNode(container);
};
3.2 脱敏显示节点创建
const createDesensitizedNode = (text, containerId, highLightPositionId) => {
const iframeDocument = iframeRef.current?.contentWindow?.document;
const tuoMingNode = iframeDocument.createElement("span");
// 设置基本属性
tuoMingNode.className = "prime-tuo-min-show";
tuoMingNode.id = containerId;
tuoMingNode.textContent = "***"; // 显示脱敏标记
// 设置状态标识
tuoMingNode.dataset.state = "desensitized";
// 关联脱敏记录ID
if (highLightPositionId) {
tuoMingNode.dataset.highLightPositionId = highLightPositionId;
}
return tuoMingNode;
};
3.3 原文存储节点创建
const createRestoreNode = (fragment, containerId) => {
const iframeDocument = iframeRef.current?.contentWindow?.document;
const restoreNode = iframeDocument.createElement("span");
// 保存原始内容
restoreNode.appendChild(fragment);
// 设置样式类名(初始状态为隐藏)
restoreNode.className =
"prime-tuo-min-restore-hidden prime-tuo-min-origin-content";
restoreNode.id = containerId;
restoreNode.dataset.state = "original";
return restoreNode;
};
3.4 操作按钮创建
const createToggleButton = (highLightPositionId, startOffset, endOffset) => {
const iframeDocument = iframeRef.current?.contentWindow?.document;
const toggleBtn = iframeDocument.createElement("img");
// 设置按钮的初始状态(撤销脱敏)
toggleBtn.className = "prime-tuo-min-undo-btn";
toggleBtn.src = "/undo.svg";
toggleBtn.title = t("es.tuomin.cancel");
toggleBtn.style.cursor = "pointer";
// 设置操作类型和关联数据
toggleBtn.dataset.type = "_undo";
toggleBtn.dataset.state = "desensitized";
// 关联脱敏记录和位置信息
if (highLightPositionId) {
toggleBtn.dataset.highLightPositionId = highLightPositionId;
}
toggleBtn.dataset.startOffset = startOffset;
toggleBtn.dataset.endOffset = endOffset;
return toggleBtn;
};
4. 界面回显阶段
4.1 最终的 DOM 结构
经过上述处理后,在 iframe 中会生成如下的 DOM 结构:
<span class="prime-tuo-min-container">
<!-- 脱敏显示节点(默认显示) -->
<span
class="prime-tuo-min-show"
data-state="desensitized"
data-high-light-position-id="highlight123"
>
***
</span>
<!-- 原文存储节点(默认隐藏) -->
<span
class="prime-tuo-min-restore-hidden prime-tuo-min-origin-content"
data-state="original"
>
敏感文本内容
</span>
<!-- 操作按钮(撤销脱敏) -->
<img
class="prime-tuo-min-undo-btn"
src="/undo.svg"
title="取消脱敏"
data-type="_undo"
data-state="desensitized"
data-high-light-position-id="highlight123"
data-start-offset="100"
data-end-offset="120"
style="cursor: pointer;"
/>
</span>
4.2 CSS 样式控制
通过 CSS 类名控制不同状态下的显示效果:
/* 脱敏状态 - 显示 *** */
.prime-tuo-min-show {
display: inline;
color: #ff6b6b;
font-weight: bold;
}
/* 脱敏状态 - 隐藏原文 */
.prime-tuo-min-restore-hidden {
display: none;
}
/* 原文状态 - 隐藏 *** */
.prime-tuo-min-hidden {
display: none;
}
/* 原文状态 - 显示原文 */
.prime-tuo-min-restore-show {
display: inline;
}
/* 操作按钮样式 */
.prime-tuo-min-undo-btn,
.prime-tuo-min-redo-btn {
width: 16px;
height: 16px;
margin-left: 4px;
vertical-align: middle;
cursor: pointer;
}
4.3 初始显示状态
脱敏节点创建后的初始状态:
- ✅ 脱敏标记显示:
***
可见 - ❌ 原文内容隐藏: 敏感文本不可见
- 🔄 撤销按钮: 显示撤销图标,点击可恢复原文
5. 用户交互逻辑
5.1 撤销脱敏操作
当用户点击撤销按钮时:
const handleUndo = (targetButton) => {
const container = targetButton.closest('.prime-tuo-min-container')
const showNode = container.querySelector('.prime-tuo-min-show')
const restoreNode = container.querySelector('.prime-tuo-min-restore-hidden')
// 1. 切换显示状态
showNode.classList.add('prime-tuo-min-hidden')
showNode.classList.remove('prime-tuo-min-show')
restoreNode.classList.add('prime-tuo-min-restore-show')
restoreNode.classList.remove('prime-tuo-min-restore-hidden')
// 2. 更新按钮状态
targetButton.src = '/redo.svg'
targetButton.dataset.type = '_redo'
targetButton.title = t('es.tuomin.redo')
targetButton.classList.remove('prime-tuo-min-undo-btn')
targetButton.classList.add('prime-tuo-min-redo-btn')
// 3. 调用后端接口取消脱敏
const params = {
highLightPositionId: targetButton.dataset.highLightPositionId,
sectionId: primeSectionId,
ptsId,
value: restoreNode.textContent
}
await desensitizationCancel(params)
}
5.2 重新脱敏操作
当用户点击重做按钮时:
const handleRedo = (targetButton) => {
const container = targetButton.closest('.prime-tuo-min-container')
const showNode = container.querySelector('.prime-tuo-min-hidden')
const restoreNode = container.querySelector('.prime-tuo-min-restore-show')
// 1. 恢复脱敏显示状态
showNode.classList.add('prime-tuo-min-show')
showNode.classList.remove('prime-tuo-min-hidden')
restoreNode.classList.add('prime-tuo-min-restore-hidden')
restoreNode.classList.remove('prime-tuo-min-restore-show')
// 2. 恢复按钮状态
targetButton.src = '/undo.svg'
targetButton.dataset.type = '_undo'
targetButton.title = t('es.tuomin.cancel')
targetButton.classList.remove('prime-tuo-min-redo-btn')
targetButton.classList.add('prime-tuo-min-undo-btn')
// 3. 调用后端接口重新脱敏
const params = {
operationType: 1,
ptsId,
value: restoreNode.textContent,
documentId: renderKey,
sectionId: primeSectionId
}
await desensitizationOperate(params)
}
5.3 状态切换对照表
操作 | 脱敏标记(***) | 原文内容 | 按钮图标 | 按钮功能 | 后端操作 |
---|---|---|---|---|---|
初始脱敏 | ✅ 显示 | ❌ 隐藏 | undo.svg | 撤销脱敏 | - |
撤销脱敏 | ❌ 隐藏 | ✅ 显示 | redo.svg | 重新脱敏 | 调用取消接口 |
重新脱敏 | ✅ 显示 | ❌ 隐藏 | undo.svg | 撤销脱敏 | 调用脱敏接口 |
6. 数据流和生命周期
6.1 完整数据流图
graph TD
A[接口请求] --> B[数据获取]
B --> C[数据解析]
C --> D[位置计算]
D --> E[DOM节点创建]
E --> F[界面渲染]
F --> G[用户交互]
G --> H[状态切换]
H --> I[后端同步]
I --> J[界面更新]
B --> B1[highLightPositionList]
C --> C1[startOffset/endOffset]
D --> D1[createTextRange]
E --> E1[脱敏容器DOM]
F --> F1[CSS样式控制]
G --> G1[点击按钮]
H --> H1[DOM类名切换]
I --> I1[API调用]
J --> J1[状态同步]
6.2 关键时间节点
- T1 - 页面加载: 调用
/original/content/detail
接口 - T2 - 数据解析: 提取
highLightPositionList
数据 - T3 - DOM 创建: 创建脱敏容器和子节点
- T4 - 界面渲染: 显示脱敏标记,隐藏原文
- T5 - 用户交互: 点击撤销/重做按钮
- T6 - 状态切换: 更新 DOM 类名和按钮状态
- T7 - 后端同步: 调用相应的 API 接口
- T8 - 状态确认: 更新全局状态管理
6.3 replaceRangeWithTuoMing 在数据流中的关键作用
6.3.1 函数调用链路
graph TD
A[接口数据] --> B[renderRange函数]
B --> C[createTextRange创建Range]
C --> D[replaceRangeWithTuoMing]
D --> E{冲突检测}
E -->|无冲突| F[performInitialDesensitization]
E -->|有冲突| G[跳过处理]
F --> H[创建脱敏容器]
H --> I[DOM插入完成]
style D fill:#ff9999
style F fill:#99ccff
6.3.2 函数执行时机详解
时机 | 触发场景 | 数据来源 | 执行结果 |
---|---|---|---|
页面初始化 | 组件挂载后 | 接口返回的 highLightPositionList |
批量创建已存在的脱敏项 |
用户脱敏操作 | 点击脱敏按钮 | 用户选择的文本范围 | 创建新的脱敏项 |
状态恢复 | 页面刷新/返回 | 缓存的脱敏状态 | 恢复之前的脱敏状态 |
数据更新 | 接口数据变化 | 更新后的脱敏数据 | 重新渲染脱敏项 |
6.3.3 性能关键点
// 批量处理优化
const batchProcessDesensitization = (desensitizationList) => {
// 1. 预处理:按章节分组
const groupedBySection = desensitizationList.reduce((acc, item) => {
const { sectionId } = item;
if (!acc[sectionId]) acc[sectionId] = [];
acc[sectionId].push(item);
return acc;
}, {});
// 2. 按章节批量处理,减少DOM查询
Object.entries(groupedBySection).forEach(([sectionId, items]) => {
const sectionNodes = iframeDocument.body.querySelectorAll(
`[prime-section-id="${sectionId}"]`
);
items.forEach((item) => {
try {
const range = createTextRange(
sectionNodes,
item.startOffset,
item.endOffset
);
replaceRangeWithTuoMing(
range,
iframeDocument,
item,
item.rangeRelativeToOrigin,
item.id
);
} catch (error) {
console.log("批量脱敏处理失败:", error);
}
});
});
};
6.3.4 错误恢复机制
// 错误恢复和重试机制
const safeReplaceRangeWithTuoMing = async (
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
const maxRetries = 3;
let retryCount = 0;
while (retryCount < maxRetries) {
try {
replaceRangeWithTuoMing(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
);
break; // 成功则退出循环
} catch (error) {
retryCount++;
console.warn(`脱敏操作失败,重试第${retryCount}次:`, error);
if (retryCount >= maxRetries) {
// 记录失败的脱敏项,用于后续处理
failedDesensitizationItems.push({
rangeItem,
rangeRelativeToOrigin,
highLightPositionId,
error: error.message,
});
} else {
// 等待一段时间后重试
await new Promise((resolve) => setTimeout(resolve, 100 * retryCount));
}
}
}
};
6.4 数据一致性保证
6.4.1 状态同步机制
// 前端状态与后端数据的同步
const syncDesensitizationState = (localState, serverData) => {
const serverIds = new Set(serverData.map((item) => item.id));
const localIds = new Set(localState.map((item) => item.highLightPositionId));
// 检测不一致的项
const missingInLocal = serverData.filter((item) => !localIds.has(item.id));
const extraInLocal = localState.filter(
(item) => !serverIds.has(item.highLightPositionId)
);
// 处理不一致
if (missingInLocal.length > 0) {
console.log("需要补充的脱敏项:", missingInLocal);
// 调用 replaceRangeWithTuoMing 补充缺失项
}
if (extraInLocal.length > 0) {
console.log("需要清理的脱敏项:", extraInLocal);
// 清理多余的DOM节点
}
};
6.4.2 DOM 状态验证
// DOM状态与数据状态的一致性验证
const validateDOMConsistency = (iframeDocument, desensitizationData) => {
const domContainers = iframeDocument.querySelectorAll('.prime-tuo-min-container')
const dataIds = new Set(desensitizationData.map(item => item.id))
domContainers.forEach(container => {
const button = container.querySelector('img[data-high-light-position-id]')
const domId = button?.dataset.highLightPositionId
if (domId && !dataIds.has(domId)) {
console.warn('发现孤立的DOM节点:', container)
// 清理孤立节点
container.remove()
}
})
}
## 7. 错误处理和边界情况
### 7.1 接口异常处理
```javascript
try {
const res = await getHtmlContentDetail(params)
setOriginalData(res)
} catch (error) {
console.error('获取HTML内容失败:', error)
// 显示错误提示
message.error('加载内容失败,请重试')
}
7.2 DOM 操作异常处理
try {
const iframeRange = createTextRange(nodeList, startOffset, endOffset);
replaceRangeWithTuoMing(
iframeRange,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
);
} catch (error) {
console.log("脱敏渲染失败:", error);
// 跳过当前项,继续处理其他脱敏项
}
7.3 边界情况处理
- 空数据: 检查
highLightPositionList
是否为空数组 - 位置越界: 验证
startOffset
和endOffset
的有效性 - DOM 不存在: 确保 iframe 和相关 DOM 元素存在
- 重复脱敏: 检查当前区间是否已存在脱敏标记
8. 性能优化策略
8.1 批量处理
// 批量处理多个脱敏项,避免频繁的DOM操作
const batchRenderDesensitization = (desensitizationList) => {
const fragment = document.createDocumentFragment();
desensitizationList.forEach((item) => {
const container = createDesensitizationContainer(item);
fragment.appendChild(container);
});
// 一次性插入DOM
targetElement.appendChild(fragment);
};
8.2 事件委托
// 使用事件委托处理按钮点击,避免为每个按钮单独绑定事件
iframeDocument.addEventListener("click", (e) => {
if (e.target.dataset.type === "_undo") {
handleUndo(e.target);
} else if (e.target.dataset.type === "_redo") {
handleRedo(e.target);
}
});
8.3 状态缓存
// 缓存脱敏状态,避免重复计算
const desensitizationStateCache = new Map();
const getDesensitizationState = (highLightPositionId) => {
if (desensitizationStateCache.has(highLightPositionId)) {
return desensitizationStateCache.get(highLightPositionId);
}
const state = calculateDesensitizationState(highLightPositionId);
desensitizationStateCache.set(highLightPositionId, state);
return state;
};
9. 技术要点总结
9.1 核心技术栈
- 数据获取: Axios + useAxios Hook
- 状态管理: Zustand (全局状态) + useState (组件状态)
- DOM 操作: 原生 DOM API + Range API
- 文本处理: 自定义文本标记工具
- 样式控制: CSS 类名切换
- 国际化: react-i18next
9.2 关键设计模式
- 组合模式: 脱敏容器由多个子节点组合而成
- 状态模式: 通过状态切换控制显示效果
- 观察者模式: 通过事件监听处理用户交互
- 工厂模式: 统一的节点创建函数
- 策略模式: 不同操作类型的处理策略
9.3 replaceRangeWithTuoMing 函数的技术亮点
9.3.1 冲突检测机制
// 使用 Range.intersectsNode() API 进行精确的冲突检测
const containsTuoMing = Array.from(primeTuoMing).some((item) => {
return range.intersectsNode(item);
});
技术优势:
- 精确性: 使用浏览器原生 API 进行范围交集检测
- 性能: 避免复杂的位置计算,直接利用 DOM 树结构
- 可靠性: 防止重复脱敏导致的 DOM 结构混乱
9.3.2 原子性操作设计
// 原子性的DOM操作序列
const fragment = range.cloneContents(); // 1. 保存
range.deleteContents(); // 2. 删除
// ... 创建新节点 ...
range.insertNode(container); // 3. 插入
设计特点:
- 事务性: 要么全部成功,要么全部失败
- 一致性: 确保 DOM 状态的一致性
- 回滚能力: 出错时可以恢复原始状态
9.3.3 函数式编程范式
const replaceRangeWithTuoMing = useCallback(
(
range,
iframeDocument,
rangeItem,
rangeRelativeToOrigin,
highLightPositionId
) => {
// 纯函数逻辑,无副作用
},
[performInitialDesensitization] // 明确的依赖声明
);
优势:
- 可预测性: 相同输入产生相同输出
- 可测试性: 易于单元测试和调试
- 性能优化: React.useCallback 避免不必要的重渲染
9.3.4 错误边界处理
// 多层次的错误处理策略
try {
replaceRangeWithTuoMing(/* 参数 */);
} catch (error) {
console.log("脱敏渲染失败:", error);
// 优雅降级,继续处理其他项
}
容错机制:
- 局部失败隔离: 单个脱敏项失败不影响其他项
- 静默处理: 避免用户感知到技术错误
- 日志记录: 便于问题排查和监控
9.3.5 DOM 操作优化
// 批量DOM操作,减少重排重绘
const container = iframeDocument.createElement("span");
container.appendChild(tuoMingNode);
container.appendChild(restoreNode);
container.appendChild(toggleBtn);
range.insertNode(container); // 一次性插入
性能优化:
- 批量操作: 减少 DOM 操作次数
- 文档片段: 使用 DocumentFragment 优化性能
- 样式批处理: 统一设置 CSS 类名
9.4 架构优势
- 数据驱动: 基于接口数据驱动整个渲染流程
- 状态一致: 前端状态与后端数据保持同步
- 用户友好: 提供直观的交互界面和即时反馈
- 性能优化: 批量处理和事件委托提升性能
- 错误容错: 完善的异常处理机制
- 可维护性: 模块化设计,职责分离清晰
- 幂等性: 重复操作不会产生副作用
- 原子性: DOM 操作的事务性保证
10. 使用场景和扩展性
10.1 适用场景
- 医疗系统: 患者隐私信息脱敏
- 金融系统: 敏感财务数据保护
- 法律文档: 机密信息处理
- 企业文档: 商业机密保护
10.2 扩展可能性
- 批量脱敏: 支持一键脱敏整个文档
- 智能识别: 自动识别敏感信息
- 权限控制: 基于用户角色的脱敏权限
- 审计日志: 记录所有脱敏操作历史
- 自定义规则: 支持用户自定义脱敏规则
通过这套完整的脱敏机制,系统能够安全、高效地处理敏感信息,在保护数据隐私的同时提供良好的用户体验。
人生到处知何似,应似飞鸿踏雪泥。