Umbraco+ReactJS.net SSR前端路由切换后嵌套React组件水化问题求助
这问题我之前帮几个做Umbraco+React集成的开发者解决过,核心就是前端路由切换后,动态加载的HTML里的React组件没触发React.NET的初始化——毕竟@Html.ReactInitJavaScript()只在服务器初始渲染时跑一次。给你几个可行的方案,按复杂度和可控性排序:
方案1:让SurfaceController返回初始化所需的完整数据(最直接)
当你调用SurfaceController获取节点网格HTML时,别只返回纯HTML,改成返回包含html和reactInitScripts的JSON对象,复用React.NET的初始化机制。
在SurfaceController里这么处理:
public ActionResult GetPageContent(int nodeId) { var node = Umbraco.Content(nodeId); var gridHtml = node.GetGridHtml("content"); // 假设你的网格属性叫content // 生成React初始化脚本,确保拿到当前请求上下文 var reactInit = Html.ReactInitJavaScript().ToString(); return Json(new { Html = gridHtml.ToString(), ReactInitScript = reactInit }, JsonRequestBehavior.AllowGet); }
前端获取数据后,先插入HTML,再动态执行初始化脚本:
// Content组件里的获取数据方法 const fetchPageContent = async (path) => { const response = await fetch(`/Surface/PageContent/GetPageContent?nodeId=${getNodeId(path)}`); const data = await response.json(); // 插入网格HTML setContentHtml(data.Html); // 动态执行初始化脚本,执行完移除避免重复 const script = document.createElement('script'); script.textContent = data.ReactInitScript; document.body.appendChild(script); script.remove(); };
注意:可以给每个页面的初始化脚本加唯一ID,避免重复执行相同脚本。
方案2:前端提前注册组件,手动触发水化(更可控)
如果不想依赖服务器返回初始化脚本,你可以在前端提前注册所有Umbraco网格用到的自定义组件,加载HTML后手动遍历节点完成水化。
首先在前端全局注册组件:
// 比如在app.js里全局注册 import MyCustomGridComponent from './components/MyCustomGridComponent'; window.ReactComponents = window.ReactComponents || {}; window.ReactComponents.MyCustomGridComponent = MyCustomGridComponent;
然后在加载完HTML后,手动处理未水化的组件:
const hydrateDynamicComponents = () => { // 找到所有未标记水化的React组件节点(React.NET会自动加data-react-component属性) const reactNodes = document.querySelectorAll('[data-react-component]:not([data-react-hydrated])'); reactNodes.forEach(node => { const componentName = node.getAttribute('data-react-component'); const props = JSON.parse(node.getAttribute('data-react-props')); const Component = window.ReactComponents[componentName]; if (Component) { ReactDOM.hydrate(<Component {...props} />, node); // 标记已水化,避免重复处理 node.setAttribute('data-react-hydrated', 'true'); } }); }; // 在contentHtml更新后调用水化方法 useEffect(() => { if (contentHtml) { hydrateDynamicComponents(); } }, [contentHtml]);
这个方案完全由前端控制水化过程,适合组件数量固定的场景。
方案3:重构数据返回,避免dangerouslySetInnerHTML(最佳实践)
其实用dangerouslySetInnerHTML加载带React组件的HTML本身就有点绕,更好的方式是让SurfaceController返回网格的结构化数据,前端直接渲染组件。
在Umbraco里把网格数据转成前端能理解的结构:
public ActionResult GetPageContent(int nodeId) { var node = Umbraco.Content(nodeId); var gridModel = node.GetPropertyValue<GridModel>("content"); // 把GridModel转成前端可用的结构化数据 var structuredData = ConvertGridToStructuredData(gridModel); return Json(structuredData, JsonRequestBehavior.AllowGet); } // 自定义转换方法,区分React组件和普通HTML private object ConvertGridToStructuredData(GridModel grid) { return grid.Sections.Select(section => new { section.Grid, Rows = section.Rows.Select(row => new { row.Grid, Areas = row.Areas.Select(area => new { area.Grid, Widgets = area.Widgets.Select(widget => { // 判断是不是自定义React网格编辑器 if (widget.Editor.Alias == "myReactGridEditor") { return new { Type = "ReactComponent", ComponentName = "MyCustomGridComponent", Props = widget.Value // 假设编辑器Value就是组件props }; } else { // 普通HTML编辑器返回渲染后的内容 return new { Type = "Html", Content = widget.GetValueAsString() }; } }) }) }) }); }
前端Content组件直接根据结构化数据渲染:
const Content = ({ structuredData }) => { return ( <div className="grid-container"> {structuredData.map(section => ( <div key={section.Grid} className="grid-section"> {section.Rows.map(row => ( <div key={row.Grid} className="grid-row"> {row.Areas.map(area => ( <div key={area.Grid} className="grid-area"> {area.Widgets.map(widget => { if (widget.Type === "ReactComponent") { const Component = window.ReactComponents[widget.ComponentName]; return <Component key={widget.Id} {...widget.Props} />; } else { return <div dangerouslySetInnerHTML={{ __html: widget.Content }} />; } })} </div> ))} </div> ))} </div> ))} </div> ); };
这个方案彻底解决了水化问题,所有React组件都是前端直接渲染的,更符合React开发模式,也更安全。
总结一下:临时解决用方案1,追求可控性选方案2,长期维护优先方案3。
内容的提问来源于stack exchange,提问作者Charlie Anstey

