web3前端

钱包连接按钮点下去,MetaMask 弹窗一闪,用户签名,交易上链。整个过程不到十秒,但前端要处理的东西比传统 Web 应用多出整整一个维度——你得同时管好链上状态、链下状态,还有两者之间那段灰色地带。

传统前端里,用户登录就是发个请求,服务器返回 token,存起来完事。Web3 前端不行。用户的"登录"是连接钱包,地址就是身份,但链上没有 session 这个概念。刷新页面,钱包断了,你得重新连。用户切了网络,你的合约地址全变了。账户换了,之前加载的数据全部作废。这些在 Web2 里根本不存在的问题,在 Web3 前端是日常。

读链和写链是两回事

读数据用 provider 就够了,不需要用户签名,不需要 gas。比如查一个 ERC-20 代币余额,调 balanceOf,走的是节点 RPC,免费,快。写数据就完全不同——得用 signer,得让用户确认,得等矿工打包,还得处理失败重试。

很多人刚开始写 dApp 的时候会把这两件事混在一起。一个 useEffect 里又读又写,结果用户还没连钱包呢,代码就去调 contract.connect(signer),直接报错。正确的做法是读操作和写操作走不同的 hook 或 service 层,读的部分只依赖 provider,写的部分才需要 signer。

状态同步是个泥潭

假设你在做一个 DEX 界面。用户发起一笔 swap,交易哈希拿到了,但这时候交易还在 mempool 里。你的 UI 该显示什么?"待确认"?那多久算超时?区块重组了怎么办?

我见过不少项目直接用 setTimeout 轮询 eth_getTransactionReceipt,每两秒查一次,查到 status: 1 就更新 UI。简单粗暴,但问题一堆:RPC 节点可能限流,用户可能切了网络,交易可能被替换(同样 nonce 发了新交易)。

更靠谱的做法是监听 provider.on('block'),每个新块检查一次 pending 交易的状态。但这也只是好一点——L2 上的排序器出块时间和 L1 完全不同,Polygon 和 Arbitrum 的确认逻辑也不一样。前端要适配这些差异,工作量不小。

ABI 和类型安全

合约 ABI 是一坨 JSON。你把它丢进前端,调方法的时候参数类型全靠脑记。传错一个 uint256 的位数,交易直接 revert,gas 白花。

TypeChain 这类工具能从 ABI 生成 TypeScript 类型,调用 contract.transfer(to, amount) 的时候 IDE 会告诉你 to 是 string,amount 是 BigNumber。这玩意刚上手觉得麻烦,用惯了再回去写裸 ABI 调用,跟裸奔一样。

wagmi 和 viem 的组合现在基本是标配了。wagmi 管钱包连接和合约交互的 hook,viem 管底层编码和 RPC 调用。两个库都是 TypeScript 优先,类型推导做得相当到位。对比早年的 web3.js,体验提升不是一点半点。

Gas 估算和用户体验

用户点"确认"之前,你得告诉他大概要花多少 gas。但 gas 估算经常不准——合约逻辑有分支,不同分支消耗不同;网络拥堵时 base fee 波动剧烈。

EIP-1559 之后,交易有 maxFeePerGas 和 maxPriorityFeePerGas 两个参数。前端得根据当前 base fee 给用户一个合理建议。给太低,交易卡住;给太高,用户觉得你在坑他。

有些钱包(比如 Rainbow)会在前端做模拟执行,提前告诉用户这笔交易会成功还是失败。这背后是 eth_call 加状态覆盖,技术上是可行的,但实现成本不低。大部分 dApp 还是靠"估算 gas + 让用户自己调"这套老办法。

Web3 前端说到底就是在不确定的环境里给用户确定性。链上状态随时在变,RPC 节点可能撒谎,钱包行为不可控。你能做的就是尽量把边界情况处理好,让用户点下按钮之后,知道发生了什么。