背景
上个月,我接手了一个多链DeFi聚合器前端项目的迭代。这个项目需要让用户能在同一个界面里,无缝操作他们在 Ethereum Mainnet、Polygon、Arbitrum 甚至 Optimism 上的资产。产品经理的原话是:"我们要让用户感觉像是在操作一个统一的账户,而不是在几个不同的区块链之间来回横跳。"
第一版的前端用的是比较原始的方案:自己用 ethers.js 写连接逻辑,然后手动管理不同网络的 Provider 和 Signer。代码里到处都是 if (chainId === 1) { ... } else if (chainId === 137) { ... },维护起来简直是噩梦。更头疼的是钱包切换网络时的状态同步问题,经常出现 UI 上显示的是 Polygon,但实际请求发到了 Ethereum 的 RPC 节点上。
所以这次重构,我的核心目标就是找一个能"优雅"处理多链连接和状态管理的库。社区里不少人推荐 RainbowKit + wagmi 的组合,号称是"React 生态下连接钱包的最佳实践"。我心想,这应该能省不少事吧?事实证明,我还是太天真了。
问题分析
我一开始的想法很简单:照着 RainbowKit 官方文档,安装、配置、把 <ConnectButton /> 一扔,不就完事了吗?文档里那个"Get Started in 5 minutes"的标语看着特别诱人。
我快速搭了个 demo:
bash
npm install @rainbow-me/rainbowkit wagmi viem
然后按照指南配置了 WagmiProvider 和 RainbowKitProvider,只加了一个 Ethereum Mainnet 的配置。跑起来一看,连接 MetaMask 确实没问题,按钮漂亮,弹窗也专业。
但问题马上就来了: 当我想让用户切换到 Polygon 网络时,我发现 RainbowKit 自带的网络切换器(就是那个点击连接按钮后下拉菜单里的"Switch Network"选项)里,只有我配置的 Ethereum。我明明需要支持多条链啊!
我第一反应是:"是不是配置里没加其他链?" 翻回文档仔细看,发现 wagmi 的 configureChains 函数需要传入一个链的数组。我原来只传了 [mainnet],于是赶紧加上了 polygon 和 arbitrum。
typescript
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
const { chains, publicClient, webSocketPublicClient } = configureChains(
[mainnet, polygon, arbitrum], // 这里!把需要的链都放进去
[publicProvider()]
);
改完重启,网络切换器里果然出现了三个网络选项。我兴致勃勃地点了"Polygon",MetaMask 弹窗提示我切换网络,我点了"批准"。然后......页面没有任何变化。钱包扩展显示网络已经切到了 Polygon,但我的应用 UI 上,当前网络仍然显示为"Ethereum",而且发起的 RPC 请求还是指向 Ethereum 的节点。
这就是我遇到的第一个核心问题:链的配置加进去了,但 wagmi 的客户端状态(chainId, account)与钱包的实际状态没有自动同步。 我需要手动去"监听"钱包的网络切换事件,并更新到 wagmi 的 store 里。这显然不是"开箱即用"该有的样子。
核心实现
第一步:配置正确的多链客户端
我意识到,光配置 chains 不够,还得让 wagmi 的客户端能正确响应钱包的网络切换。关键在于 createConfig 时传入的 publicClient 和 webSocketPublicClient 函数。这个函数会根据当前的 chainId 返回对应链的客户端。
这里有个坑:如果你用了像 publicProvider() 这样的公共提供商,它可能对某些链(特别是 Layer2 或测试网)的支持不稳定或速度慢。 为了更好的体验,最好为每条链配置一个可靠的 RPC 端点,比如来自 Infura 或 Alchemy 的。
我的改进方案是使用 jsonRpcProvider 来为每条链指定专属的 RPC URL。同时,我学会了要用 chain.id 作为判断条件,动态返回客户端。
typescript
import { configureChains, createConfig } from 'wagmi';
import { publicProvider } from 'wagmi/providers/public';
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
const { chains, publicClient, webSocketPublicClient } = configureChains(
[mainnet, polygon, arbitrum],
[
jsonRpcProvider({
rpc: (chain) => {
// 为每条链配置独立的 RPC URL
if (chain.id === mainnet.id) {
return { http: `https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY` };
}
if (chain.id === polygon.id) {
return { http: `https://polygon-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY` };
}
if (chain.id === arbitrum.id) {
return { http: `https://arb-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY` };
}
// 对于没有配置的链,回退到公共提供商(可选)
return null;
},
}),
// 可以加一个 publicProvider 作为后备,增加鲁棒性
publicProvider(),
]
);
const wagmiConfig = createConfig({
autoConnect: true, // 这个很重要,允许页面刷新后自动重连
publicClient,
webSocketPublicClient,
});
第二步:处理网络切换与状态同步
配置好了客户端,但网络切换后状态不同步的问题还在。经过一番搜索和阅读源码,我发现需要正确使用 wagmi 提供的 hooks 来获取和监听网络状态。
核心是 useAccount 和 useNetwork 这两个 hook。useAccount 可以拿到连接账户的地址和连接状态,而 useNetwork 能拿到当前激活的链(chain)以及一个 switchNetwork 函数。
这里注意一个细节: useNetwork 返回的 chain 对象,代表的是 wagmi 内部当前记录的活动链。它应该和钱包扩展里显示的链保持一致。如果不一致,说明状态同步有问题。
我写了一个自定义的 NetworkStatus 组件来展示和调试状态:
typescript
import { useAccount, useNetwork } from 'wagmi';
export function NetworkStatus() {
const { address, isConnected } = useAccount();
const { chain } = useNetwork();
return (
<div>
<p>连接状态: {isConnected ? '已连接' : '未连接'}</p>
<p>账户地址: {address}</p>
<p>当前网络: {chain?.name ?? '未知'}</p>
<p>链ID: {chain?.id ?? 'N/A'}</p>
</div>
);
}
把这个组件放到页面上后,我发现当我从钱包扩展里切换网络时,这个组件显示的网络信息会延迟几秒,但最终会更新到正确的链。这说明 wagmi 的监听机制是工作的,只是有延迟。对于用户体验来说,这个延迟可能有点长。
第三步:实现主动网络切换与错误处理
产品要求用户能在我们的应用内直接点击一个按钮就切换到目标网络,而不是非得去点 RainbowKit 连接按钮的下拉菜单。这就需要用到 useNetwork 返回的 switchNetwork 函数。
我封装了一个 ChainSwitcher 组件:
typescript
import { useNetwork } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
export function ChainSwitcher() {
const { chain, chains, switchNetwork } = useNetwork();
const handleSwitch = async (targetChainId: number) => {
if (!switchNetwork) {
console.error('switchNetwork 函数不可用');
return;
}
try {
await switchNetwork(targetChainId);
// 切换成功后的逻辑(比如显示一个成功提示)可以在这里处理
console.log(`已切换到链ID: ${targetChainId}`);
} catch (error) {
// 这里是个大坑!错误处理非常重要。
console.error('切换网络失败:', error);
// 常见的错误:用户拒绝了切换请求,或者钱包不支持该网络。
// 对于不支持的链,你可能需要提示用户手动添加网络。
}
};
return (
<div>
<p>当前网络: {chain?.name}</p>
<button onClick={() => handleSwitch(mainnet.id)} disabled={chain?.id === mainnet.id}>
切换到 Ethereum
</button>
<button onClick={() => handleSwitch(polygon.id)} disabled={chain?.id === polygon.id}>
切换到 Polygon
</button>
<button onClick={() => handleSwitch(arbitrum.id)} disabled={chain?.id === arbitrum.id}>
切换到 Arbitrum
</button>
</div>
);
}
这里有个至关重要的坑: switchNetwork 函数可能会失败,而且失败原因多种多样。最常见的是用户在小狐狸钱包弹窗里点了"拒绝"。但还有一种隐晦的情况:如果用户的钱包(比如某些移动端钱包)里根本没有配置你目标链的网络信息,switchNetwork 调用会静默失败或者抛出难以解析的错误。
为了解决"钱包未添加该网络"的问题,wagmi v1 有一个 addNetwork 动作,但在更现代的版本中,更常见的做法是准备好网络的添加参数,在 switchNetwork 失败时(尤其是捕捉到特定的错误码,如 4902),引导用户或自动调用钱包的 wallet_addEthereumChain RPC 方法。RainbowKit 和 wagmi 的内部逻辑其实已经处理了部分这种情况,但了解其原理对于调试很有帮助。
第四步:与 RainbowKit 组件深度集成
虽然我自己写了切换按钮,但 RainbowKit 自带的那个连接按钮和下拉菜单仍然是我的主要 UI。我需要确保它的行为符合预期。
RainbowKit 的 <ConnectButton /> 组件有一个 chainStatus 属性,可以控制链状态信息的显示方式。我发现在多链环境下,设置为 "icon" 或 "full" 体验更好,用户能一眼看到当前是哪个网络。
typescript
import { ConnectButton } from '@rainbow-me/rainbowkit';
function MyApp() {
return (
<div>
<ConnectButton chainStatus="icon" /> {/* 显示网络图标 */}
{/* 或者 */}
<ConnectButton chainStatus="full" /> {/* 显示网络名称 */}
</div>
);
}
另外,RainbowKit 的模态框(即点击连接按钮后弹出的那个窗口)里显示的可选网络列表,完全来源于你传给 RainbowKitProvider 的 chains 属性。这和我之前配置 wagmi 的 chains 是同一个数组,确保了一致性。
typescript
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css'; // 别忘了引入样式!
function App({ children }) {
return (
<WagmiProvider config={wagmiConfig}>
<RainbowKitProvider chains={chains}> {/* 就是这里! */}
{children}
</RainbowKitProvider>
</WagmiProvider>
);
}
完整代码
下面是一个整合了以上所有要点的、可运行的 App.tsx 示例:
typescript
import React from 'react';
import { configureChains, createConfig, WagmiProvider, useAccount, useNetwork } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { publicProvider } from 'wagmi/providers/public';
import { jsonRpcProvider } from 'wagmi/providers/jsonRpc';
import { RainbowKitProvider, ConnectButton, lightTheme } from '@rainbow-me/rainbowkit';
import '@rainbow-me/rainbowkit/styles.css';
// 1. 配置多链和提供商
const { chains, publicClient, webSocketPublicClient } = configureChains(
[mainnet, polygon, arbitrum],
[
jsonRpcProvider({
rpc: (chain) => {
// 替换为你自己的 RPC 端点,这里用公共端点示例
if (chain.id === mainnet.id) return { http: 'https://ethereum.publicnode.com' };
if (chain.id === polygon.id) return { http: 'https://polygon-rpc.com' };
if (chain.id === arbitrum.id) return { http: 'https://arb1.arbitrum.io/rpc' };
return null;
},
}),
publicProvider(),
]
);
// 2. 创建 wagmi 配置
const wagmiConfig = createConfig({
autoConnect: true,
publicClient,
webSocketPublicClient,
});
// 3. 自定义网络状态显示组件
function NetworkStatus() {
const { address, isConnected } = useAccount();
const { chain } = useNetwork();
return (
<div style={{ border: '1px solid #ccc', padding: '1rem', margin: '1rem 0' }}>
<h3>网络状态</h3>
<p>连接: {isConnected ? '是' : '否'}</p>
<p>地址: {address ? `${address.slice(0, 6)}...${address.slice(-4)}` : '无'}</p>
<p>网络: {chain?.name || '未知'}</p>
<p>链ID: {chain?.id || 'N/A'}</p>
</div>
);
}
// 4. 自定义链切换器组件
function ChainSwitcher() {
const { chain, switchNetwork } = useNetwork();
const targetChains = [mainnet, polygon, arbitrum];
return (
<div style={{ margin: '1rem 0' }}>
<h3>快速切换网络</h3>
{targetChains.map((targetChain) => (
<button
key={targetChain.id}
onClick={() => switchNetwork?.(targetChain.id)}
disabled={!switchNetwork || chain?.id === targetChain.id}
style={{ marginRight: '0.5rem', padding: '0.5rem 1rem' }}
>
切换到 {targetChain.name}
</button>
))}
{!switchNetwork && <p style={{ color: 'orange' }}>请先连接钱包以切换网络</p>}
</div>
);
}
// 5. 主应用组件
function AppContent() {
return (
<div style={{ maxWidth: '800px', margin: '2rem auto', fontFamily: 'sans-serif' }}>
<h1>RainbowKit 多链集成 Demo</h1>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
<h2>钱包连接</h2>
<ConnectButton chainStatus="full" />
</div>
<NetworkStatus />
<ChainSwitcher />
<div style={{ marginTop: '2rem', padding: '1rem', backgroundColor: '#f5f5f5' }}>
<h3>功能说明</h3>
<ul>
<li>点击右上角按钮连接钱包(支持 MetaMask, Coinbase Wallet 等)。</li>
<li>连接后,"网络状态"区域会显示地址和当前网络。</li>
<li>使用"快速切换网络"按钮或 RainbowKit 按钮下拉菜单中的"Switch Network"来切换链。</li>
<li>切换时请留意钱包扩展的确认弹窗。</li>
</ul>
</div>
</div>
);
}
// 6. 根组件,注入 Providers
export default function App() {
return (
<WagmiProvider config={wagmiConfig}>
<RainbowKitProvider
chains={chains}
theme={lightTheme({
accentColor: '#3B82F6',
borderRadius: 'medium',
})}
>
<AppContent />
</RainbowKitProvider>
</WagmiProvider>
);
}
踩坑记录
switchNetwork静默失败(用户拒绝) :最初我的切换按钮点击后,如果用户在钱包弹窗里点了拒绝,页面没有任何反馈。这非常糟糕。解决方法 :用try...catch包裹switchNetwork调用,并在 catch 块中给用户友好的提示(例如使用 toast 通知)。- RPC 端点不可用或限速 :一开始用了免费的公共 RPC,在 Polygon 上经常请求失败或超时,导致余额查询等功能出错。解决方法:申请 Alchemy 或 Infura 的免费层服务,为每条链配置专属的 RPC URL,稳定性和速度大幅提升。
- 页面刷新后连接状态丢失 :虽然设置了
autoConnect: true,但有时刷新后钱包还是显示未连接。排查发现 :autoConnect依赖于钱包提供商和本地存储的会话缓存,有些钱包(特别是移动端钱包应用)可能不支持或行为不一致。缓解方案:在 UI 上做好"未连接"状态的引导,始终显示连接按钮。 - RainbowKit 主题与项目样式冲突 :RainbowKit 的模态框样式有时会被项目全局 CSS 覆盖,导致布局错乱。解决方法 :确保
import '@rainbow-me/rainbowkit/styles.css';语句放在项目全局样式之后,或者使用 CSS-in-JS 方案提高样式优先级。也可以直接使用theme属性深度定制,覆盖其 CSS 变量。
小结
这次折腾让我明白,RainbowKit 提供的确实是"快速入门"的便利,但真要实现生产级可用的多链体验,必须深入理解其底层依赖 wagmi 的状态管理机制,并亲手处理好网络切换的边界情况和错误。现在,我的聚合器前端终于有了一个稳定、用户友好的多链钱包连接底座,我可以把更多精力放在业务逻辑上了。下一步,我打算深入研究 wagmi 的 useContractRead 和 useContractWrite 如何与多链场景结合,实现真正的跨链合约调用。