Web3前端开发完整教程 | wagmi+viem实战指南2026

深紫渐变背景的代码编辑器界面,显示wagmi和viem的React Hooks代码片段

一、wagmi和viem是什么

1.1 为什么要用wagmi

直接用ethers.js或viem写以太坊交互也能工作,但涉及到React状态管理、缓存、自动重连、错误处理这些「脚手架」代码时,工作量就上来了。wagmi把这些都封装成React Hooks,让开发者专注于业务逻辑。

wagmi的核心价值:

  • 开箱即用的React Hooks:连接钱包、读取数据、发送交易都有现成方案
  • 自动状态管理:连接状态、链ID、余额变化自动同步到组件
  • 缓存与去重:基于TanStack Query,不会重复发送相同的请求
  • TypeScript优先:类型提示完善,开发时就能发现错误

1.2 viem的角色

viem是底层库,负责与以太坊节点的通信。它比ethers.js更轻量、更快,提供了完整的以太坊JSON-RPC API封装。wagmi依赖viem作为传输层,同时提供React层面的抽象。

两者的关系:viem做网络和编码工作,wagmi做React集成工作。

展示wagmi与viem技术栈的协作架构图,左侧为用户界面(连接钱包按钮、代币余额),中间为wagmi的五个核心功能模块,右侧为viem RPC调用与以太坊区块链节点,通过连线清晰展示数据流向

二、项目初始化

2.1 创建React项目

我习惯用Vite创建项目,比Create React App快很多:

bash

npm create vite@latest my-web3-dapp -- --template react-ts
cd my-web3-dapp
npm install

2.2 安装依赖

bash

# 核心依赖
npm install wagmi viem @tanstack/react-query

# UI组件库(可选,推荐RainbowKit)
npm install @rainbow-me/rainbowkit
npm install @tanstack/react-query

# 用于样式(Vite默认没有CSS方案)
npm install tailwindcss postcss autoprefixer
npx tailwindcss init -p

2.3 基础配置

创建wagmi配置文件:

typescript

// src/wagmi.config.ts
import { http, createConfig } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { injected, walletConnect } from 'wagmi/connectors'

// WalletConnect项目ID(https://cloud.walletconnect.com/ 注册)
const projectId = import.meta.env.VITE_WALLET_CONNECT_PROJECT_ID

export const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    injected(),           // 浏览器插件钱包(MetaMask、Coinbase Wallet等)
    walletConnect({ 
      projectId,
      metadata: {
        name: 'My Web3 DApp',
        description: 'A sample Web3 application',
        url: window.location.origin,
        icons: ['https://example.com/icon.png']
      }
    }),
  ],
  transports: {
    [mainnet.id]: http(),  // 使用公共RPC,生产环境建议用自己的节点
    [sepolia.id]: http(),
  },
})

2.4 在main.tsx中配置Provider

typescript

// src/main.tsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
import App from './App'
import { config } from './wagmi.config'
import '@rainbow-me/rainbowkit/styles.css'  // RainbowKit样式

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowKitProvider>
          <App />
        </RainbowKitProvider>
      </QueryClientProvider>
    </WagmiProvider>
  </React.StrictMode>,
)

三、钱包连接实战

3.1 连接按钮组件

使用RainbowKit可以快速获得美观的连接按钮:

typescript

// src/components/ConnectButton.tsx
import { ConnectButton } from '@rainbow-me/rainbowkit'

export function WalletConnect() {
  return (
    <ConnectButton
      showBalance={{
        smallScreen: false,
        largeScreen: true,
      }}
      chainStatus="icon"
      accountStatus={{
        smallScreen: 'avatar',
        largeScreen: 'full',
      }}
    />
  )
}

3.2 自定义连接逻辑

如果不使用RainbowKit,可以自己实现:

typescript

// src/components/CustomConnect.tsx
import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'

export function CustomConnect() {
  const { address, isConnected } = useAccount()
  const { connect } = useConnect()
  const { disconnect } = useDisconnect()

  if (isConnected) {
    return (
      <div>
        <p>Connected: {address?.slice(0, 6)}...{address?.slice(-4)}</p>
        <button onClick={() => disconnect()}>Disconnect</button>
      </div>
    )
  }

  return (
    <button onClick={() => connect({ connector: injected() })}>
      Connect Wallet
    </button>
  )
}

四、读取区块链数据

4.1 读取原生币余额

typescript

import { useBalance, useAccount } from 'wagmi'

