"use client"; import { useEffect, useLayoutEffect, useRef } from "react"; import type { SidebarWidgetPublic, User } from "@/lib/api"; import { mountWidget } from "@/lib/widgetRuntime"; import { ensureWidgetApi } from "@/lib/widgetApi"; // 站长自定义工具块:所有工具统一在 Shadow DOM 内渲染(attachShadow 是客户端能力, // 因此服务端只输出空容器,HTML/CSS/JS 全部在客户端 effect 中注入,无水合不一致)。 // - CSS 注入 shadow 内,不污染全站,也不被全站样式污染; // - JS 以函数形式执行,接收 (root, shadow, toolId, registerCleanup), // DOM 查找从 root 出发,同页多个同款工具互不干扰; // - unmount 时统一执行 cleanup 并清空 shadow。 // 站长可信内容,不做消毒/沙箱。 export default function CustomWidgetBlock({ widget, viewer, }: { widget: SidebarWidgetPublic; viewer?: User | null; }) { const hostRef = useRef(null); // 服务端用 useEffect 避免告警,客户端用 layout effect 首帧前完成注入,避免闪烁 const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect; // 两个 effect 同类型、按声明顺序执行:先建立站点接口,再挂载工具, // 保证用户脚本首帧同步读取 window.J13 时接口已就绪。 useIsomorphicLayoutEffect(() => { ensureWidgetApi(viewer); }, [viewer]); useIsomorphicLayoutEffect(() => { const host = hostRef.current; if (!host) return; const handle = mountWidget(host, { id: widget.id, html: widget.html, css: widget.css, js: widget.js, }); return () => handle.unmount(); }, [widget.id, widget.html, widget.css, widget.js]); return (
); }