技术解读
viem:给以太坊套上一层真正类型安全的 TypeScript 外衣
项目背景与痛点
在 viem 出现之前,前端与以太坊节点打交道基本只有两条路:ethers.js 或 web3.js。它们都不差,但都背着一个时代的包袱——诞生于浏览器端 DApp 的黄金期,设计取向是“面向对象友好”:Provider、Signer、Contract 三个实例把账户、网络、ABI 全部绑死在一起。
这在 2021 年之后开始暴露问题。Next.js 的 Server Components 里,一个有状态的 Provider 单例既不能被序列化,也没法在多请求间安全复用;new Contract(address, abi, signer) 把 ABI 固化在实例上,打包器无法做 tree-shaking,一个只想读余额的应用照样拖进整套编解码器;最要命的是类型——contract.transfer(to, amount) 的参数在 ethers 里几乎是 any[],拼错函数名要等运行时才报错,参数顺序写反只能靠人工 review 兜底。
viem 的核心主张可以概括成一句话:把以太坊交互从“对象”还变成“函数 + 类型”。无状态、可组合、按需加载,并且让 ABI 在编译期就发挥作用。
核心技术揭秘
viem 的架构是清晰的三层加两个横切层:Actions 是业务语义层,Client Core 是编排层,Transport 是 IO 层;ox 原语层负责底层编解码,类型层则贯穿始终。
Actions 层的设计是整库的灵魂。 每个 action 本质上是 (client, params) => Promise<T>,createPublicClient 返回的对象只是把这些函数做了部分应用,于是 readContract({...}) 变成 client.readContract({...})。这不是语法糖,而是刻意保留了两条调用路径:你可以只导入 readContract 函数配合任意 client,也可以走绑定后的方法。client.extend(customActions) 让第三方无缝注入能力——wagmi 的 React hooks 正是通过这一入口挂上去的,viem 本身对 React 一无所知。组合优于继承,在这里体现得很彻底。
类型推导是它最难被复制的部分。 viem 用条件类型把 ABI 的 JSON 结构映射成 TypeScript 类型:functionName 被收窄成 ABI 里所有函数名的联合类型,选定函数后 args 被推导成对应 tuple,返回值由 outputs 推出。嵌套 tuple、uint256 到 bigint、address 到模板字面量类型 0x${string} 的映射,全靠一层层递归的条件类型完成。viem 2.x 强依赖 TypeScript 5.0 的 const 类型参数,否则字面量 ABI 会被推宽成 string,整条链路失效。
编解码交给 ox。 viem 把 Hex、Address、RLP、Transaction 这类原语下沉到同团队维护的 ox 库。一个容易被忽视的工程决策是:整个调用链以 hex 字符串作为唯一中间态,不做 BigInt ↔ Number 的来回转换。大整数只在需要业务计算的边界上转一次,这让高频 eth_call 场景下的序列化开销显著下降。
功能亮点与差异
编译期 ABI 校验是它最直接的杀手锏,写错函数名、漏传参数、类型不匹配都会在 IDE 里变红,这在 ethers 时代是奢望。
体积与运行时兼容性同样关键。无状态设计意味着它能跑在 Cloudflare Workers、Deno、Bun、React Server Components 里而不需要任何 polyfill 包装;模块化导出让打包器能真正砍掉未使用的 actions。
内置批处理值得一提。client.batch.multicall 把同一区块内的多个读请求聚合成一次 eth_call,对需要拉取大量状态的看板类应用几乎是刚需,而这个能力在 ethers 里要靠额外的 Multicall 合约封装。
结构化错误体系被低估了。viem 把 RPC 返回的原始错误按 EIP-1474 和合约 revert 原因解析成 BaseError 的子类树,ContractFunctionRevertedError 能直接拿到 revert 的 reason 和自定义 error 名,而不是让你去正则匹配 "execution reverted"。
对新提案的跟进速度构成生态优势:EIP-4844 blob 交易、EIP-7702 委托账户、EIP-5792 批量调用,viem 往往是第一批支持者,因为它离协议层足够近。
应用场景与落地建议
最适合的场景是 DApp 前端(配合 wagmi,几乎成了当下 React 生态的默认组合)、链上脚本与机器人(Node 环境下无需浏览器兼容层)、钱包与签名服务(细粒度控制 nonce、gas、交易序列化),以及索引器与监控(multicall 批处理收益明显)。
落地时有几个坑值得提前踩掉:
- 锁定版本。viem 迭代快,minor 版本有过行为变更,生产环境建议配合 lockfile 并关注 changelog。
- 别把 RPC 当无限资源。fallback transport 和批处理都很香,但批处理会把请求打到同一个节点,遇到限流反而更慢,建议按业务分组。
- 错误处理要抓子类。直接 catch
Error会丢掉 revert 原因和 RPC 错误码,instanceof判断BaseError家族才是正确姿势。 - 大 ABI 会拖慢 tsc。上百个方法的 ABI 做全量类型推导时编译时间明显上升,必要时用
as const收窄但按需拆分。 - JSON 序列化陷阱。
bigint无法直出 JSON,服务端返回给前端时要提前转字符串,否则会静默丢字段。
综合评价
优点:类型安全做到了同类库的顶点;无状态 + 模块化让它天然适配现代运行时;ox 底层带来扎实的性能与正确性;错误体系和对新 EIP 的跟进速度体现团队对协议的理解深度。
缺点:API 比 ethers 啰嗦——每次调用都得传 abi 和 address,习惯了 contract.transfer() 的人会有落差;生态工具链(Hardhat 插件、各类 ABI 工具)仍以 ethers 为中心;类型魔法虽强,但报错信息在推导失败时相当晦涩,新手排查成本高;版本演进激进,长期维护需要持续跟进。
一句话结论:如果你在做 2024 年之后的新项目,viem 应该是默认选择;但请把它当成一个持续演进的协议接口,而不是一个稳定的黑盒依赖。
评论