🎨 第12课:前端集成(ethers.js)

DApp开发 ✅ 验证通过

🎯 学习目标:掌握ethers.js与智能合约交互,实现钱包连接、合约读写、事件监听。

📖 一、ethers.js核心概念

ethers.js = DApp前端的以太坊交互库,v6基于ES模块,支持Tree-shaking。四大核心模块:Provider(只读)、Signer(读写)、Contract(合约交互)、Utils(工具)。ethers.js是目前最流行的以太坊JavaScript库,几乎所有DApp前端都使用它来与区块链交互。

import { ethers } from "ethers";

// 1. 连接MetaMask
async function connect() {
  const provider = new ethers.BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  const address = await signer.getAddress();
  const balance = await provider.getBalance(address);
  console.log("地址:", address);
  console.log("余额:", ethers.formatEther(balance), "ETH");
  return { provider, signer };
}

// 2. 连接合约
const ABI = [
  "function getCount() view returns (uint256)",
  "function increment()",
  "event CountIncremented(uint256,address)"
];
const contract = new ethers.Contract(ADDR, ABI, signer);

// 3. 读取(view函数, 免费)
const count = await contract.getCount();

// 4. 写入(需Gas)
const tx = await contract.increment();
const receipt = await tx.wait();

// 5. 事件监听
contract.on("CountIncremented", (n, caller) => {
  console.log("Count:", n, "by", caller);
});

// 6. 查询历史事件
const events = await contract.queryFilter("CountIncremented");

📖 二、React集成模式

import { useState, useEffect } from "react";
import { ethers } from "ethers";

function useCounter() {
  const [count, setCount] = useState(null);
  const [contract, setContract] = useState(null);

  useEffect(() => {
    async function init() {
      if (!window.ethereum) return;
      const provider = new ethers.BrowserProvider(window.ethereum);
      const signer = await provider.getSigner();
      const c = new ethers.Contract(ADDR, ABI, signer);
      setContract(c);
      setCount(Number(await c.getCount()));
      c.on("CountIncremented", (n) => setCount(Number(n)));
    }
    init();
  }, []);

  const increment = async () => {
    if (!contract) return;
    const tx = await contract.increment();
    await tx.wait();
  };

  return { count, increment };
}

function DApp() {
  const { count, increment } = useCounter();
  return <div><p>Count: {count}</p>
    <button onClick={increment}>+1</button></div>;
}

📖 三、链切换与错误处理

// 监听链切换
window.ethereum.on("chainChanged", () => window.location.reload());
window.ethereum.on("accountsChanged", (accs) => console.log(accs[0]));

// 切换到Sepolia
async function switchChain() {
  try {
    await window.ethereum.request({
      method: "wallet_switchEthereumChain",
      params: [{ chainId: "0xaa36a7" }],
    });
  } catch (err) {
    if (err.code === 4902) {
      await window.ethereum.request({
        method: "wallet_addEthereumChain",
        params: [{ chainId: "0xaa36a7", chainName: "Sepolia",
          rpcUrls: ["https://rpc.sepolia.org"],
          nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
          blockExplorerUrls: ["https://sepolia.etherscan.io"] }],
      });
    }
  }
}
ethers.js v6连接MetaMask? view函数Gas? tx.wait()? error 4902? contract.on监听?

🔧 动手实验

  1. 创建React项目+安装ethers.js
  2. 实现钱包连接+余额显示
  3. 连接合约+读写操作
  4. 添加事件监听实时更新
  5. 实现链切换和错误处理

📖 四、交易状态管理

function useTransaction() {
  const [status, setStatus] = useState('idle');
  // idle → pending → confirming → success/error

  const sendTx = async (contractFn) => {
    try {
      setStatus('pending');
      const tx = await contractFn();
      setStatus('confirming');
      const receipt = await tx.wait(2);  // 等2个确认
      setStatus('success');
      return receipt;
    } catch (err) {
      setStatus('error');
      if (err.code === 'ACTION_REJECTED') {
        console.log('用户拒绝交易');
      } else if (err.code === 'TRANSACTION_REPLACED') {
        console.log('交易被替换');
      }
      throw err;
    }
  };
  return { status, sendTx };
}

📖 五、Web3前端最佳实践

  1. 连接钱包后缓存provider/signer避免重复请求
  2. 监听chainChanged事件重新加载页面
  3. 监听accountsChanged事件更新UI状态
  4. 交易状态实时反馈给用户(loading/success/error)
  5. 处理用户拒绝交易的情况(code 4001)
  6. 使用React Query或SWR缓存链上数据
  7. 错误消息用户友好化(不直接显示原始error)
  8. 响应式设计适配移动端MetaMask

📖 六、常见陷阱与最佳实践

  1. ❌ 不检查window.ethereum是否存在就调用
  2. ❌ 不处理用户拒绝连接钱包
  3. ❌ 不处理链切换导致的合约地址错误
  4. ❌ view函数调用提示用户付Gas(应该免费)
  5. ❌ 事件监听不清理导致内存泄漏
  6. ✅ 使用wagmi+viem替代原生ethers.js
  7. ✅ 使用RainbowKit简化钱包连接

📖 七、Wagmi + Viem现代方案

