牧野听埙 發表於 2025-7-26 02:40:00

微前端实践:如何让子项目脱离 React Router,优雅支持主项目的 Next.js 路由系统?

<p data-start="107" data-end="135">在微前端、多模块系统或模块联邦架构中,常见的一个问题是:</p>
<blockquote data-start="137" data-end="211">
<p data-start="139" data-end="211"><strong data-start="139" data-end="211">子项目是 Vite + React + React Router 构建的组件库,主项目是 Next.js,如何处理 Link 路由跳转?</strong></p>
</blockquote>
<p data-start="213" data-end="274">如果子项目中大量使用了 React Router 的 <code data-start="240" data-end="248">&lt;Link&gt;</code> 组件,部署到 Next.js 主项目中时就会报错:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;
<div class="cnblogs_code">
<pre>Uncaught TypeError: Cannot destructure property 'basename' of 'React.useContext(...)' as it is <span style="color: rgba(0, 0, 255, 1)">null</span>.</pre>
</div>
<p>&nbsp;</p>
</div>
<div class="overflow-y-auto p-4" dir="ltr">这是因为子项目依赖了 <code data-start="397" data-end="412">BrowserRouter</code> 上下文,而主项目并没有提供这个 context。</div>
</div>
<p data-start="439" data-end="517">那该怎么办?本文将讲清楚:<strong data-start="452" data-end="517">如何将 <code data-start="458" data-end="466">&lt;Link&gt;</code> 抽象为“注入式组件”,实现路由系统解耦,达到“UI 组件独立 + 路由行为由主项目控制”的目的。</strong></p>
<hr data-start="519" data-end="522">
<h2 data-start="524" data-end="559">❌ 错误做法:子项目自行包裹 <code data-start="542" data-end="559">&lt;BrowserRouter&gt;</code></h2>
<p data-start="561" data-end="576">许多开发者尝试在子项目中包裹:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr"><code class="whitespace-pre! language-tsx">&lt;<span class="hljs-title class_">BrowserRouter&gt; <span class="language-xml"><span class="hljs-tag">&lt;<span class="hljs-name">App /&gt; &lt;/<span class="hljs-title class_">BrowserRouter&gt; </span></span></span></span></span></code></div>
</div>
<p data-start="633" data-end="686">这样虽然可以让 <code data-start="641" data-end="649">&lt;Link&gt;</code> 不报错,但在主项目(尤其是 SSR 的 Next.js)中引入后会产生:</p>
<ul data-start="688" data-end="779">
<li data-start="688" data-end="707">
<p data-start="690" data-end="707">✅ 不可控的 History 实例</p>
</li>
<li data-start="708" data-end="722">
<p data-start="710" data-end="722">❌ 无法与主项目共享路由</p>
</li>
<li data-start="723" data-end="734">
<p data-start="725" data-end="734">❌ 地址栏行为混乱</p>
</li>
<li data-start="735" data-end="779">
<p data-start="737" data-end="779">❌ SSR Hydration mismatch(首次渲染时 HTML 内容对不上)</p>
</li>
</ul>
<hr data-start="781" data-end="784">
<h2 data-start="786" data-end="817">✅ 正确做法:将 <code data-start="798" data-end="806">&lt;Link&gt;</code> 抽象为“注入式组件”</h2>
<h3 data-start="819" data-end="830">🎯 核心理念</h3>
<p data-start="832" data-end="910">子项目不再直接使用 <code data-start="842" data-end="850">&lt;Link&gt;</code>,而是通过 <code data-start="856" data-end="877">props.linkComponent</code> 或 Context 注入方式,<strong data-start="893" data-end="909">将导航行为交由主项目决定</strong>。</p>
<hr data-start="912" data-end="915">
<h2 data-start="917" data-end="945">🔧 Step by Step:组件注入式解耦方案</h2>
<h3 data-start="947" data-end="979">✅ 第一步:定义 linkComponent props</h3>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>type LinkProps = { href: string; children: React.ReactNode; className?: string; onClick?: () =&gt; <span style="color: rgba(0, 0, 255, 1)">void</span>; }; type MyCardProps = { linkComponent: (props: LinkProps) =&gt; JSX.Element; };</pre>
</div>
<p>&nbsp;</p>
</div>
</div>
<hr data-start="1182" data-end="1185">
<h3 data-start="1187" data-end="1222">✅ 第二步:子项目组件中使用注入的 linkComponent</h3>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>export <span style="color: rgba(0, 0, 255, 1)">function</span> MyCard({ linkComponent }: MyCardProps) { const Link = linkComponent; <span style="color: rgba(0, 0, 255, 1)">return</span> ( &lt;Link href="/product/123" className="text-blue-600 hover:underline"&gt; 查看商品详情 &lt;/Link&gt; ); }</pre>
</div>
<p>&nbsp;</p>
</div>
</div>
<hr data-start="1440" data-end="1443">
<h3 data-start="1445" data-end="1466">✅ 第三步:在主项目中注入具体实现</h3>
<h4 data-start="1468" data-end="1495">🟦 在 React Router 项目中:</h4>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>import { Link as RouterLink } from 'react-router-dom'; &lt;MyCard linkComponent={({ href, ...rest }) =&gt; &lt;RouterLink to={href} {...rest} /&gt;} /&gt;</pre>
</div>
<p>&nbsp;</p>
<span style="font-size: 1em">🟪 在 Next.js 中:</span></div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>import NextLink from 'next/link'; &lt;MyCard linkComponent={({ href, children, ...rest }) =&gt; ( &lt;NextLink href={href} {...rest}&gt; {children} &lt;/NextLink&gt; )} /&gt;</pre>
</div>
<p>&nbsp;</p>
<h3 data-start="1445" data-end="1466">✅ 第四步:加入BrowserRouter</h3>
<p data-start="158" data-end="181">✅ 最小本地调试示例(Vite 子项目)</p>
<p data-start="183" data-end="214">🔧 <code data-start="190" data-end="200">main.tsx</code>(或 <code data-start="203" data-end="213">main.jsx</code>)</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>import React from 'react'<span style="color: rgba(0, 0, 0, 1)">;
import ReactDOM from </span>'react-dom/client'<span style="color: rgba(0, 0, 0, 1)">;
import { BrowserRouter, Link as RouterLink } from </span>'react-router-dom'<span style="color: rgba(0, 0, 0, 1)">;
import MyCard from </span>'./MyCard'; <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 你的组件文件 </span>
<span style="color: rgba(0, 0, 0, 1)">
const App </span>= () =&gt;<span style="color: rgba(0, 0, 0, 1)">

