背景
上个月,我接手了一个多链 NFT 交易平台的前端重构工作。这个平台需要同时支持以太坊主网、Polygon 和 Arbitrum 三条链上的 NFT 交易。原来的实现用了 ethers.js 直接连接 MetaMask,代码里到处都是 window.ethereum.request({ method: 'eth_requestAccounts' }) 这种硬编码,而且多链切换的逻辑写得特别乱——用户切链得手动去 MetaMask 里点,体验很差。
产品经理提了个需求:要像 Uniswap 那样,用户点一下就能切换网络,而且最好能支持更多钱包,比如 Coinbase Wallet、WalletConnect 等。我调研了一圈,发现 RainbowKit 配合 wagmi 是个不错的方案,它封装了钱包连接、网络切换、链状态管理这些繁琐的逻辑,还提供了漂亮的 UI 组件。于是,我决定用 RainbowKit 来重构钱包连接模块。
问题分析
一开始我以为集成 RainbowKit 就是装个包、写几行配置的事。按照官方文档,我快速搭了个 demo:
npm install @rainbow-me/rainbowkit wagmi viem
然后照搬了示例代码:
import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit';
import { configureChains, createConfig, WagmiConfig } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { publicProvider } from 'wagmi/providers/public';
const { chains, publicClient } = configureChains(
[mainnet, polygon, arbitrum],
[publicProvider()]
);
const { connectors } = getDefaultWallets({
appName: 'My NFT Platform',
projectId: 'YOUR_PROJECT_ID',
chains
});
const wagmiConfig = createConfig({
autoConnect: true,
connectors,
publicClient
});
function App() {
return (
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider chains={chains}>
{/* 你的应用 */}
</RainbowKitProvider>
</WagmiConfig>
);
}
跑起来一看,连接按钮是出来了,也能弹出钱包选择框。但马上遇到了第一个问题:连接上之后,UI 显示的是“Ethereum”,而不是“Ethereum Mainnet”。产品说这不够专业,得显示完整的网络名称。而且,我们平台的主推链是 Polygon,希望默认选中 Polygon 而不是以太坊。
更麻烦的是,当用户钱包不在我们支持的链上时(比如他在 BSC 上),RainbowKit 默认只是显示一个“Wrong Network”的提示,用户得点好几下才能切换到我们支持的链。这体验肯定不行,我们需要一个“一键切换”的功能。
核心实现
1. 自定义配置与默认链
首先解决默认链和显示名称的问题。我翻看了 RainbowKit 和 wagmi 的文档,发现 configureChains 时可以对链进行自定义。
这里有个坑:wagmi/chains 里导出的链对象,其 name 属性就是显示在 UI 上的文本。但直接修改这个对象可能影响其他地方的使用。更好的做法是创建链的副本。
import { Chain } from 'wagmi';
// 自定义链配置,主要是为了控制UI显示
const supportedChains: Chain[] = [
{
...mainnet,
name: 'Ethereum Mainnet' // 覆盖默认的'Ethereum'
},
{
...polygon,
name: 'Polygon Mainnet'
},
{
...arbitrum,
name: 'Arbitrum One'
}
];
// 设置平台默认链(比如我们主推Polygon)
const defaultChain = polygon;
const { chains, publicClient } = configureChains(
supportedChains,
[publicProvider()]
);
// 在RainbowKitProvider中指定初始链
<RainbowKitProvider
chains={chains}
initialChain={defaultChain}
>
这样配置后,UI 显示就符合要求了,而且用户打开页面时,RainbowKit 会优先使用 Polygon 链。
2. 处理错误网络与一键切换
接下来要解决用户连接时钱包处于错误网络的问题。RainbowKit 提供了 Chain 组件来显示网络切换提示,但我想做得更友好一些:当检测到用户在不支持的链上时,直接弹出一个模态框,让他一键切换到我们指定的链(比如 Polygon)。
注意这个细节:直接调用 switchChain 可能会被钱包拒绝(比如 MetaMask 会弹出确认框),所以需要处理用户拒绝的情况。
我创建了一个自定义的 NetworkModal 组件:
import { useSwitchChain } from 'wagmi';
import { useEffect, useState } from 'react';
function NetworkModal() {
const { chain, chains } = useAccount();
const { switchChain } = useSwitchChain();
const [showModal, setShowModal] = useState(false);
// 检查当前链是否支持
const isUnsupported = chain && !chains.some(c => c.id === chain.id);
useEffect(() => {
// 如果是不支持的链,显示模态框
setShowModal(isUnsupported);
}, [isUnsupported]);
const handleSwitchToPolygon = async () => {
try {
await switchChain({ chainId: polygon.id });
setShowModal(false);
} catch (error) {
console.error('用户拒绝切换网络:', error);
// 可以在这里给用户一个提示
}
};
if (!showModal) return null;
return (
<div className="network-modal-overlay">
<div className="network-modal">
<h3>不支持的网络</h3>
<p>您的钱包当前连接的是 {chain?.name},请切换到以下支持的网络:</p>
<button onClick={handleSwitchToPolygon}>
切换到 Polygon Mainnet
</button>
<p className="hint">
如果切换失败,请手动在钱包中切换网络
</p>
</div>
</div>
);
}
然后在应用里使用这个组件。这样当用户在不支持的链上时,会看到一个友好的提示,而不是 RainbowKit 默认的那个小标签。
3. 自定义连接按钮与主题
RainbowKit 默认的连接按钮样式和我们的设计系统不太搭。我需要自定义主题,并且把连接按钮集成到我们的导航栏里。
RainbowKit 提供了 ConnectButton 组件和 Theme 配置:
import { ConnectButton, Theme } from '@rainbow-me/rainbowkit';
import { darkTheme } from '@rainbow-me/rainbowkit';
// 自定义主题,匹配我们的设计系统
const customTheme: Theme = {
...darkTheme(), // 基于暗色主题扩展
colors: {
...darkTheme().colors,
accentColor: '#8B5CF6', // 主色调改为紫色
accentColorForeground: '#FFFFFF',
connectButtonBackground: '#1F2937',
connectButtonBackgroundError: '#EF4444',
connectButtonText: '#FFFFFF',
connectButtonTextError: '#FFFFFF'
},
radii: {
...darkTheme().radii,
connectButton: '12px' // 更大的圆角
},
fonts: {
...darkTheme().fonts,
body: 'Inter, sans-serif' // 使用我们的字体
}
};
// 在RainbowKitProvider中使用自定义主题
<RainbowKitProvider
chains={chains}
theme={customTheme}
appInfo={{
appName: 'NFT Trading Platform',
learnMoreUrl: 'https://docs.ourplatform.com'
}}
>
{/* 导航栏中的连接按钮 */}
<nav className="navbar">
<div className="logo">NFT Platform</div>
<div className="nav-links">
{/* 其他导航链接 */}
</div>
<div className="wallet-connect">
<ConnectButton
showBalance={false} // 我们不显示余额
accountStatus="address" // 只显示地址,不显示ENS名
chainStatus="icon" // 只显示链图标,不显示名称
label="连接钱包"
/>
</div>
</nav>
</RainbowKitProvider>
这里有个坑:ConnectButton 的 accountStatus 和 chainStatus 属性需要根据设计稿仔细调整。如果设计稿里空间有限,可能只能显示图标;如果有足够空间,可以显示完整的地址和链名。
4. 钱包连接状态与业务逻辑集成
钱包连接状态需要和我们的业务逻辑联动。比如,当用户断开钱包连接时,我们需要清空用户数据;当用户切换链时,我们需要更新合约实例。
我创建了一个自定义 hook 来管理这些副作用:
import { useAccount, useDisconnect } from 'wagmi';
import { useEffect } from 'react';
import { useUserStore } from '../stores/userStore';
import { useContractStore } from '../stores/contractStore';
export function useWalletIntegration() {
const { address, chainId, isConnected } = useAccount();
const { disconnect } = useDisconnect();
const { setUser, clearUser } = useUserStore();
const { updateContract, clearContract } = useContractStore();
// 监听连接状态变化
useEffect(() => {
if (isConnected && address && chainId) {
// 用户连接成功,设置用户信息
setUser({ address, chainId });
// 根据链ID更新合约实例
updateContract(chainId);
// 可以在这里发送连接事件到分析平台
console.log('用户连接:', { address, chainId });
} else {
// 用户断开连接,清理状态
clearUser();
clearContract();
console.log('用户断开连接');
}
}, [isConnected, address, chainId]);
// 提供一个手动断开连接的方法,可以添加确认对话框
const handleDisconnect = () => {
if (window.confirm('确定要断开钱包连接吗?')) {
disconnect();
}
};
return {
isConnected,
address,
chainId,
handleDisconnect
};
}
然后在需要钱包状态的组件中使用这个 hook:
function UserProfile() {
const { isConnected, address, handleDisconnect } = useWalletIntegration();
if (!isConnected) {
return <p>请先连接钱包</p>;
}
return (
<div className="user-profile">
<p>已连接: {address?.slice(0, 6)}...{address?.slice(-4)}</p>
<button onClick={handleDisconnect}>断开连接</button>
</div>
);
}
这样,钱包状态的变化就能自动同步到我们的业务状态了。
完整代码
下面是一个完整的、可运行的示例,展示了如何集成 RainbowKit 并实现上述功能:
// App.tsx
import React from 'react';
import { RainbowKitProvider, getDefaultWallets, ConnectButton, Theme } from '@rainbow-me/rainbowkit';
import { configureChains, createConfig, WagmiConfig, Chain } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { publicProvider } from 'wagmi/providers/public';
import { useWalletIntegration } from './hooks/useWalletIntegration';
import NetworkModal from './components/NetworkModal';
import '@rainbow-me/rainbowkit/styles.css';
// 自定义链配置
const supportedChains: Chain[] = [
{ ...mainnet, name: 'Ethereum Mainnet' },
{ ...polygon, name: 'Polygon Mainnet' },
{ ...arbitrum, name: 'Arbitrum One' }
];
const defaultChain = polygon;
// 配置链和提供者
const { chains, publicClient } = configureChains(
supportedChains,
[publicProvider()]
);
// 配置钱包连接器
const { connectors } = getDefaultWallets({
appName: 'NFT Trading Platform',
projectId: 'YOUR_PROJECT_ID', // 从 WalletConnect Cloud 获取
chains
});
// 创建 wagmi 配置
const wagmiConfig = createConfig({
autoConnect: true, // 自动重新连接
connectors,
publicClient
});
// 自定义主题
const customTheme: Theme = {
colors: {
accentColor: '#8B5CF6',
accentColorForeground: '#FFFFFF',
actionButtonSecondaryBackground: 'transparent',
closeButton: '#6B7280',
closeButtonBackground: 'transparent',
connectButtonBackground: '#1F2937',
connectButtonBackgroundError: '#EF4444',
connectButtonText: '#FFFFFF',
connectButtonTextError: '#FFFFFF',
connectionIndicator: '#10B981',
error: '#EF4444',
generalBorder: '#374151',
generalBorderDim: '#1F2937',
menuItemBackground: '#111827',
modalBackdrop: 'rgba(0, 0, 0, 0.5)',
modalBackground: '#111827',
modalBorder: '#374151',
modalText: '#F9FAFB',
modalTextDim: '#9CA3AF',
modalTextSecondary: '#D1D5DB',
profileAction: '#1F2937',
profileActionHover: '#374151',
profileForeground: '#111827',
selectedOptionBorder: '#8B5CF6',
standby: '#F59E0B'
},
radii: {
connectButton: '12px',
menuButton: '12px',
modal: '16px',
modalMobile: '16px'
},
fonts: {
body: 'Inter, sans-serif'
}
};
// 主应用组件
function AppContent() {
useWalletIntegration();
return (
<div className="app">
<header className="navbar">
<div className="logo">NFT Trading Platform</div>
<div className="wallet-section">
<ConnectButton
showBalance={false}
accountStatus="address"
chainStatus="icon"
label="连接钱包"
/>
</div>
</header>
<main className="main-content">
<NetworkModal />
{/* 你的应用内容 */}
</main>
</div>
);
}
// 应用根组件
export default function App() {
return (
<WagmiConfig config={wagmiConfig}>
<RainbowKitProvider
chains={chains}
initialChain={defaultChain}
theme={customTheme}
appInfo={{
appName: 'NFT Trading Platform',
learnMoreUrl: 'https://docs.ourplatform.com'
}}
>
<AppContent />
</RainbowKitProvider>
</WagmiConfig>
);
}
// hooks/useWalletIntegration.ts
import { useAccount, useDisconnect } from 'wagmi';
import { useEffect } from 'react';
export function useWalletIntegration() {
const { address, chainId, isConnected } = useAccount();
const { disconnect } = useDisconnect();
useEffect(() => {
if (isConnected && address && chainId) {
console.log('钱包已连接:', { address, chainId });
// 这里可以添加你的业务逻辑,比如获取用户数据
} else {
console.log('钱包已断开');
// 清理用户数据
}
}, [isConnected, address, chainId]);
const handleDisconnect = () => {
if (window.confirm('确定要断开钱包连接吗?')) {
disconnect();
}
};
return {
isConnected,
address,
chainId,
handleDisconnect
};
}
// components/NetworkModal.tsx
import React from 'react';
import { useAccount, useSwitchChain } from 'wagmi';
import { polygon } from 'wagmi/chains';
export default function NetworkModal() {
const { chain, chains } = useAccount();
const { switchChain } = useSwitchChain();
const [showModal, setShowModal] = React.useState(false);
const isUnsupported = chain && !chains.some(c => c.id === chain.id);
React.useEffect(() => {
setShowModal(!!isUnsupported);
}, [isUnsupported]);
const handleSwitchToPolygon = async () => {
try {
await switchChain({ chainId: polygon.id });
setShowModal(false);
} catch (error) {
console.error('切换网络失败:', error);
alert('请手动在钱包中切换网络到 Polygon');
}
};
if (!showModal) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}}>
<div style={{
backgroundColor: '#1F2937',
padding: '24px',
borderRadius: '16px',
maxWidth: '400px',
width: '90%'
}}>
<h3 style={{ marginTop: 0, color: '#F9FAFB' }}>
不支持的网络
</h3>
<p style={{ color: '#D1D5DB' }}>
您的钱包当前连接的是 <strong>{chain?.name}</strong>,
请切换到支持的网络以继续使用。
</p>
<button
onClick={handleSwitchToPolygon}
style={{
backgroundColor: '#8B5CF6',
color: 'white',
border: 'none',
padding: '12px 24px',
borderRadius: '8px',
cursor: 'pointer',
width: '100%',
fontSize: '16px',
fontWeight: 'bold'
}}
>
切换到 Polygon Mainnet
</button>
<p style={{
color: '#9CA3AF',
fontSize: '14px',
marginTop: '12px',
textAlign: 'center'
}}>
如果切换失败,请手动在钱包中切换网络
</p>
</div>
</div>
);
}
踩坑记录
在实际集成过程中,我遇到了几个比较典型的坑:
-
WalletConnect Project ID 问题
- 现象:连接时提示 "Invalid projectId" 错误。
- 原因:直接从官方示例复制了
projectId: 'YOUR_PROJECT_ID',没有去 WalletConnect Cloud 注册获取真实的 ID。 - 解决:去 cloud.walletconnect.com 注册项目,获取真实的 Project ID。
-
Next.js 中的 Hydration 错误
- 现象:在 Next.js 项目中使用时,控制台报 Hydration 错误。
- 原因:RainbowKit 的一些组件在服务端渲染时和客户端渲染时状态不一致。
- 解决:用
dynamic导入并设置ssr: false:import dynamic from 'next/dynamic'; const RainbowKitProvider = dynamic( () => import('@rainbow-me/rainbowkit').then(mod => mod.RainbowKitProvider), { ssr: false } );
-
自定义链的 RPC 节点问题
- 现象:添加自定义测试网时,交易一直失败。
- 原因:使用的公共 RPC 节点有速率限制或已经失效。
- 解决:申请 Infura 或 Alchemy 的免费 API 密钥,使用可靠的 RPC 节点:
import { alchemyProvider } from 'wagmi/providers/alchemy'; const { chains } = configureChains( [mainnet, polygon], [ alchemyProvider({ apiKey: process.env.ALCHEMY_API_KEY! }), publicProvider() // 备用公共节点 ] );
-
移动端样式问题
- 现象:在手机上连接钱包时,弹窗太大,超出屏幕。
- 原因:RainbowKit 的模态框默认宽度在移动端不够自适应。
- 解决:通过自定义主题调整移动端样式:
const customTheme = { // ... 其他配置 radii: { modalMobile: '0px', // 移动端去掉圆角,充分利用屏幕 } };
小结
通过这次 RainbowKit 集成,我最大的收获是:不要重复造轮子,但要知道轮子是怎么转的。RainbowKit 确实大大简化了多链钱包连接的开发,但真正用好在生产环境,还是需要理解其背后的 wagmi 和 viem 的工作原理,特别是链状态管理和错误处理。下一步我打算深入研究 RainbowKit 的插件系统,看看如何集成更多定制化的功能,比如交易确认弹窗的自定义、钱包连接后的自动操作等。