// 推荐使用wagmi+viem替代原生ethers.js
import { createConfig, http } from 'wagmi';
import { sepolia } from 'wagmi/chains';
import { useAccount, useConnect, useDisconnect } from 'wagmi';
import { injected } from 'wagmi/connectors';

const config = createConfig({
  chains: [sepolia],
  transports: { [sepolia.id]: http() },
});

function ConnectWallet() {
  const { address } = useAccount();
  const { connect } = useConnect();
  const { disconnect } = useDisconnect();

  if (address) return (
    <div>
      <p>{address.slice(0,6)}...{address.slice(-4)}</p>
      <button onClick={() => disconnect()}>Disconnect</button>
    </div>
  );
  return <button onClick={() => connect({ connector: injected() })}>Connect</button>;
}

📖 八、合约读写Hook封装

import { useReadContract, useWriteContract } from 'wagmi';
import { parseEther } from 'viem';

const ABI = [
  "function balanceOf(address) view returns (uint256)",
  "function transfer(address to, uint256 amount) returns (bool)"
];

function useToken(ADDRESS, userAddress) {
  const { data: balance } = useReadContract({
    address: ADDRESS, abi: ABI,
    functionName: 'balanceOf', args: [userAddress],
  });

  const { writeContract } = useWriteContract();

  const transfer = (to, amount) => {
    writeContract({
      address: ADDRESS, abi: ABI,
      functionName: 'transfer',
      args: [to, parseEther(amount)],
    });
  };

  return { balance, transfer };
}

📖 九、WalletConnect集成

import { EthereumClient, w3mConnectors, w3mProvider } from '@web3modal/ethereum'
import { Web3Modal } from '@web3modal/html'
import { configureChains, createConfig } from 'wagmi'
import { sepolia, mainnet } from 'wagmi/chains'

// WalletConnect配置
const projectId = 'YOUR_WALLETCONNECT_PROJECT_ID'
const chains = [mainnet, sepolia]

const { publicClient } = configureChains(chains, [
  w3mProvider({ projectId })
])

const wagmiConfig = createConfig({
  autoConnect: true,
  connectors: w3mConnectors({ projectId, chains }),
  publicClient,
})

const ethereumClient = new EthereumClient(wagmiConfig, chains)
const web3modal = new Web3Modal({ projectId }, ethereumClient)

// ✅ 支持移动端钱包连接(扫码/深度链接)

📖 十、DApp性能优化

  1. 使用多调用(Multicall)批量查询减少RPC请求
  2. 缓存链上数据减少重复请求
  3. 使用SWR/React Query自动缓存和重新验证
  4. 延迟加载非关键数据
  5. 使用WebSocket订阅替代轮询
  6. 图片和元数据使用CDN缓存

📖 十一、Web3前端调试

调试技巧:
• MetaMask → 设置 → 高级 → 显示测试网络
• 检查window.ethereum是否可用
• 使用etherscan查看交易详情和revert原因
• 使用tenderly.co模拟和调试失败交易
• React DevTools检查组件状态和props
• 浏览器Console查看ethers.js错误详情
• 使用hardhat console交互式测试合约

📖 十二、Web3状态管理

// 使用zustand管理Web3全局状态
import { create } from 'zustand';

const useWeb3Store = create((set) => ({
  address: null,
  chainId: null,
  provider: null,
  signer: null,
  connect: async () => {
    const provider = new ethers.BrowserProvider(window.ethereum);
    const signer = await provider.getSigner();
    const address = await signer.getAddress();
    const network = await provider.getNetwork();
    set({ provider, signer, address, chainId: Number(network.chainId) });
  },
  disconnect: () => set({ address: null, chainId: null, provider: null, signer: null }),
}));

function useContract(address, abi) {
  const { signer, provider } = useWeb3Store();
  return useMemo(() => {
    if (!provider) return null;
    return new ethers.Contract(address, abi, signer || provider);
  }, [address, signer, provider]);
}

📖 十三、DApp安全最佳实践

前端安全检查清单:
1. ✅ 始终验证合约地址(防钓鱼)
2. ✅ 检查交易参数再签名(防恶意授权)
3. ✅ 使用EIP-712结构化签名(防签名钓鱼)
4. ✅ 限制approve额度(不要无限授权)
5. ✅ 检查connected chain是否正确
6. ✅ 监控可疑合约交互
7. ✅ 实现交易模拟(Tenderly)
8. ✅ 教育用户识别常见骗局

📖 十四、Web3用户体验优化

UX改进方向:
• 社交登录→钱包(账户抽象)
• 法币入金→加密(Stripe/MoonPay)
• Gas代付(用户无感)
• 批量交易(一个签名多操作)
• 预估交易结果(模拟)
• 清晰的错误提示
• 交易进度实时反馈

📖 十五、Web3前端框架选择

框架优点缺点
Next.js + wagmiSSR/SSG,SEO友好配置复杂
Vite + RainbowKit极速开发,简单无SSR
Remix + getRoute全栈Web3生态较小
React Native移动端DApp钱包集成困难
🎨

DApp构建者

你已掌握DApp前端——ethers.js/钱包连接/事件监听!

✅ ethers.js ✅ 钱包连接 ✅ 合约交互 ✅ 事件监听

📋 课程目录