🚀 探索更多React Hooks的可能性?访问 www.reactuse.com 查看完整文档,通过 npm install @reactuses/core
快速安装,让你的React开发效率翻倍!
楔子
三年前,当我第一次接到"给这个卡片加个曝光埋点"的需求时,内心毫无波澜。不就是个IntersectionObserver吗?十分钟搞定。
现在回想起来,那时的我就像初入江湖的少年,以为手里握着一把剑就能闯荡天下,却不知道真正的高手从不亮剑。
最初的美好设想
那个阳光明媚的下午,我在白板上画出了理想中的API:
tsx
<Exposure traceId="user_card_exposure" traceData={{ userId: 123 }}>
<UserCard />
</Exposure>
"看,多优雅!"我对旁边的老李说。
老李只是笑了笑,那种看透一切却又不忍心戳破的笑。现在我明白了,那是前辈对后辈必经之路的宽容。
第一次撞墙:天真的包装器
年轻的我选择了最"显而易见"的方案------既然要监控可见性,那就包一层div呗:
tsx
const ExposureWrapper = (props: ExposureWrapperProps) => {
const { traceId, traceData, ...rest } = props;
const [io] = useState(
() => typeof IntersectionObserver === 'function'
? new IntersectionObserver(handleVisibilityChange)
: null,
);
const dom = useRef<HTMLDivElement>(null);
useEffect(() => {
const target = dom.current;
if (target) {
io?.observe(target);
}
return () => {
if (target) {
io?.unobserve(target);
}
io?.disconnect();
};
}, [io]);
function handleVisibilityChange(changes: IntersectionObserverEntry[]) {
const change = changes[0];
if (change.intersectionRatio > 0) {
const container = change.target;
io?.unobserve(container);
io?.disconnect();
if (traceId) {
iLog.logTrace(traceId, traceData);
}
}
}
return (
<div ref={dom} {...rest}>
{/* 子组件在这里 */}
</div>
);
};
代码写得行云流水,测试也通过了。我甚至还为自己的"效率"沾沾自喜。
然后,现实给了我当头一棒。
"你这个组件把我的布局搞坏了!"前端小王气冲冲地跑到我桌前。
"我的Flexbox对不齐了!"
"Grid布局全乱了!"
那一刻我才意识到,在CSS的世界里,每多一层DOM都可能是一场蝴蝶效应。那个我以为"无害"的div包装器,就像在精密的钟表里硬塞进了一颗螺丝钉。
顿悟:findDOMNode的救赎时刻
在一次痛苦的debug过程中,我翻遍了React文档,突然看到了findDOMNode
这个API。
"等等,这不就是我要的吗?"
那种感觉就像在黑暗中摸索许久,突然看到了一丝光明。
tsx
export class Impr extends Component<ExposureWrapperProps> {
public io: IntersectionObserver | null = null;
constructor(props) {
super(props);
this.io = typeof IntersectionObserver === 'function'
? new IntersectionObserver(this.handleVisibilityChange)
: null;
}
componentDidMount() {
// 这一行代码改变了一切
const target = findDOMNode(this) as HTMLElement;
if (target) {
this.io?.observe(target);
}
}
componentWillUnmount() {
const target = findDOMNode(this) as HTMLElement;
if (target) {
this.io?.unobserve(target);
}
this.io?.disconnect();
}
handleVisibilityChange = changes => {
const change = changes[0];
if (change.intersectionRatio > 0) {
const container = change.target;
this.io?.unobserve(container);
this.io?.disconnect();
if (this.props.traceId) {
iLog.logTrace(this.props.traceId, this.props.traceData);
}
}
};
render() {
const { children } = this.props;
return <>{children}</>;
}
}
重新发布后,世界安静了。没有布局抱怨,没有样式冲突,组件就像隐身了一样,默默地完成着自己的使命。
"这才叫专业!"老李拍了拍我的肩膀。
那一刻,我以为自己已经掌握了前端的精髓。
晴天霹雳:React 19的"背叛"
时间快进到今年,React 19的发布让我体验了什么叫"乐极生悲"。
"findDOMNode
has been removed from React 19"
看到这行字时,我的内心是崩溃的。就像一个剑客苦练十年的绝世剑法,突然被告知这套剑法已经失传。
React团队的解释很"官方":我们更推荐使用ref来访问DOM,findDOMNode
破坏了组件的封装性。
理论上,用ref确实更现代化:
tsx
// 理想很美好
function MyComponent() {
const ref = useRef();
useEffect(() => {
if (ref.current) {
io?.observe(ref.current);
}
}, []);
return <div ref={ref}>Content</div>;
}
但现实很骨感。当我们面对自定义组件时,ref就显得力不从心了:
tsx
// 这样是不行的,CustomComponent没有forwardRef
<Exposure>
<CustomComponent />
</Exposure>
即使我们尝试用Children.only
强行注入ref:
tsx
function VisibilityChange(props: any) {
const { children } = props;
const defaultRef = useRef();
const ref = children.ref || defaultRef;
// 各种复杂的ref处理逻辑...
return Children.only({ ...children, ref });
}
这种方法也有致命缺陷:它要求所有的子组件都支持ref,而现实中大量的第三方组件或老代码并不支持forwardRef
。
深夜的灵感:锚点定位法
在一个失眠的夜晚(程序员的灵感总是在深夜降临),我突然想到了一个疯狂的想法:既然不能直接获取子组件的DOM,那我就在它前面放个"路标"!
tsx
function FindDOMNodeReplacement({ children }) {
const [renderAnchor, setRenderAnchor] = useState(true);
const anchorRef = useRef();
const findDomNodeRef = useRef();
useEffect(() => {
if (anchorRef.current) {
// 神奇的一刻:通过兄弟节点找到目标
findDomNodeRef.current = anchorRef.current.nextElementSibling;
setRenderAnchor(false); // 任务完成,立即销毁证据
console.log('找到了!', findDomNodeRef.current);
}
}, []);
return (
<>
{renderAnchor && (
<span
ref={anchorRef}
style={{
position: 'absolute',
visibility: 'hidden',
pointerEvents: 'none',
width: 0,
height: 0,
overflow: 'hidden'
}}
/>
)}
{children}
</>
);
}
这个方案的巧妙之处让我自己都佩服:
- 插入一个完全不可见的锚点元素
- 利用DOM树的兄弟节点关系定位真正的目标
- 找到目标后立即移除锚点,不留任何痕迹
就像一个完美的间谍任务------潜入、定位、撤离,不留痕迹。
涅槃重生:最终的完美方案
将锚点定位技术融入曝光组件,我们得到了这个时代的终极方案:
tsx
function Exposure({ children, traceId, traceData }) {
const [renderAnchor, setRenderAnchor] = useState(true);
const [io] = useState(() =>
typeof IntersectionObserver === 'function'
? new IntersectionObserver(handleVisibilityChange)
: null
);
const anchorRef = useRef();
const targetRef = useRef();
useEffect(() => {
if (anchorRef.current) {
targetRef.current = anchorRef.current.nextElementSibling;
setRenderAnchor(false);
if (targetRef.current) {
io?.observe(targetRef.current);
}
}
}, [io]);
useEffect(() => {
return () => {
if (targetRef.current) {
io?.unobserve(targetRef.current);
}
io?.disconnect();
};
}, [io]);
function handleVisibilityChange(changes) {
const change = changes[0];
if (change.intersectionRatio > 0) {
io?.unobserve(change.target);
io?.disconnect();
if (traceId) {
iLog.logTrace(traceId, traceData);
}
}
}
return (
<>
{renderAnchor && (
<span
ref={anchorRef}
style={{
position: 'absolute',
visibility: 'hidden',
pointerEvents: 'none',
width: 0,
height: 0,
overflow: 'hidden'
}}
/>
)}
{children}
</>
);
}
写在最后的思考
这段技术演进的历程让我明白了几个道理:
没有完美的方案,只有适合当前场景的方案。 包装器方案虽然有缺陷,但在某些场景下可能是最直接的选择。
技术更新是双刃剑。 React团队移除findDOMNode
有其合理性,但也给现有项目带来了迁移成本。
创新往往诞生于绝望。 当常规路径被堵死时,我们反而能找到更有创意的解决方案。
真正的技术成长来自于解决真实的问题。 不是背会了多少API,而是在面对具体困难时,能够找到可行的解决路径。
现在,每当有新同事问我关于曝光埋点的实现时,我都会把这段经历分享给他们。不是为了炫耀技术,而是希望他们明白:技术的道路从来不是一帆风顺的,但正是这些挫折和突破,让我们成为了更好的工程师。
毕竟,代码会过时,框架会更新,但解决问题的思维方式是永恒的。