Preact Redaction组件遍历转换子元素时重复渲染原始内容的问题及优化咨询
问题定位:虚拟DOM节点构造错误导致的渲染异常
首先,你代码里的redactContent函数在处理非字符串子元素(也就是Preact虚拟DOM节点)时,构造新元素的方式完全不符合Preact的虚拟DOM规范,这就是导致原始内容被额外渲染的核心原因。
你当前的错误逻辑:
const {children, ...rest} = child.props; return { ...rest, props: { ...child.props, children: redactContent(secinfo, level, child.props.children) } };
Preact的虚拟DOM节点是包含type(元素类型,比如"div"或组件函数)、props、key、ref等顶层属性的对象,而你把child.props里的属性(比如class、id)直接展开到返回对象的顶层,又额外嵌套了props字段,这完全是一个无效的虚拟DOM节点结构。Preact无法识别这个错误结构,要么渲染出异常内容,要么 fallback 渲染原始节点,最终导致处理后的内容和原始内容同时出现。
修复方案:正确构造虚拟DOM节点
你需要保留原始虚拟DOM节点的核心属性(type、key、ref等),仅替换children即可。推荐用Preact官方提供的cloneElement工具函数,它能帮你自动处理所有节点属性的复制,避免手动构造出错:
首先导入cloneElement:
import { h, Children, cloneElement } from 'preact';
然后修改redactContent里的虚拟DOM节点处理逻辑:
else if (child.props) { return cloneElement(child, { children: redactContent(secinfo, level, child.props.children) }); }
cloneElement会自动保留原始节点的所有原有属性和结构,你只需要传入要修改的children即可,既简洁又不容易出错。修复后,Preact就能正确渲染处理后的内容,不会再出现原始内容的重复渲染。
更可维护的标准写法优化
你的核心递归遍历思路是对的,但可以从以下几个方面优化,让代码更符合Preact社区最佳实践,提升可维护性:
1. 避免组件内部直接操作DOM
你现在在Redaction组件里直接用document.getElementById('secinfo')读取DOM数据,这会让组件和DOM强耦合,复用性、可测试性都很差。建议让父组件负责获取权限数据,通过props传递给Redaction:
// 父组件使用示例 const secinfo = document.getElementById('secinfo'); const elevatedUsers = JSON.parse(secinfo?.getAttribute('data-elevatedUsers') || '{}'); <Redaction level={2} elevatedUsers={elevatedUsers}> {/* 需要处理的内容 */} </Redaction> // Redaction组件简化为 interface RedactionProps { level: number; children: any; elevatedUsers: elevatedUserList; } export function Redaction(props: RedactionProps) { return ( <div class="redaction" data-level={props.level}> {redactContent(props.elevatedUsers, props.level, props.children)} </div> ); }
2. 增强类型安全
当前代码里children: any的类型太宽泛,建议用Preact提供的ComponentChildren和VNode类型来约束,TypeScript能在编译阶段帮你发现潜在错误:
import type { ComponentChildren, VNode } from 'preact'; interface RedactionProps { level: number; children: ComponentChildren; elevatedUsers: elevatedUserList; } function redactContent(secinfo: elevatedUserList, level: number, content: ComponentChildren): ComponentChildren { return Children.map(content, (child: VNode | string | null | undefined): ComponentChildren => { // 你的处理逻辑 }); }
3. 处理边界情况
补充对null/undefined/boolean等Preact会自动忽略的类型的处理,避免递归逻辑报错:
function redactContent(secinfo: elevatedUserList, level: number, content: ComponentChildren): ComponentChildren { return Children.map(content, (child: VNode | string | null | undefined): ComponentChildren => { if (child == null || typeof child === 'boolean') { return child; } if (typeof child === 'string') { return isElevated(secinfo, level, true) ? child : child.replace(/\S/g, '█'); } if (child.props) { return cloneElement(child, { children: redactContent(secinfo, level, child.props.children) }); } return child; }); }
完整修复后的代码示例
import { h, Children, cloneElement } from 'preact'; import { isElevated } from "../stores/UserStateStore.ts"; import type { elevatedUserList } from "../../../plugins/remark/util/remarkOfWikiLinks-utils.ts"; import type { ComponentChildren, VNode } from 'preact'; interface RedactionProps { level: number; children: ComponentChildren; elevatedUsers: elevatedUserList; } function redactContent(secinfo: elevatedUserList, level: number, content: ComponentChildren): ComponentChildren { return Children.map(content, (child: VNode | string | null | undefined): ComponentChildren => { // 处理Preact会忽略的空值/布尔值 if (child == null || typeof child === 'boolean') { return child; } // 处理文本节点 if (typeof child === 'string') { return isElevated(secinfo, level, true) ? child : child.replace(/\S/g, '█'); } // 处理虚拟DOM节点 if (child.props) { return cloneElement(child, { children: redactContent(secinfo, level, child.props.children) }); } return child; }); } export function Redaction(props: RedactionProps) { return ( <div class="redaction" data-level={props.level}> {redactContent(props.elevatedUsers, props.level, props.children)} </div> ); }
这样修改后,不仅解决了重复渲染的问题,代码也更符合Preact的设计理念,可维护性和复用性都有明显提升。
内容来源于stack exchange

