2、Web3.js入门:库安装、Provider配置与事件监听

好,咱们进入实战环节。Web3.js 是连接前端和以太坊的桥梁。说白了,它就是一套 JavaScript 工具包,让你能在浏览器或 Node.js 里跟链上交互。

我个人习惯把 Web3.js 比作「遥控器」。你不需要知道电视内部怎么工作的,按按钮就行。Web3.js 就是那个按钮,帮你调用合约、监听事件、查询余额。

2.1 安装 Web3.js

安装很简单,一行命令搞定。但我建议你注意版本号,不同版本 API 有差异。

npm install web3
# 或者用 yarn
yarn add web3

我在项目中遇到过一个问题:直接装最新版,结果发现有些旧合约的 ABI 格式不兼容。后来我习惯锁定版本:

npm install web3@1.10.0

嗯,这里要注意。Web3.js 1.x 和 2.x 的 Provider 配置方式不太一样。咱们课程基于 1.x 版本,因为目前生产环境用得最多。

2.2 Provider 配置:连接以太坊节点

光装库没用,你得连上一个节点。节点就是以太坊的「入口」。你可以自己跑节点,但太麻烦。一般用 Infura 或 Alchemy 这类第三方服务。

为什么用它们?你想想看,自己维护一个以太坊全节点,硬盘要几个 TB,同步要几天。Infura 和 Alchemy 帮你做了这些脏活累活。

2.2.1 使用 Infura

先去 infura.io 注册,创建一个项目,拿到你的 Project ID。然后这样配置:

const Web3 = require('web3');

// Infura 的 RPC 地址
const infuraUrl = 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID';
const web3 = new Web3(new Web3.providers.HttpProvider(infuraUrl));

// 测试连接
web3.eth.net.isListening()
  .then(() => console.log('✅ 连接成功'))
  .catch(e => console.error('❌ 连接失败', e));

我曾经犯过一个低级错误:把 Project ID 写死在代码里,结果不小心提交到了 GitHub。几分钟后,有人用我的额度去扫链,账单直接爆了。所以,永远不要把 API Key 硬编码。用环境变量:

const infuraUrl = `https://mainnet.infura.io/v3/${process.env.INFURA_PROJECT_ID}`;

2.2.2 使用 Alchemy

Alchemy 和 Infura 类似,但我觉得它的开发者体验更好。尤其是它的 Dashboard,能看到请求的详细日志,调试时特别方便。

const alchemyUrl = 'https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY';
const web3 = new Web3(new Web3.providers.HttpProvider(alchemyUrl));

两者选哪个?我个人偏好 Alchemy,因为它的免费额度更慷慨,而且 WebSocket 支持更稳定。但 Infura 用户基数大,社区资源多。你都可以试试。

2.2.3 HTTP vs WebSocket Provider

这里有个关键区别。HTTP Provider 只能做「请求-响应」式的调用。比如查询余额、调用合约方法。但如果你想实时监听事件,必须用 WebSocket Provider。

为什么?因为事件是链上主动推给你的,不是你去拉的。HTTP 是单向的,你问一句它答一句。WebSocket 是双向的,链上可以主动通知你。

// WebSocket Provider 配置
const wsUrl = 'wss://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY';
const web3Ws = new Web3(new Web3.providers.WebsocketProvider(wsUrl));

// 注意:WebSocket 连接有超时机制
// 我建议加上重连逻辑
const provider = new Web3.providers.WebsocketProvider(wsUrl, {
  clientConfig: {
    keepalive: true,
    keepaliveInterval: 60000 // 60秒心跳
  },
  reconnect: {
    auto: true,
    delay: 5000, // 5秒后重连
    maxAttempts: 10
  }
});
⚠️ 避坑指南: 我曾经在生产环境只用 HTTP Provider 去监听事件,结果写了个轮询脚本每 3 秒查一次。不仅浪费流量,而且延迟高。后来换成 WebSocket,延迟从秒级降到毫秒级。记住:监听事件必须用 WebSocket

2.3 监听合约事件的 subscribe 方法

好,现在连上节点了,该监听事件了。Web3.js 提供了 subscribe 方法,专门用来订阅链上事件。

2.3.1 基本用法

假设你有一个 ERC-20 代币合约,它有个 Transfer 事件。你想实时监控所有转账:

// 合约地址和 ABI
const contractAddress = '0x...'; // 你的合约地址
const contractABI = [ /* 合约 ABI 数组 */ ];

const contract = new web3Ws.eth.Contract(contractABI, contractAddress);

