完整 dApp 实战
把前四卷学的东西拼成一个能跑的东西:一个链上留言板。合约存留言、前端连钱包读写、监听事件实时刷新。我们还会撞上一个真实世界的难题——"怎么高效查历史数据",并引出 dApp 架构里那个常被忽略但不可或缺的角色:索引器。
合约:一个链上留言板
contract GuestBook { event NewMessage(address indexed author, string text, uint256 time); uint256 public count; function post(string calldata text) external { require(bytes(text).length <= 280, "too long"); count++; // 只发事件,不把每条留言存进 storage —— 省 gas! emit NewMessage(msg.sender, text, block.timestamp); } }
注意这个设计决策(呼应第 12 章):留言历史只 emit 事件,不进 storage。因为合约逻辑本身不需要回看历史留言,前端要显示历史?去读日志。这样每条留言的 gas 成本从"存一大段字符串到 storage(极贵)"降到"emit 一个事件(便宜得多)"。这是 dApp 常见的省 gas 模式:合约要用的数据上 storage,只给人看的历史发事件。
前端:连接 + 写 + 读
// 1) 连接钱包(第 19 章) const [account] = await wallet.requestAddresses(); // 2) 发一条留言(写,弹钱包花 gas) const hash = await wallet.writeContract({ account, address: GUESTBOOK, abi, functionName: 'post', args: ['gm, on-chain world'], }); await client.waitForTransactionReceipt({ hash }); // 等确认(第 4 章) // 3) 读历史留言 = 查事件日志(第 12 章) const logs = await client.getContractEvents({ address: GUESTBOOK, abi, eventName: 'NewMessage', fromBlock: 'earliest', }); // logs 里每条有 author / text / time,渲染成列表
实时刷新:监听事件
要让别人的新留言自动出现,订阅事件(通常走 WebSocket 端点):
const unwatch = client.watchContractEvent({ address: GUESTBOOK, abi, eventName: 'NewMessage', onLogs: (logs) => { logs.forEach(l => prependMessage(l.args)); // 新留言实时插到顶部 }, });
整个数据流和你在 Android 里用 Flow/LiveData 观察数据源、自动刷新 UI 是一回事:getContractEvents 是"拉一次历史"(≈ 首次加载),watchContractEvent 是"订阅增量"(≈ collect 一个持续的 Flow)。合约的 event(第 12 章)就是那个可被观察的数据源。你已有的响应式 UI 直觉在这里直接复用。
真实痛点:查历史很难
上面 fromBlock: 'earliest' 扫全链听着美好,现实很骨感:直接从节点查大范围日志又慢又常被限流,更别提"按作者分组""按时间排序""分页"这类查询,RPC 根本做不到。这就是为什么严肃 dApp 几乎都有一个额外组件——索引器(indexer)。
| 需求 | 直接查 RPC | 用索引器 |
|---|---|---|
| 拿最新几条 | 勉强能 | 轻松 |
| 按字段过滤/排序/分页 | 基本做不到 | 擅长(它把事件灌进数据库) |
| 跨合约聚合 | 很痛苦 | 可以 |
| 响应速度 | 慢、易限流 | 快(查的是它的库) |
The Graph 与索引器
索引器持续监听链上事件、解析后写进传统数据库,再对外提供 GraphQL / REST 查询。The Graph 是最知名的去中心化索引协议:你写一个 "subgraph" 描述"关心哪些合约的哪些事件、怎么存",它就帮你把链上事件变成可高效查询的 API。也有 Ponder、自建索引等方案。关键认知是——它把"链上事件流"翻译成"你熟悉的数据库查询"。
索引器就是你无比熟悉的后端 + 数据库那一层,只不过它的数据源不是用户请求,而是链上事件。链负责"可信的写入与真相",索引器负责"高效的读取与查询"。一个成熟 dApp 的架构因此是:合约(可信写)+ 索引器(高效读)+ 前端(展示)+ 钱包(身份)——是不是和你的三层架构又对上了?链只是替换了其中"可信状态"那一块。
一个 dApp 的完整架构
用户钱包(身份/签名)
│
▼
前端(React + viem/wagmi)
│ 写:发交易 ─────────────► 智能合约(可信状态,第三卷)
│ 读:GraphQL 查询 ──────► 索引器 / The Graph ◄─监听事件─┘
▼
静态资源托管(Vercel/IPFS)
一个完整 dApp:合约负责可信写入、只把该存的存 storage、其余发事件;前端用钱包签名做写、用事件/索引器做读;实时更新靠订阅事件。当查询变复杂,引入索引器(The Graph 等)把事件流变成可高效查询的数据库——它就是你熟悉的后端读取层。至此卷四完成,你能独立做出一个端到端 dApp 了。卷五,我们把战场搬回你的主场:Android 原生。
本章小结
- 省 gas 模式:合约逻辑要用的数据进 storage,纯历史记录发事件供链下读取。
- 前端流程:连钱包 → writeContract 发交易并等确认 → getContractEvents 读历史 → watchContractEvent 实时订阅。
- 直接用 RPC 查历史又慢又弱,复杂查询需要索引器。
- The Graph 等把事件流索引成可 GraphQL 查询的 API;成熟 dApp = 合约(写)+ 索引器(读)+ 前端 + 钱包。