在 React 中获取真实 DOM 节点:开发 EmptyGhost
组件
在 React 严格模式下,findDOMNode
方法会发出警告。因此,我设计了一个名为 EmptyGhost
的 React 组件,它允许我们优雅地获取真实的 DOM 节点,而不会引发任何警告。下面是我的开发过程。
背景
在 React 中,使用 ref
获取 DOM 节点是常见的需求。尤其是在操作事件、样式或子元素时,能够访问真实的 DOM 节点非常重要。然而,React 提供的 findDOMNode
方法由于一些潜在的性能问题,在严格模式下会发出警告。因此,我想通过创建自定义元素结合 React 的 forwardRef
和 useLayoutEffect
来实现这一功能。
组件实现
EmptyGhost
组件的核心目标是保持组件的透明性,同时让父组件能够获取到实际的 DOM 节点。为此,我使用了 customElements.define
来创建一个自定义元素,并通过 display: contents
确保它不会在布局中产生影响。
以下是完整的代码实现:
import * as React from 'react';
export interface EmptyGhostProps extends React.HTMLAttributes<HTMLDivElement> {
is?: string,
style?: Omit<React.CSSProperties, "display">
}
function assignment(node: HTMLDivElement | null, ref: React.ForwardedRef<Element | null | Text>) {
const content = (node?.firstChild || null) as (Element | Text | null);
typeof ref === 'function' ? ref(content) : ref && (ref.current = content);
}
customElements.define('empty-ghost', class Fragment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot!.innerHTML = `
<style>
:host {
display: contents !important;
}
</style>
<slot></slot>
`;
}
});
const EmptyGhostRender: React.ForwardRefRenderFunction<Element | null | Text, EmptyGhostProps> = (props, ref) => {
const contentsRef = React.useRef<HTMLDivElement | null>(null);
React.useLayoutEffect(() => {
const ob = new MutationObserver(() => {
assignment(contentsRef.current!, ref);
});
ob.observe(contentsRef.current!, { childList: true });
return () => {
ob.disconnect();
};
}, [ref]);
React.useLayoutEffect(() => {
const styles = window.getComputedStyle(contentsRef.current!);
if (styles.display !== 'contents') {
console.warn(`[EmptyGhost]:`, contentsRef.current, `better an empty element`);
}
}, []);
const { className, children, ...rest } = props;
return React.createElement("empty-ghost",
{
ref: (node: HTMLDivElement | null) => {
assignment(node, ref);
contentsRef.current = node;
},
...rest,
class: className,
style: {
...props.style,
display: undefined
},
},
children
);
}
const EmptyGhost = React.forwardRef(EmptyGhostRender);
EmptyGhost.displayName = 'EmptyGhost';
export default EmptyGhost;
核心概念
- 自定义元素 (customElements.define)
我使用 customElements.define 创建了一个名为 empty-ghost 的自定义元素。这个元素最大的特点是使用了 display: contents,这使得它不会渲染为一个可见的容器,而仅作为一个内容占位符,避免破坏布局结构。
customElements.define('empty-ghost', class Fragment extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot!.innerHTML = `
<style>
:host {
display: contents !important;
}
</style>
<slot></slot>
`;
}
});
- 使用 ref 获取真实 DOM 节点
为了让父组件能够获取到 EmptyGhost 内的真实 DOM 节点,我使用了 React 的 forwardRef 和一个辅助函数 assignment。这个函数的作用是确保传入的 ref 可以引用到子元素的真实 DOM 节点,而不是自定义元素本身。
function assignment(node: HTMLDivElement | null, ref: React.ForwardedRef<Element | null | Text>) {
const content = (node?.firstChild || null) as (Element | Text | null);
typeof ref === 'function' ? ref(content) : ref && (ref.current = content);
}
- 使用 MutationObserver 监听子元素变化
在 EmptyGhost 的子元素发生变化时,我使用 MutationObserver 来监听 DOM 变动。这样,当子元素发生变化时,ref 会自动更新。
React.useLayoutEffect(() => {
const ob = new MutationObserver(() => {
assignment(contentsRef.current!, ref);
});
ob.observe(contentsRef.current!, { childList: true });
return () => {
ob.disconnect();
};
}, [ref]);
- display: contents 的使用
为了确保 EmptyGhost 不会生成额外的 DOM 节点,我使用了 display: contents。这使得 EmptyGhost 的子元素直接参与布局,而不会被 empty-ghost 标签干扰。
React.useLayoutEffect(() => {
const styles = window.getComputedStyle(contentsRef.current!);
if (styles.display !== 'contents') {
console.warn(`[EmptyGhost]:`, contentsRef.current, `better an empty element`);
}
}, []);
总结
EmptyGhost 组件通过自定义元素与 React 的 forwardRef 实现了一种优雅的方式,让父组件能够引用真实 DOM 节点,同时避免了 React 严格模式下的警告。display: contents 确保组件不会生成多余的容器节点,保持了 DOM 的简洁性。
这个组件为在 React 项目中透明地操作 DOM 节点提供了灵活的解决方案,希望它能为你提供帮助和灵感。
最后附上npm链接 EmptyGhost