function ETHBalance() {
  const { address, isConnected } = useAccount()
  
  const { data, isError, isLoading } = useBalance({
    address,
    query: {
      enabled: !!isConnected,
    }
  })

  if (!isConnected) return <p>Please connect your wallet</p>
  if (isLoading) return <p>Loading...</p>
  if (isError) return <p>Error fetching balance</p>

  return (
    <div>
      <p>Balance: {data?.formatted} {data?.symbol}</p>
      <p>In USD: ${(Number(data?.formatted) * 3500).toFixed(2)}</p>
    </div>
  )
}

4.2 读取ERC20代币余额

需要自己构建读取合约的请求:

typescript

import { useContractRead, useAccount } from 'wagmi'
import { erc20Abi } from 'viem'

// USDC地址(主网)
const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'

function USDCBalance() {
  const { address, isConnected } = useAccount()
  
  const { data: balance } = useContractRead({
    address: USDC_ADDRESS,
    abi: erc20Abi,
    functionName: 'balanceOf',
    args: [address!],
    query: {
      enabled: !!address,
    }
  })

  if (!isConnected) return <p>Please connect your wallet</p>
  
  // USDC是6位精度
  const formattedBalance = Number(balance) / 10 ** 6
  
  return <p>USDC Balance: {formattedBalance.toFixed(2)}</p>
}

4.3 批量读取多个合约

useContractReads可以一次读取多个合约的状态,减少网络请求:

typescript

import { useContractReads, useAccount } from 'wagmi'
import { erc20Abi } from 'viem'

const TOKENS = [
  { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 },
  { address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', symbol: 'WETH', decimals: 18 },
]

function TokenBalances() {
  const { address, isConnected } = useAccount()

  const { data, isLoading } = useContractReads({
    contracts: TOKENS.map((token) => ({
      address: token.address as `0x${string}`,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [address!],
    })),
    query: { enabled: !!address }
  })

  if (!isConnected) return <p>Connect wallet</p>
  if (isLoading) return <p>Loading...</p>

  return (
    <div>
      {TOKENS.map((token, index) => {
        const balance = data?.[index]?.result
        const formatted = balance ? Number(balance) / 10 ** token.decimals : 0
        return (
          <p key={token.symbol}>
            {token.symbol}: {formatted.toFixed(4)}
          </p>
        )
      })}
    </div>
  )
}

五、发送交易

5.1 发送ETH

typescript

import { useSendTransaction, useWaitForTransactionReceipt } from 'wagmi'
import { parseEther } from 'viem'

function SendETH() {
  const { sendTransaction, data: hash, isPending, error } = useSendTransaction()
  const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({
    hash,
  })

  async function handleSend(to: string, amount: string) {
    sendTransaction({
      to: to as `0x${string}`,
      value: parseEther(amount),  // 字符串转成Wei
    })
  }

  return (
    <div>
      <button 
        onClick={() => handleSend('0x...', '0.01')}
        disabled={isPending}
      >
        {isPending ? 'Confirming...' : 'Send 0.01 ETH'}
      </button>
      
      {error && <p>Error: {error.message}</p>}
      {isConfirming && <p>Waiting for confirmation...</p>}
      {isConfirmed && <p>Transaction confirmed!</p>}
    </div>
  )
}

5.2 调用合约函数

以ERC20的transfer为例:

typescript

import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { erc20Abi } from 'viem'

const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'

function TransferUSDC() {
  const { 
    writeContract, 
    data: hash, 
    isPending, 
    error 
  } = useWriteContract()
  
  const { isLoading: isConfirming, isSuccess: isConfirmed } = 
    useWaitForTransactionReceipt({ hash })

  function handleTransfer(to: string, amount: string) {
    // amount是USDC的原始精度(6位小数)
    writeContract({
      address: USDC_ADDRESS,
      abi: erc20Abi,
      functionName: 'transfer',
      args: [
        to as `0x${string}`,
        BigInt(Math.floor(Number(amount) * 10 ** 6))
      ],
    })
  }

  return (
    <div>
      <button
        onClick={() => handleTransfer('0x...', '100')}
        disabled={isPending}
      >
        {isPending ? 'Sign in wallet...' : 'Transfer 100 USDC'}
      </button>
      
      {isConfirming && <p>Transaction submitting...</p>}
      {isConfirmed && <p>Transfer complete!</p>}
      {error && <p>Error: {error.shortMessage || error.message}</p>}
    </div>
  )
}

六、实战案例:DeFi代币兑换界面

6.1 组件结构

一个简单的代币兑换界面需要:

  • 代币选择器
  • 金额输入
  • 汇率显示
  • Swap按钮
  • 交易状态显示

typescript

// src/components/Swap.tsx
import { useState } from 'react'
import { useAccount, useBalance } from 'wagmi'
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'
import { parseUnits, formatUnits } from 'viem'

// 简化示例:直接调用Uniswap V2 Router
const ROUTER_ADDRESS = '0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D'

export function Swap() {
  const { address, isConnected } = useAccount()
  
  // 代币信息
  const [tokenIn, setTokenIn] = useState('ETH')
  const [tokenOut, setTokenOut] = useState('USDC')
  const [amountIn, setAmountIn] = useState('')
  
  const { writeContract, data: hash, isPending } = useWriteContract()
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })

  // 读取ETH余额
  const { data: ethBalance } = useBalance({ address })

  // 处理Swap
  async function handleSwap() {
    if (!amountIn) return
    
    const amountInWei = parseEther(amountIn)
    
    writeContract({
      address: ROUTER_ADDRESS,
      abi: UNISWAP_V2_ROUTER_ABI,
      functionName: 'swapExactETHForTokens',
      args: [
        BigInt(0),  // 最小输出金额,生产环境应该设合理值
        [WETH_ADDRESS, USDC_ADDRESS],  // 交易路径
        address!,
        BigInt(Math.floor(Date.now() / 1000) + 60 * 20)  // 20分钟过期
      ],
      value: amountInWei,
    })
  }

  if (!isConnected) {
    return <p>Please connect your wallet</p>
  }

  return (
    <div className="swap-container">
      <div className="input-group">
        <label>You Pay</label>
        <input 
          type="number"
          value={amountIn}
          onChange={(e) => setAmountIn(e.target.value)}
          placeholder="0.0"
        />
        <span>{tokenIn}</span>
        <p>Balance: {ethBalance?.formatted}</p>
      </div>
      
      <button onClick={handleSwap} disabled={isPending || !amountIn}>
        {isPending ? 'Sign in wallet...' : 'Swap'}
      </button>
      
      {isSuccess && <p>Swap successful!</p>}
    </div>
  )
}

