Web3前端开发:使用ethers.js监听智能合约事件

15 阅读1分钟

Web3前端开发:使用ethers.js监听智能合约事件

前言

在Web3开发中,实时获取区块链上的状态变化是构建交互式DApp的关键。传统的前端轮询方式不仅效率低下,还会消耗大量API调用。本文将分享我在一个NFT铸造项目中,如何从轮询优化为事件监听,实现实时更新的完整踩坑记录。

为什么需要事件监听?

在以太坊生态中,智能合约通过事件(Events)向外广播状态变化。与轮询相比,事件监听具有以下优势:

  1. 实时性:事件触发后立即通知前端
  2. 高效性:减少不必要的RPC调用
  3. 可靠性:不会错过任何状态变化
  4. 节省成本:减少API调用次数

基础实现:轮询方式

// 传统轮询方式 - 不推荐
async function pollNFTBalance(userAddress, contract) {
  setInterval(async () => {
    const balance = await contract.balanceOf(userAddress);
    updateUI(balance);
  }, 5000); // 每5秒查询一次
}

这种方式的问题很明显:延迟高、API调用频繁、用户体验差。

优化方案:ethers.js事件监听

1. 基础事件监听

import { ethers } from 'ethers';

// 连接合约
const provider = new ethers.JsonRpcProvider('https://mainnet.infura.io/v3/YOUR_KEY');
const contract = new ethers.Contract(
  CONTRACT_ADDRESS,
  NFT_ABI,
  provider
);

// 监听Transfer事件
contract.on('Transfer', (from, to, tokenId, event) => {
  console.log(`NFT #${tokenId}${from} 转移到 ${to}`);
  updateNFTList(tokenId, to);
});

2. 过滤特定地址的事件

// 只监听与用户相关的事件
const filter = contract.filters.Transfer(null, userAddress);
contract.on(filter, (from, to, tokenId, event) => {
  console.log(`用户收到 NFT #${tokenId}`);
  addToUserCollection(tokenId);
});

3. 处理历史事件

// 获取过去24小时的事件
async function getPastEvents() {
  const blockNumber = await provider.getBlockNumber();
  const fromBlock = blockNumber - 5760; // 大约24小时前的区块
  
  const events = await contract.queryFilter('Transfer', fromBlock, blockNumber);
  events.forEach(event => {
    console.log(`历史事件: ${event.args.tokenId}`);
  });
}

实战踩坑记录

坑1:事件重复触发

问题:同一个事件被监听到多次 原因:重新连接合约时没有移除旧监听器 解决方案

let eventListeners = [];

function setupEventListeners() {
  // 先移除所有旧监听器
  eventListeners.forEach(listener => contract.off(listener));
  eventListeners = [];
  
  // 添加新监听器
  const transferListener = (from, to, tokenId) => {
    console.log(`Transfer: ${tokenId}`);
  };
  contract.on('Transfer', transferListener);
  eventListeners.push(['Transfer', transferListener]);
}

坑2:内存泄漏

问题:页面切换后监听器未清理,导致内存占用持续增长 解决方案

// 组件卸载时清理
useEffect(() => {
  const transferListener = (from, to, tokenId) => {
    // 处理事件
  };
  
  contract.on('Transfer', transferListener);
  
  return () => {
    contract.off('Transfer', transferListener);
  };
}, []);

坑3:网络切换处理

问题:用户切换网络(如从主网切换到测试网)后事件监听失效 解决方案

// 监听网络变化
provider.on('network', (newNetwork, oldNetwork) => {
  if (oldNetwork) {
    // 网络变化,重新连接合约
    setupEventListeners();
  }
});

高级技巧

1. 批量处理事件

// 使用防抖避免频繁UI更新
let eventQueue = [];
let processing = false;

contract.on('Transfer', async (from, to, tokenId) => {
  eventQueue.push({ from, to, tokenId });
  
  if (!processing) {
    processing = true;
    setTimeout(processEvents, 1000); // 1秒后批量处理
  }
});

async function processEvents() {
  if (eventQueue.length === 0) {
    processing = false;
    return;
  }
  
  const batch = [...eventQueue];
  eventQueue = [];
  
  // 批量更新UI
  await updateUIBatch(batch);
  
  processing = false;
}

2. 错误处理与重连

function setupEventListenersWithRetry() {
  try {
    contract.on('Transfer', handleTransfer);
    
    // 监听错误
    contract.on('error', (error) => {
      console.error('事件监听错误:', error);
      setTimeout(setupEventListenersWithRetry, 5000); // 5秒后重试
    });
  } catch (error) {
    console.error('设置监听器失败:', error);
    setTimeout(setupEventListenersWithRetry, 5000);
  }
}

性能优化建议

  1. 按需监听:只监听用户相关的事件
  2. 使用过滤器:减少不必要的事件处理
  3. 批量更新:避免频繁的UI重绘
  4. 清理机制:及时移除不需要的监听器
  5. 错误边界:添加适当的错误处理和重试机制

完整示例代码

import { ethers } from 'ethers';
import { useEffect, useRef } from 'react';

function useNFTEventListeners(contract, userAddress) {
  const listenersRef = useRef([]);
  
  useEffect(() => {
    if (!contract || !userAddress) return;
    
    const setupListeners = () => {
      // 清理旧监听器
      listenersRef.current.forEach(([event, listener]) => {
        contract.off(event, listener);
      });
      listenersRef.current = [];
      
      // 监听用户收到的NFT
      const receivedFilter = contract.filters.Transfer(null, userAddress);
      const receivedListener = (from, to, tokenId) => {
        console.log(`收到 NFT #${tokenId}`);
        addToCollection(tokenId);
      };
      contract.on(receivedFilter, receivedListener);
      listenersRef.current.push([receivedFilter, receivedListener]);
      
      // 监听用户发送的NFT
      const sentFilter = contract.filters.Transfer(userAddress, null);
      const sentListener = (from, to, tokenId) => {
        console.log(`发送 NFT #${tokenId}`);
        removeFromCollection(tokenId);
      };
      contract.on(sentFilter, sentListener);
      listenersRef.current.push([sentFilter, sentListener]);
    };
    
    setupListeners();
    
    return () => {
      listenersRef.current.forEach(([event, listener]) => {
        contract.off(event, listener);
      });
    };
  }, [contract, userAddress]);
}

总结

从轮询到事件监听,不仅仅是技术方案的改变,更是对Web3开发理念的深入理解。通过合理使用ethers.js的事件监听功能,我们可以:

  1. 构建更实时的用户体验
  2. 显著降低API调用成本
  3. 提高应用的整体性能
  4. 减少服务器压力

希望本文的踩坑经验能帮助你在Web3开发中少走弯路。记住,好的事件监听策略是构建优秀DApp的基石。

下一步

  1. 尝试使用ethers.js的contract.once()方法处理一次性事件
  2. 探索使用The Graph等索引服务替代复杂的事件监听
  3. 考虑使用WebSocket提供商(如Alchemy)获得更好的实时性

Happy building! 🚀