Web3 前端开发
Web3 前端开发与传统 Web2 开发有显著差异,需要处理钱包连接、智能合约交互、交易签名等特殊场景。本章将介绍现代 Web3 前端开发的技术栈和最佳实践。
Web3 前端技术栈
核心库对比
| 库 | 类型 | 特点 | 适用场景 |
|---|---|---|---|
| wagmi | React Hooks | 类型安全、模块化 | React 应用 |
| viem | 底层库 | 轻量、高性能 | 通用 TypeScript |
| ethers.js | 底层库 | 功能完整、文档丰富 | 通用 JavaScript |
| web3.js | 底层库 | 历史悠久、兼容性好 | 传统项目迁移 |
推荐技术栈
┌─────────────────────────────────────────────────────────────┐
│ Web3 前端技术栈 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ UI 层 │ │
│ │ React / Next.js / Vue / Svelte │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 钱包连接层 │ │
│ │ RainbowKit / ConnectKit / Web3Modal │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 状态管理层 │ │
│ │ wagmi / @wagmi/core │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 底层交互层 │ │
│ │ viem / ethers.js │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
wagmi 快速入门
wagmi 是目前最流行的 React Web3 库,提供类型安全的 Hooks。
安装配置
npm install wagmi viem @tanstack/react-query
import { createConfig, http } from 'wagmi';
import { mainnet, sepolia, arbitrum, optimism } from 'wagmi/chains';
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
const projectId = 'YOUR_WALLET_CONNECT_PROJECT_ID';
export const config = createConfig({
chains: [mainnet, sepolia, arbitrum, optimism],
connectors: [
injected(),
walletConnect({ projectId }),
coinbaseWallet({
appName: 'My DApp',
}),
],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
[arbitrum.id]: http(),
[optimism.id]: http(),
},
});
declare module 'wagmi' {
interface Register {
config: typeof config;
}
}
Provider 配置
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './config';
const queryClient = new QueryClient();
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</WagmiProvider>
);
}
核心 Hooks
账户状态
import { useAccount, useBalance, useConnect, useDisconnect } from 'wagmi';
function AccountInfo() {
const { address, isConnected, chain } = useAccount();
const { data: balance } = useBalance({
address,
});
const { connectors, connect } = useConnect();
const { disconnect } = useDisconnect();
if (!isConnected) {
return (
<div>
{connectors.map((connector) => (
<button
key={connector.id}
onClick={() => connect({ connector })}
>
Connect {connector.name}
</button>
))}
</div>
);
}
return (
<div>
<p>Address: {address}</p>
<p>Chain: {chain?.name}</p>
<p>Balance: {balance?.formatted} {balance?.symbol}</p>
<button onClick={() => disconnect()}>Disconnect</button>
</div>
);
}
读取合约
import { useReadContract, useReadContracts } from 'wagmi';
import { parseAbi } from 'viem';
const erc20Abi = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function balanceOf(address owner) view returns (uint256)',
'function totalSupply() view returns (uint256)',
]);
function TokenInfo({ tokenAddress, userAddress }: {
tokenAddress: `0x${string}`;
userAddress: `0x${string}`;
}) {
const { data: name } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'name',
});
const { data: balance } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
});
const { data: multiData } = useReadContracts({
contracts: [
{
address: tokenAddress,
abi: erc20Abi,
functionName: 'symbol',
},
{
address: tokenAddress,
abi: erc20Abi,
functionName: 'decimals',
},
{
address: tokenAddress,
abi: erc20Abi,
functionName: 'totalSupply',
},
],
});
return (
<div>
<p>Token Name: {name}</p>
<p>Balance: {balance?.toString()}</p>
<p>Symbol: {multiData?.[0].result}</p>
<p>Decimals: {multiData?.[1].result}</p>
<p>Total Supply: {multiData?.[2].result?.toString()}</p>
</div>
);
}
写入合约
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseAbi } from 'viem';
const erc20Abi = parseAbi([
'function transfer(address to, uint256 amount) returns (bool)',
'function approve(address spender, uint256 amount) returns (bool)',
]);
function TransferToken({ tokenAddress }: {
tokenAddress: `0x${string}`;
}) {
const { data: hash, writeContract, isPending } = useWriteContract();
const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({
hash,
});
const handleTransfer = async () => {
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [
'0x1234567890123456789012345678901234567890',
BigInt(1000000000000000000),
],
});
};
return (
<div>
<button
onClick={handleTransfer}
disabled={isPending || isConfirming}
>
{isPending ? 'Confirming...' : 'Transfer'}
</button>
{hash && <p>Transaction Hash: {hash}</p>}
{isConfirming && <p>Waiting for confirmation...</p>}
{isSuccess && <p>Transaction confirmed!</p>}
</div>
);
}
监听事件
import { useWatchContractEvent } from 'wagmi';
import { parseAbi } from 'viem';
const erc20Abi = parseAbi([
'event Transfer(address indexed from, address indexed to, uint256 value)',
'event Approval(address indexed owner, address indexed spender, uint256 value)',
]);
function EventListener({ tokenAddress }: {
tokenAddress: `0x${string}`;
}) {
useWatchContractEvent({
address: tokenAddress,
abi: erc20Abi,
eventName: 'Transfer',
onLogs(logs) {
console.log('Transfer events:', logs);
logs.forEach((log) => {
console.log('From:', log.args.from);
console.log('To:', log.args.to);
console.log('Value:', log.args.value?.toString());
});
},
});
return <div>Listening for Transfer events...</div>;
}
RainbowKit 集成
RainbowKit 提供美观的钱包连接界面。
安装配置
npm install @rainbow-me/rainbowkit
import '@rainbow-me/rainbowkit/styles.css';
import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit';
import { WagmiProvider } from 'wagmi';
import { mainnet, sepolia, arbitrum, optimism } from 'wagmi/chains';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const config = getDefaultConfig({
appName: 'My DApp',
projectId: 'YOUR_WALLET_CONNECT_PROJECT_ID',
chains: [mainnet, sepolia, arbitrum, optimism],
ssr: true,
});
const queryClient = new QueryClient();
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<RainbowKitProvider>
<YourApp />
</RainbowKitProvider>
</QueryClientProvider>
</WagmiProvider>
);
}
使用 Connect Button
import { ConnectButton } from '@rainbow-me/rainbowkit';
function Header() {
return (
<header>
<nav>
<h1>My DApp</h1>
<ConnectButton />
</nav>
</header>
);
}
自定义主题
import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit';
function App() {
return (
<RainbowKitProvider
theme={darkTheme({
accentColor: '#7b3ff2',
accentColorForeground: 'white',
borderRadius: 'small',
fontStack: 'system',
})}
>
<YourApp />
</RainbowKitProvider>
);
}
viem 底层交互
viem 是 wagmi 的底层依赖,提供高性能的以太坊交互能力。
创建客户端
import { createPublicClient, createWalletClient, http } from 'viem';
import { mainnet } from 'viem/chains';
import { privateKeyToAccount } from 'viem/accounts';
const publicClient = createPublicClient({
chain: mainnet,
transport: http(),
});
const account = privateKeyToAccount('0x...');
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(),
});
读取数据
const blockNumber = await publicClient.getBlockNumber();
const balance = await publicClient.getBalance({
address: '0x1234567890123456789012345678901234567890',
});
const ensName = await publicClient.getEnsName({
address: '0x1234567890123456789012345678901234567890',
});
const bytecode = await publicClient.getBytecode({
address: '0x1234567890123456789012345678901234567890',
});
合约交互
import { parseAbi } from 'viem';
const erc20Abi = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
]);
const balance = await publicClient.readContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
functionName: 'balanceOf',
args: ['0x1234567890123456789012345678901234567890'],
});
const hash = await walletClient.writeContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
functionName: 'transfer',
args: [
'0x1234567890123456789012345678901234567890',
BigInt(1000000),
],
});
模拟交易
const { request } = await publicClient.simulateContract({
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
abi: erc20Abi,
functionName: 'transfer',
args: [
'0x1234567890123456789012345678901234567890',
BigInt(1000000),
],
account: '0x1234567890123456789012345678901234567890',
});
const hash = await walletClient.writeContract(request);
等待交易确认
const receipt = await publicClient.waitForTransactionReceipt({
hash,
});
console.log('Transaction confirmed:', receipt.status);
console.log('Block number:', receipt.blockNumber);
console.log('Gas used:', receipt.gasUsed);
完整 DApp 示例
代币交换组件
import { useState } from 'react';
import { useAccount, useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseAbi, formatUnits, parseUnits } from 'viem';
const erc20Abi = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function balanceOf(address owner) view returns (uint256)',
'function allowance(address owner, address spender) view returns (uint256)',
'function approve(address spender, uint256 amount) returns (bool)',
]);
const routerAbi = parseAbi([
'function swapExactTokensForTokens(uint256 amountIn, uint256 amountOutMin, address[] path, address to, uint256 deadline) returns (uint256[])',
]);
interface TokenSwapProps {
tokenIn: `0x${string}`;
tokenOut: `0x${string}`;
router: `0x${string}`;
}
export function TokenSwap({ tokenIn, tokenOut, router }: TokenSwapProps) {
const { address } = useAccount();
const [amount, setAmount] = useState('');
const { data: decimals } = useReadContract({
address: tokenIn,
abi: erc20Abi,
functionName: 'decimals',
});
const { data: balance } = useReadContract({
address: tokenIn,
abi: erc20Abi,
functionName: 'balanceOf',
args: [address!],
query: {
enabled: !!address,
},
});
const { data: allowance } = useReadContract({
address: tokenIn,
abi: erc20Abi,
functionName: 'allowance',
args: [address!, router],
query: {
enabled: !!address,
},
});
const { writeContract: approve, data: approveHash, isPending: isApproving } = useWriteContract();
const { writeContract: swap, data: swapHash, isPending: isSwapping } = useWriteContract();
const { isSuccess: isApproveConfirmed } = useWaitForTransactionReceipt({
hash: approveHash,
});
const { isSuccess: isSwapConfirmed } = useWaitForTransactionReceipt({
hash: swapHash,
});
const needsApproval = allowance !== undefined &&
amount !== '' &&
allowance < parseUnits(amount, decimals || 18);
const handleApprove = () => {
if (!decimals || !amount) return;
approve({
address: tokenIn,
abi: erc20Abi,
functionName: 'approve',
args: [router, parseUnits(amount, decimals)],
});
};
const handleSwap = () => {
if (!decimals || !amount || !address) return;
swap({
address: router,
abi: routerAbi,
functionName: 'swapExactTokensForTokens',
args: [
parseUnits(amount, decimals),
BigInt(0),
[tokenIn, tokenOut],
address,
BigInt(Math.floor(Date.now() / 1000) + 1200),
],
});
};
return (
<div className="swap-container">
<div className="balance">
Balance: {balance ? formatUnits(balance, decimals || 18) : '0'}
</div>
<input
type="number"
value={amount}
onChange={(e) => setAmount(e.target.value)}
placeholder="Amount"
/>
{needsApproval ? (
<button
onClick={handleApprove}
disabled={isApproving}
>
{isApproving ? 'Approving...' : 'Approve'}
</button>
) : (
<button
onClick={handleSwap}
disabled={isSwapping || !amount}
>
{isSwapping ? 'Swapping...' : 'Swap'}
</button>
)}
{isApproveConfirmed && <p>Approval confirmed!</p>}
{isSwapConfirmed && <p>Swap confirmed!</p>}
</div>
);
}
签名验证组件
import { useState } from 'react';
import { useAccount, useSignMessage, useVerifyMessage } from 'wagmi';
export function SignMessage() {
const { address } = useAccount();
const [message, setMessage] = useState('');
const { signMessage, data: signature, isPending } = useSignMessage();
const { data: isValid, isLoading: isVerifying } = useVerifyMessage({
address,
message,
signature,
});
const handleSign = () => {
signMessage({ message });
};
return (
<div>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter message to sign"
/>
<button
onClick={handleSign}
disabled={isPending || !message}
>
{isPending ? 'Signing...' : 'Sign Message'}
</button>
{signature && (
<div>
<p>Signature: {signature}</p>
<p>Valid: {isVerifying ? 'Verifying...' : isValid ? 'Yes' : 'No'}</p>
</div>
)}
</div>
);
}
错误处理
交易错误处理
import { useWriteContract } from 'wagmi';
import { UserRejectedRequestError, ContractFunctionExecutionError } from 'viem';
function SafeTransaction() {
const { writeContract, error, isPending } = useWriteContract();
const handleError = (error: Error) => {
if (error instanceof UserRejectedRequestError) {
console.log('User rejected the transaction');
} else if (error instanceof ContractFunctionExecutionError) {
console.log('Contract execution failed:', error.message);
} else {
console.log('Unknown error:', error);
}
};
return (
<div>
<button
onClick={() => {
try {
writeContract({...});
} catch (e) {
handleError(e as Error);
}
}}
disabled={isPending}
>
Execute
</button>
{error && <p className="error">{error.message}</p>}
</div>
);
}
网络切换
import { useSwitchChain, useChainId } from 'wagmi';
function NetworkSwitcher() {
const chainId = useChainId();
const { chains, switchChain, isPending } = useSwitchChain();
return (
<select
value={chainId}
onChange={(e) => {
switchChain({ chainId: Number(e.target.value) });
}}
disabled={isPending}
>
{chains.map((chain) => (
<option key={chain.id} value={chain.id}>
{chain.name}
</option>
))}
</select>
);
}
性能优化
缓存策略
import { useReadContract } from 'wagmi';
function OptimizedRead() {
const { data, refetch } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
query: {
staleTime: 10_000,
gcTime: 60_000,
refetchOnWindowFocus: false,
},
});
return (
<div>
<p>Balance: {data?.toString()}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}
批量查询
import { useReadContracts } from 'wagmi';
function BatchQuery({ tokens, userAddress }: {
tokens: `0x${string}`[];
userAddress: `0x${string}`;
}) {
const { data, isLoading } = useReadContracts({
contracts: tokens.map((token) => ({
address: token,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
})),
query: {
select: (results) => results.map((r) => r.result),
},
});
if (isLoading) return <div>Loading...</div>;
return (
<ul>
{tokens.map((token, i) => (
<li key={token}>
{token}: {data?.[i]?.toString()}
</li>
))}
</ul>
);
}
小结
Web3 前端开发需要掌握钱包连接、合约交互、交易管理等核心技能。wagmi 提供了类型安全的 React Hooks,viem 提供了高性能的底层交互能力,RainbowKit 提供了美观的钱包连接界面。结合这些工具,可以快速构建现代化的去中心化应用。