七、常见问题与解决方案

7.1 用户拒绝签名

用户可能拒绝交易或者根本就没装钱包:

typescript

const { writeContract, error } = useWriteContract()

// error.shortMessage 通常包含有用的信息
// "User rejected the request" = 用户拒绝
// "insufficient funds" = 余额不足

7.2 链不匹配

用户连接的链与DApp不匹配时,需要提示切换:

typescript

import { useChainId, useSwitchChain } from 'wagmi'

function NetworkWarning() {
  const chainId = useChainId()
  const { switchChain } = useSwitchChain()
  
  if (chainId !== sepolia.id) {
    return (
      <div className="warning">
        <p>Please switch to Sepolia testnet</p>
        <button onClick={() => switchChain({ chainId: sepolia.id })}>
          Switch Network
        </button>
      </div>
    )
  }
}

7.3 交易Pending处理

用户发起交易后,有时候会Pending很久:

typescript

import { useWaitForTransactionReceipt } from 'wagmi'

const { isLoading: isPending } = useWaitForTransactionReceipt({
  hash,
  // 超时设置
  timeout: 1000 * 60 * 5, // 5分钟超时
})

if (isPending) {
  return <p>Transaction pending for a while... Please check your wallet.</p>
}

八、生产环境注意事项

8.1 使用自己的RPC节点

公共RPC有速率限制,生产环境应该用Alchemy或Infura:

typescript

import { http } from 'wagmi'
import { mainnet } from 'wagmi/chains'

const alchemyKey = import.meta.env.VITE_ALCHEMY_API_KEY

export const config = createConfig({
  chains: [mainnet],
  transports: {
    [mainnet.id]: http(`https://eth-mainnet.g.alchemy.com/v2/${alchemyKey}`),
  },
})

8.2 其他优化建议

注意缓存策略和错误边界。TanStack Query的缓存配置要适合区块链数据的实时性,错误边界能防止单个组件崩溃影响整个应用。

结语

wagmi和viem让Web3前端开发变得异常流畅。你不需要成为以太坊专家,只需要理解几个核心概念——连接钱包、读取数据、发送交易——就能构建出体验良好的DApp。

我建议的学习路径是:

  1. 先跑通官方的示例项目
  2. 学会连接钱包和读取余额
  3. 尝试发送一笔简单的ETH转账
  4. 挑战一个完整的合约调用(比如ERC20转账)
  5. 最后实现一个简单的DeFi交互界面

Web3前端开发的门槛已经很低了,缺的只是更多有热情加入的开发者。

相关阅读:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注