在上一篇文章中,我们搭建了一个完整的钱包集成流程,并且实现了对 ValueStore 合约的读写操作。但是,仅仅通过手动点击按钮去查询链上状态,并不够“实时”。真正的 DApp 通常需要主动监听链上事件,第一时间更新 UI,同时后端也需要索引这些事件来构建历史记录和数据分析。
今天这篇教程,我们就以 ValueStore 合约的 ValueChanged 事件为例,一步步完成:
- 前端使用
useWatchContractEvent实时接收事件并更新页面 - 后端使用
viem的watchContractEvent将事件持久化到 SQLite - 前端通过 REST API 查询历史事件,实现数据回放
全文代码可直接运行,你可以在上一篇项目的基础上继续添加。
1. 为什么需要监听合约事件
智能合约的 event 是日志(Log)的一种高层次封装。每当合约状态发生重要变更(比如转账、参数修改),就会触发相应的事件。监听事件有三大好处:
- 实时性:不用轮询,一旦上链就能立刻知道。
- 完整性:可以回溯所有历史事件,构建可靠的数据索引。
- 解耦:前端/后端只关心事件,不需要知道触发交易的具体逻辑。
我们将使用 ValueChanged(uint256 newValue) 事件,它由 setValue 触发,携带新值。
2. 前端:实时监听事件
2.1 回顾合约与 ABI
确保你的 frontend/src/contracts/valueStore.ts 包含事件定义(上一篇已经加了):
export const VALUE_STORE_ADDRESS = '0xYourContractAddress'
export const VALUE_STORE_ABI = [
// ... 之前的函数定义
{
anonymous: false,
inputs: [{ indexed: false, name: 'newValue', type: 'uint256' }],
name: 'ValueChanged',
type: 'event',
},
] as const
2.2 使用 useWatchContractEvent
wagmi 提供了 useWatchContractEvent Hook,它底层封装了 viem 的 watchContractEvent,使用起来非常方便。我们创建一个新组件 EventWatcher.tsx:
import { useWatchContractEvent, useAccount } from 'wagmi'
import { useState } from 'react'
import { VALUE_STORE_ADDRESS, VALUE_STORE_ABI } from './contracts/valueStore'
import { parseAbiItem } from 'viem'
function EventWatcher() {
const [latestValue, setLatestValue] = useState<string>('')
const [logs, setLogs] = useState<{ newValue: string; txHash: string }[]>([])
useWatchContractEvent({
address: VALUE_STORE_ADDRESS,
abi: VALUE_STORE_ABI,
eventName: 'ValueChanged',
onLogs(logs) {
for (const log of logs) {
// log.args 里包含事件参数
const newValue = log.args.newValue?.toString() ?? '0'
setLatestValue(newValue)
setLogs((prev) => [
{ newValue, txHash: log.transactionHash },
...prev,
])
}
},
})
return (
<div>
<h3>📡 实时事件监听</h3>
<p>最新值:{latestValue || '暂无'}</p>
<ul>
{logs.slice(0, 5).map((log, i) => (
<li key={i}>
新值: {log.newValue} | 交易: {log.transactionHash.slice(0, 10)}...
</li>
))}
</ul>
</div>
)
}
export default EventWatcher
useWatchContractEvent会在组件挂载后开始监听新产生的区块,一旦匹配到指定事件,onLogs回调就会被触发。它默认监听从当前区块开始的新日志,非常适合实时 UI 更新。
2.3 集成到 App
在 App.tsx 中引入 EventWatcher,放在 ContractData 组件旁边:
import EventWatcher from './EventWatcher'
// ...
{isConnected && (
<>
<ContractData />
<SetValue />
<EventWatcher />
</>
)}
现在,当你用 setValue 修改合约值后,EventWatcher 会几乎同步显示新的值,并且列出最近的事件记录。
3. 后端:事件索引与持久化
前端实时监听很酷,但页面上只能看到监听之后的新事件,历史记录无法获取。我们需要一个后端事件监听器,把每次事件都存进数据库。
3.1 初始化后端项目
我们复用上一篇的 backend 项目,如果没有则需要先创建。安装额外依赖:
cd backend
pnpm add better-sqlite3 viem
pnpm add -D @types/better-sqlite3
3.2 创建数据库与事件存储
新建 backend/src/db.ts:
import Database from 'better-sqlite3'
const db = new Database('events.db')
db.exec(`
CREATE TABLE IF NOT EXISTS value_changed_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
blockNumber INTEGER NOT NULL,
transactionHash TEXT NOT NULL,
newValue TEXT NOT NULL,
timestamp INTEGER NOT NULL
)
`)
export function insertValueChangedEvent(
blockNumber: bigint,
transactionHash: string,
newValue: bigint,
timestamp: number
) {
const stmt = db.prepare(`
INSERT INTO value_changed_events (blockNumber, transactionHash, newValue, timestamp)
VALUES (?, ?, ?, ?)
`)
stmt.run(Number(blockNumber), transactionHash, newValue.toString(), timestamp)
}
export function getAllEvents(limit = 50) {
return db.prepare('SELECT * FROM value_changed_events ORDER BY blockNumber DESC LIMIT ?').all(limit)
}
3.3 编写事件监听器
新建 backend/src/eventListener.ts:
import { createPublicClient, http, parseAbiItem } from 'viem'
import { sepolia } from 'viem/chains'
import { VALUE_STORE_ADDRESS, VALUE_STORE_ABI } from './contracts' // 需共享 ABI 和地址
import { insertValueChangedEvent } from './db'
// 建议将合约配置提取到共享模块,这里假设已经存在
const client = createPublicClient({
chain: sepolia,
transport: http(),
})
export function startEventListener() {
console.log('启动事件监听...')
client.watchContractEvent({
address: VALUE_STORE_ADDRESS,
abi: VALUE_STORE_ABI,
eventName: 'ValueChanged',
onLogs(logs) {
for (const log of logs) {
const { blockNumber, transactionHash, args } = log
if (!args.newValue) continue
insertValueChangedEvent(
blockNumber,
transactionHash,
args.newValue,
Math.floor(Date.now() / 1000)
)
console.log(`已存储事件:${transactionHash} -> ${args.newValue}`)
}
},
})
}
这里我们用
client.watchContractEvent代替前端版本,因为它不依赖 React,可以直接在 Node 环境长期运行。
3.4 提供查询 API
在 backend/src/index.ts 中加入获取历史事件的接口:
import { getAllEvents } from './db'
import { startEventListener } from './eventListener'
// ... 原有代码 ...
app.get('/events', (req, res) => {
const events = getAllEvents()
res.json(events)
})
// 启动事件监听器(只启动一次)
startEventListener()
app.listen(3001, () => {
console.log('后端运行在 http://localhost:3001')
})
3.5 共享合约配置
为了让前端和后端共享 VALUE_STORE_ADDRESS 和 VALUE_STORE_ABI,最好把它们放到一个公共包里。简单的做法是:在项目根目录创建 packages/contracts 并导出,或者直接在 backend 里复制一份。这里为了简便,我们在 backend/src/contracts.ts 中复制相同的配置:
export const VALUE_STORE_ADDRESS = '0xYourContractAddress'
export const VALUE_STORE_ABI = [...] // 与前端保持一致
4. 前端集成历史事件接口
现在前端可以通过 API 获取所有历史事件,补全数据展示。
4.1 创建历史事件组件
import { useEffect, useState } from 'react'
interface EventRecord {
blockNumber: number
transactionHash: string
newValue: string
timestamp: number
}
function EventHistory() {
const [events, setEvents] = useState<EventRecord[]>([])
useEffect(() => {
fetch('http://localhost:3001/events')
.then(res => res.json())
.then(setEvents)
}, [])
return (
<div>
<h3> 历史事件(来自后端)</h3>
<ul>
{events.map((e) => (
<li key={e.transactionHash}>
块 {e.blockNumber} - 值 {e.newValue} ({new Date(e.timestamp * 1000).toLocaleString()})
</li>
))}
</ul>
</div>
)
}
export default EventHistory
4.2 添加到页面
在 App.tsx 中引入:
import EventHistory from './EventHistory'
// ...
<EventHistory />
现在,前端同时拥有了实时推送(EventWatcher)和历史回放(EventHistory),体验提升了一个档次。
5. 运行与测试
- 启动后端:
cd backend && npx tsx src/index.ts - 启动前端:
cd frontend && pnpm dev - 打开 DApp,连接钱包,执行一次
setValue操作。 - 观察前端实时显示新值,刷新页面后历史事件依然可见。
6. 优化与生产建议
- 批量拉取历史:如果事件量很大,应该分页并实现懒加载。
- WebSocket 推送:可以用 Socket.IO 将后端监听到的事件实时推送到前端,取代前端的直接监听,统一数据源。
- 错误重连:
watchContractEvent在 WebSocket 断开时会自动重试,但在 Node 环境中如果网络不稳定可以加上手动重试逻辑。 - 索引器服务:对于复杂业务,推荐使用 The Graph、Subsquid 或自建 Envio 索引器,它们提供了更强大的查询能力。
- 安全性:API 接口如果需要认证,记得加上上一篇实现的 JWT 验证。
7. 总结
通过这篇文章,你学会了如何在 Web3 全栈应用中处理合约事件:
- 前端:使用
useWatchContractEvent实时捕获链上日志,更新 UI。 - 后端:用
viem建立独立的事件监听进程,将数据持久化到 SQLite,并提供查询接口。 - 数据流:实现了“链上事件 → 后端数据库 → 前端展示”的完整闭环。