上链ON-CHAIN BLOCK 20 / 26
卷四 · dApp 前端BLOCK 20

完整 dApp 实战


把前四卷学的东西拼成一个能跑的东西:一个链上留言板。合约存留言、前端连钱包读写、监听事件实时刷新。我们还会撞上一个真实世界的难题——"怎么高效查历史数据",并引出 dApp 架构里那个常被忽略但不可或缺的角色:索引器。

合约:一个链上留言板

Solidity · GuestBook.sol
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,只给人看的历史发事件。

前端:连接 + 写 + 读

TypeScript · viem 打通
// 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 端点):

TypeScript · 订阅新事件
const unwatch = client.watchContractEvent({
  address: GUESTBOOK, abi, eventName: 'NewMessage',
  onLogs: (logs) => {
    logs.forEach(l => prependMessage(l.args));  // 新留言实时插到顶部
  },
});
Android 类比

整个数据流和你在 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、自建索引等方案。关键认知是——它把"链上事件流"翻译成"你熟悉的数据库查询"

Android 类比

索引器就是你无比熟悉的后端 + 数据库那一层,只不过它的数据源不是用户请求,而是链上事件。链负责"可信的写入与真相",索引器负责"高效的读取与查询"。一个成熟 dApp 的架构因此是:合约(可信写)+ 索引器(高效读)+ 前端(展示)+ 钱包(身份)——是不是和你的三层架构又对上了?链只是替换了其中"可信状态"那一块。

一个 dApp 的完整架构

典型 dApp 架构
用户钱包(身份/签名)
      │
      ▼
前端(React + viem/wagmi)
   │ 写:发交易 ─────────────► 智能合约(可信状态,第三卷)
   │ 读:GraphQL 查询 ──────► 索引器 / The Graph ◄─监听事件─┘
   ▼
静态资源托管(Vercel/IPFS)
要点入账

一个完整 dApp:合约负责可信写入、只把该存的存 storage、其余发事件;前端用钱包签名做写、用事件/索引器做读;实时更新靠订阅事件。当查询变复杂,引入索引器(The Graph 等)把事件流变成可高效查询的数据库——它就是你熟悉的后端读取层。至此卷四完成,你能独立做出一个端到端 dApp 了。卷五,我们把战场搬回你的主场:Android 原生。

本章小结

◆ 返回目录