// 订阅 Transfer 事件
const subscription = contract.events.Transfer({
  filter: {}, // 可以加过滤条件,比如只监听某个地址
  fromBlock: 'latest' // 从最新区块开始
})
.on('data', (event) => {
  console.log('📦 收到事件:', event);
  // event.returnValues 里包含事件参数
  console.log(`从 ${event.returnValues.from} 转账到 ${event.returnValues.to}`);
  console.log(`金额: ${event.returnValues.value}`);
})
.on('changed', (event) => {
  // 当交易被回滚时触发
  console.log('🔄 事件状态变更(可能被回滚):', event);
})
.on('error', (error) => {
  console.error('❌ 监听出错:', error);
});

// 取消订阅
// subscription.unsubscribe();

这里有个细节:fromBlock: 'latest' 表示只监听新产生的区块。如果你想从某个历史区块开始监听,可以指定区块号:fromBlock: 15000000

2.3.2 过滤事件

实际项目中,你往往不需要监听所有事件。比如你只关心某个地址的转账:

// 只监听 from 为特定地址的事件
const subscription = contract.events.Transfer({
  filter: {
    from: '0xYourAddressHere'
  },
  fromBlock: 'latest'
});

你想想看,如果不加过滤,一个热门合约每秒可能产生几十个事件。你的程序会疯掉的。所以,能用 filter 就用 filter

2.3.3 监听所有事件

有时候你想监听合约的所有事件,不管是什么类型。可以用 events.allEvents

const subscription = contract.events.allEvents({
  fromBlock: 'latest'
})
.on('data', (event) => {
  console.log('事件类型:', event.event);
  console.log('事件参数:', event.returnValues);
});

这个方法在调试时特别有用。我记得有一次合约升级后,某个事件突然不触发了。我用 allEvents 一监听,发现是事件名拼写错了。嗯,这种低级错误排查起来最费时间。

2.3.4 处理重连

WebSocket 连接不稳定是常态。网络抖动、节点重启、服务端断开,都可能导致连接中断。所以,必须实现重连机制

function subscribeWithReconnect(contract, eventName, callback) {
  let subscription;
  
  function start() {
    subscription = contract.events[eventName]({
      fromBlock: 'latest'
    })
    .on('data', callback)
    .on('error', (err) => {
      console.error('监听出错,5秒后重连...', err.message);
      setTimeout(start, 5000);
    });
  }
  
  start();
  
  // 返回取消订阅的函数
  return () => {
    if (subscription) subscription.unsubscribe();
  };
}

// 使用
const unsubscribe = subscribeWithReconnect(
  contract,
  'Transfer',
  (event) => {
    console.log('收到转账:', event.returnValues);
  }
);
💡 个人经验: 我在做一个 DeFi 监控系统时,发现重连后的事件会丢失。后来我加了一个「断点续传」机制:每次收到事件时记录区块号,重连时从上次的区块号 + 1 开始监听。这样就不会漏掉任何事件了。

2.4 实战:完整的事件监听脚本

把上面的知识点串起来,写一个完整的监听脚本:

const Web3 = require('web3');
require('dotenv').config();

// 配置
const WS_URL = process.env.ALCHEMY_WS_URL;
const CONTRACT_ADDRESS = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; // USDT 合约
const CONTRACT_ABI = [ /* USDT 的 ABI,包含 Transfer 事件 */ ];

// 初始化 WebSocket Provider
const provider = new Web3.providers.WebsocketProvider(WS_URL, {
  clientConfig: { keepalive: true, keepaliveInterval: 60000 },
  reconnect: { auto: true, delay: 5000, maxAttempts: 20 }
});

const web3 = new Web3(provider);
const contract = new web3.eth.Contract(CONTRACT_ABI, CONTRACT_ADDRESS);

console.log('🚀 开始监听 USDT 转账事件...');

// 订阅 Transfer 事件
const sub = contract.events.Transfer({
  filter: { from: '0x...' }, // 可选过滤
  fromBlock: 'latest'
});

sub.on('data', (event) => {
  const { from, to, value } = event.returnValues;
  console.log(`💸 ${from} → ${to} : ${web3.utils.fromWei(value, 'ether')} USDT`);
  console.log(`区块: ${event.blockNumber}, 交易: ${event.transactionHash}`);
});

sub.on('error', (err) => {
  console.error('监听出错:', err);
});

// 优雅退出
process.on('SIGINT', () => {
  console.log('\n👋 正在关闭监听...');
  sub.unsubscribe();
  provider.disconnect();
  process.exit();
});
核心要点回顾:
  • Web3.js 是前端与以太坊交互的 SDK,安装时注意版本锁定
  • Infura 和 Alchemy 是常用的节点服务商,推荐用 WebSocket 连接
  • 监听事件必须用 WebSocket Provider,HTTP 只能做轮询
  • subscribe 方法支持 filter 过滤,能减少不必要的数据处理
  • 生产环境必须实现重连机制,防止连接中断导致数据丢失

下一章咱们会深入解析事件日志的结构,看看 event.returnValues 背后到底藏着什么。到时候你会明白,为什么有时候事件参数会「对不上号」。