背景
上个月,我接手了一个新的DeFi仪表板项目。这个项目需要同时支持以太坊主网、Arbitrum、Polygon和Base四条链,用户需要能自由切换网络,查看不同链上的资产和合约交互。老板给的时间很紧,要求两周内做出可演示的MVP。
我第一时间想到的是自己用wagmi和viem从头搭建一套钱包连接系统。但粗略评估了一下,这至少需要处理:1)连接按钮UI;2)钱包列表弹窗;3)链切换逻辑;4)连接状态持久化;5)错误处理。没个三五天搞不定,而且容易出边界case。
这时候我想起了RainbowKit------一个基于wagmi构建的React钱包连接套件。官方文档说它能"几分钟内集成钱包连接",支持多链,还有不错的默认UI。我决定赌一把,用它来加速开发。没想到,这个"几分钟"的背后,我花了整整一个周末来填坑。
问题分析
我的初始思路很简单:照着RainbowKit官方文档的"快速开始"一步步来。文档确实清晰,安装、包裹Provider、添加连接按钮,三步走。我很快就在本地跑起了一个基础版本。
第一个不对劲的地方立刻出现了:我的项目只需要四条链,但默认的RainbowKit连接弹窗里显示了十几种链,包括一些测试链。这会让用户困惑,尤其是我们的目标用户可能并不熟悉所有EVM链。
第二个问题是UI风格。RainbowKit的默认暗色主题和我们项目的亮色设计系统格格不入。我需要自定义主题颜色、圆角、字体,让它看起来像是我们产品原生的一部分。
最棘手的是第三个问题:连接状态同步。当用户在MetaMask里手动切换了网络,或者断开连接后,我的应用界面需要实时响应这些变化。我最初以为RainbowKit会自动处理好这一切,但在初步测试中发现了状态延迟和不同步的情况。
我意识到,我需要深入配置RainbowKit,而不是仅仅使用它的默认设置。下面就是我解决问题的具体步骤。
核心实现
第一步:基础安装与项目配置
首先,我创建了一个新的Next.js项目(使用TypeScript),然后安装必要的依赖。这里注意,RainbowKit需要配合wagmi和viem使用。
bash
npm install @rainbow-me/rainbowkit wagmi viem @tanstack/react-query
接下来,我创建了一个Providers组件,用来包裹应用的根组件。这是集中管理Web3配置的地方。
typescript
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider } from '@rainbow-me/rainbowkit';
import { config } from './wagmi.config';
import '@rainbow-me/rainbowkit/styles.css';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
然后在app/layout.tsx中用<Providers>包裹{children}。基础架子就搭好了。
第二步:精准配置多链网络
这是第一个关键点。我不想让用户看到一堆用不到的链。我需要创建一个自定义的wagmi配置,只包含我需要的四条链。
我创建了一个单独的配置文件wagmi.config.ts:
typescript
// app/wagmi.config.ts
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import {
mainnet,
polygon,
arbitrum,
base,
} from 'wagmi/chains';
// 这里有个坑:getDefaultConfig在RainbowKit v2.x版本中引入
// 如果你用的是旧版本,可能需要不同的配置方式
export const config = getDefaultConfig({
appName: 'MyDeFiDashboard',
projectId: 'YOUR_WALLETCONNECT_PROJECT_ID', // 必须去WalletConnect Cloud创建
chains: [mainnet, polygon, arbitrum, base], // 关键!只配置我们需要的链
ssr: true, // 如果你用Next.js等框架,需要开启SSR
});
注意这个细节 :projectId是必须的,用于WalletConnect连接。你需要去WalletConnect Cloud免费创建一个项目来获取ID。没有这个ID,WalletConnect相关的钱包(比如Rainbow钱包本身)会连接失败。
通过精确指定chains数组,RainbowKit的链切换弹窗里就只会显示这四条链,清爽多了。
第三步:深度自定义UI主题
RainbowKit的默认样式是暗色系,我们的设计是亮色系。我需要覆盖几乎所有的主题变量。
我修改了RainbowKitProvider的配置:
typescript
// 在providers.tsx中更新RainbowKitProvider
import { darkTheme, lightTheme } from '@rainbow-me/rainbowkit';
<RainbowKitProvider
theme={lightTheme({
accentColor: '#3B82F6', // 主按钮颜色,用了Tailwind的blue-500
accentColorForeground: 'white',
borderRadius: 'medium',
fontStack: 'system',
overlayBlur: 'small',
})}
// 这个locale配置可以汉化按钮文本,对中文用户友好
locale="en-US"
>
{children}
</RainbowKitProvider>
但这样还不够。我发现连接按钮的尺寸和我们设计系统中的按钮对不上。RainbowKit提供了ConnectButton组件,但它暴露的可定制属性有限。我最终用了它的ConnectButton.Custom来获得完全的控制权。
tsx
// components/CustomConnectButton.tsx
import { ConnectButton } from '@rainbow-me/rainbowkit';
export const CustomConnectButton = () => {
return (
<ConnectButton.Custom>
{({
account,
chain,
openAccountModal,
openChainModal,
openConnectModal,
authenticationStatus,
mounted,
}) => {
// 注意:这里必须处理未挂载的状态,否则hydration会出错
const ready = mounted && authenticationStatus !== 'loading';
const connected =
ready &&
account &&
chain &&
(!authenticationStatus || authenticationStatus === 'authenticated');
return (
<div
{...(!ready && {
'aria-hidden': true,
'style': {
opacity: 0,
pointerEvents: 'none',
userSelect: 'none',
},
})}
>
{(() => {
if (!connected) {
return (
<button
onClick={openConnectModal}
className="px-4 py-2 bg-blue-500 text-white rounded-lg font-semibold hover:bg-blue-600 transition"
>
连接钱包
</button>
);
}
if (chain.unsupported) {
return (
<button
onClick={openChainModal}
className="px-4 py-2 bg-red-500 text-white rounded-lg font-semibold"
>
不支持的网络
</button>
);
}
return (
<div style={{ display: 'flex', gap: 12 }}>
<button
onClick={openChainModal}
className="px-3 py-2 bg-gray-100 rounded-lg flex items-center gap-2"
>
{chain.hasIcon && (
<div
style={{
background: chain.iconBackground,
width: 12,
height: 12,
borderRadius: 999,
overflow: 'hidden',
}}
>
{chain.iconUrl && (
<img
alt={chain.name ?? 'Chain icon'}
src={chain.iconUrl}
style={{ width: 12, height: 12 }}
/>
)}
</div>
)}
{chain.name}
</button>
<button
onClick={openAccountModal}
className="px-4 py-2 bg-blue-500 text-white rounded-lg"
>
{account.displayName}
</button>
</div>
);
})()}
</div>
);
}}
</ConnectButton.Custom>
);
};
这样,我就得到了一个完全符合设计规范的连接按钮,并且能根据连接状态显示不同的UI。
第四步:可靠的状态同步与监听
这是最让我头疼的部分。我需要确保:1)当用户在钱包扩展中切换网络时,应用能立即感知;2)当用户断开钱包连接时,应用状态能正确重置。
我创建了一个自定义钩子来集中管理连接状态逻辑:
typescript
// hooks/useWeb3Connection.ts
import { useAccount, useDisconnect, useSwitchChain } from 'wagmi';
import { useEffect } from 'react';
import { showToast } from '@/utils/toast'; // 假设有一个toast工具
export const useWeb3Connection = () => {
const { address, isConnected, chain, chainId } = useAccount();
const { disconnect } = useDisconnect();
const { switchChain } = useSwitchChain();
// 监听链切换
useEffect(() => {
if (!isConnected || !chainId) return;
// 检查当前链是否在我们支持的列表中
const supportedChainIds = [1, 137, 42161, 8453]; // 主网、Polygon、Arbitrum、Base
const isUnsupported = !supportedChainIds.includes(chainId);
if (isUnsupported) {
showToast.warning(`请切换到支持的网络`, {
action: {
text: '切换',
onClick: () => {
// 默认切换到主网
switchChain({ chainId: 1 });
},
},
});
}
}, [chainId, isConnected, switchChain]);
// 监听账户变化(比如用户在MetaMask里切换了账户)
useEffect(() => {
if (!address) return;
// 这里可以做一些事情,比如更新用户数据
console.log('当前连接地址:', address);
// 在实际项目中,这里可能会触发一个API调用
// fetchUserData(address);
}, [address]);
// 处理手动断开连接
const handleDisconnect = () => {
disconnect();
showToast.info('已断开钱包连接');
// 清除任何与钱包相关的本地状态
localStorage.removeItem('lastConnectedWallet');
};
return {
address,
isConnected,
chain,
chainId,
handleDisconnect,
isUnsupportedChain: chain?.unsupported,
};
};
然后在需要的地方使用这个钩子:
tsx
// app/dashboard/page.tsx
import { useWeb3Connection } from '@/hooks/useWeb3Connection';
export default function DashboardPage() {
const { address, isConnected, chain, isUnsupportedChain } = useWeb3Connection();
if (!isConnected) {
return <div>请先连接钱包</div>;
}
if (isUnsupportedChain) {
return <div>当前网络不支持,请切换网络</div>;
}
return (
<div>
<h1>仪表板</h1>
<p>连接地址: {address}</p>
<p>当前网络: {chain?.name}</p>
{/* 其他业务组件 */}
</div>
);
}
完整代码
以下是一个最小化但完整的可运行示例,展示了RainbowKit的核心集成:
typescript
// app/providers.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider } from 'wagmi';
import { RainbowKitProvider, lightTheme } from '@rainbow-me/rainbowkit';
import { config } from './wagmi.config';
import '@rainbow-me/rainbowkit/styles.css';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider
theme={lightTheme({
accentColor: '#3B82F6',
accentColorForeground: 'white',
borderRadius: 'medium',
})}
>
{children}
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
typescript
// app/wagmi.config.ts
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { mainnet, polygon, arbitrum, base } from 'wagmi/chains';
export const config = getDefaultConfig({
appName: 'DeFiDashboard',
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID || 'demo-project-id',
chains: [mainnet, polygon, arbitrum, base],
ssr: true,
});
tsx
// app/layout.tsx
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Providers } from './providers';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'DeFi Dashboard',
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}
tsx
// app/page.tsx
'use client';
import { ConnectButton } from '@rainbow-me/rainbowkit';
import { useAccount } from 'wagmi';
export default function Home() {
const { isConnected } = useAccount();
return (
<main className="min-h-screen p-8">
<header className="flex justify-between items-center mb-12">
<h1 className="text-3xl font-bold">DeFi Dashboard</h1>
<ConnectButton />
</header>
<div className="max-w-4xl mx-auto">
{isConnected ? (
<div>
<h2 className="text-2xl mb-4">欢迎使用仪表板</h2>
<p>连接成功!这里可以显示你的资产、交易记录等。</p>
</div>
) : (
<div className="text-center py-12">
<h2 className="text-2xl mb-4">请连接钱包开始使用</h2>
<p className="text-gray-600">
连接钱包后,你可以查看多链资产并进行交易。
</p>
</div>
)}
</div>
</main>
);
}
踩坑记录
-
Hydration错误 :在Next.js中使用时,如果不在
ConnectButton.Custom中处理!ready的状态,会出现hydration不匹配的错误。解决方案就是像上面代码那样,在未挂载时设置opacity: 0并禁用交互。 -
WalletConnect项目ID缺失 :第一次运行时,点击WalletConnect钱包(如Rainbow)没有任何反应,控制台也没有明显错误。后来发现是
projectId配置成了空字符串或假值。必须去WalletConnect Cloud创建真实项目获取ID,并确保在客户端代码中能访问到(通过环境变量)。 -
链图标不显示 :自定义链切换按钮时,我直接用了
chain.iconUrl,但在某些情况下这个值是undefined。后来发现需要先检查chain.hasIcon,并且对于自定义添加的链,可能需要手动提供图标URL。我最终添加了条件渲染和备用处理。 -
状态更新延迟 :当用户在MetaMask中手动断开连接后,我的应用界面有时需要几秒钟才更新。这是因为RainbowKit/wagmi的某些状态更新是异步的。我通过组合使用
useAccount的各个状态字段,并添加本地loading状态来改善用户体验,在状态同步期间显示加载提示。
小结
经过这个周末的折腾,我深刻体会到RainbowKit确实能极大加速Web3钱包连接的开发,但"开箱即用"仅限于最简单场景。真实项目中,你需要深入配置链列表、自定义UI、并仔细处理状态同步。最终,我用大约两天时间完成了一个稳定、美观、支持多链的钱包连接系统,比从头开始节省了至少三天。下一步,我计划研究如何集成更多类型的钱包(如智能合约钱包),并优化移动端的连接体验。