(</span>&lt;MyCard linkComponent={({ href, children, ...rest }) =&gt; (&lt;RouterLink to={href} {...rest}&gt; {children} &lt;/RouterLink&gt;)} /&gt;<span style="color: rgba(0, 0, 0, 1)">);

ReactDOM.createRoot(document.getElementById(</span>'root')!).render(&lt;BrowserRouter&gt; &lt;App /&gt; &lt;/BrowserRouter&gt;);</pre>
</div>
<p>&nbsp;</p>
</div>
<div class="overflow-y-auto p-4" dir="ltr">&nbsp;</div>
</div>
</div>
</div>
<hr data-start="1859" data-end="1862">
<h2 data-start="1864" data-end="1900">🚀 Bonus:抽象整个 Routing Context(进阶)</h2>
<p data-start="1902" data-end="1945">如果子项目中还用到了 <code data-start="1913" data-end="1928">useNavigate()</code> 等跳转逻辑,我们可以统一抽象为:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>type RoutingContextValue = { linkComponent: (props: LinkProps) =&gt; JSX.Element; push: (path: string) =&gt; <span style="color: rgba(0, 0, 255, 1)">void</span>; }; const RoutingContext = React.createContext&lt;RoutingContextValue | <span style="color: rgba(0, 0, 255, 1)">null</span>&gt;(<span style="color: rgba(0, 0, 255, 1)">null</span>); export const useRouting = () =&gt; { const ctx = useContext(RoutingContext); <span style="color: rgba(0, 0, 255, 1)">if</span> (!ctx) <span style="color: rgba(0, 0, 255, 1)">throw</span> <span style="color: rgba(0, 0, 255, 1)">new</span> Error('RoutingContext not provided'); <span style="color: rgba(0, 0, 255, 1)">return</span> ctx; };</pre>
</div>
<p>&nbsp;</p>
</div>
</div>
<p data-start="2308" data-end="2319">然后子项目中统一使用:</p>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>const { push, linkComponent: Link } = useRouting(); <span style="color: rgba(0, 0, 255, 1)">return</span> ( &lt;Link href="/profile"&gt;我的主页&lt;/Link&gt; ); // 或跳转 &lt;button onClick={() =&gt; push('/checkout')}&gt;结账&lt;/button&gt;</pre>
</div>
<p>&nbsp;</p>
主项目注入:</div>
</div>
<div class="contain-inline-size rounded-2xl relative bg-token-sidebar-surface-primary">
<div class="flex items-center text-token-text-secondary px-4 py-2 text-xs font-sans justify-between h-9 bg-token-sidebar-surface-primary select-none rounded-t-2xl">&nbsp;</div>
<div class="overflow-y-auto p-4" dir="ltr">
<div class="cnblogs_code">
<pre>&lt;RoutingContext.Provider value={{ push: (url) =&gt; router.push(url), linkComponent: ({ href, ...rest }) =&gt; &lt;NextLink href={href} {...rest} /&gt;, }} &gt; &lt;SubApp /&gt; &lt;/RoutingContext.Provider&gt;</pre>
</div>
<p>&nbsp;</p>
</div>
</div>
<hr data-start="2714" data-end="2717">
<h2 data-start="2719" data-end="2726">✅ 总结</h2>
<div class="_tableContainer_80l1q_1">
<div class="_tableWrapper_80l1q_14 group flex w-fit flex-col-reverse">
<table class="w-fit min-w-(--thread-content-width)" data-start="2728" data-end="2966">
<thead data-start="2728" data-end="2746">
<tr data-start="2728" data-end="2746"><th data-start="2728" data-end="2733" data-col-size="sm">方案</th><th data-start="2733" data-end="2740" data-col-size="sm">是否推荐</th><th data-start="2740" data-end="2746" data-col-size="sm">说明</th></tr>
</thead>
<tbody data-start="2775" data-end="2966">
<tr data-start="2775" data-end="2821">
<td data-start="2775" data-end="2796" data-col-size="sm">子项目包 BrowserRouter</td>
<td data-col-size="sm" data-start="2796" data-end="2800">❌</td>
<td data-col-size="sm" data-start="2800" data-end="2821">会和 Next.js 路由系统冲突</td>
</tr>
<tr data-start="2822" data-end="2863">
<td data-start="2822" data-end="2842" data-col-size="sm">主项目包 MemoryRouter</td>
<td data-start="2842" data-end="2847" data-col-size="sm">⚠️</td>
<td data-start="2847" data-end="2863" data-col-size="sm">勉强可行,但无地址栏联动</td>
</tr>
<tr data-start="2864" data-end="2904">
<td data-start="2864" data-end="2886" data-col-size="sm">抽象 linkComponent 注入</td>
<td data-start="2886" data-end="2890" data-col-size="sm">✅</td>
<td data-start="2890" data-end="2904" data-col-size="sm">通用、安全、强扩展性</td>
</tr>
<tr data-start="2905" data-end="2966">
<td data-start="2905" data-end="2930" data-col-size="sm">使用 RoutingContext 管理跳转</td>
<td data-start="2930" data-end="2935" data-col-size="sm">✅✅</td>
<td data-start="2935" data-end="2966" data-col-size="sm">推荐:支持 <code data-start="2943" data-end="2949">Link</code> 和 <code data-start="2952" data-end="2960">push()</code> 双能力</td>
</tr>
</tbody>
</table>
<div class="sticky end-(--thread-content-margin) h-0 self-end select-none">&nbsp;</div>
</div>
</div>
<hr data-start="2968" data-end="2971">
<h2 data-start="2973" data-end="2983">🧠 适用场景</h2>
<ul data-start="2985" data-end="3063">
<li data-start="2985" data-end="2992">
<p data-start="2987" data-end="2992">多项目整合</p>
</li>
<li data-start="2993" data-end="3000">
<p data-start="2995" data-end="3000">微前端架构</p>
</li>
<li data-start="3001" data-end="3026">
<p data-start="3003" data-end="3026">模块联邦(Module Federation)</p>
</li>
<li data-start="3027" data-end="3063">
<p data-start="3029" data-end="3063">任意非同源路由系统整合(如 Vue 主项目 + React 子项目)</p>
</li>
</ul>
<hr data-start="3065" data-end="3068">
<h2 data-start="3070" data-end="3080">🧩 最终收益</h2>
<ul data-start="3082" data-end="3188">
<li data-start="3082" data-end="3111">
<p data-start="3084" data-end="3111">子项目可独立运行(本地包 BrowserRouter)</p>
</li>
<li data-start="3112" data-end="3144">
<p data-start="3114" data-end="3144">子项目可被 Next.js / Vue / 原生项目安全嵌入</p>
</li>
<li data-start="3145" data-end="3164">
<p data-start="3147" data-end="3164">组件逻辑纯粹,UI 与行为彻底解耦</p>
</li>
<li data-start="3165" data-end="3188">
<p data-start="3167" data-end="3188">未来支持 SSR、CSR、SPA 多种模式</p>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/sabertobih/p/19005687
頁: [1]
查看完整版本: 微前端实践:如何让子项目脱离 React Router,优雅支持主项目的 Next.js 路由系统?