跳转到内容

viem — 现代 TypeScript EVM 库

待复核

viem 是 wevm 团队(也是 wagmi 作者)2023 年发布的新一代以太坊 JavaScript 客户端库。日常类比:像一台模块化拼装的电话总机——你不用买整套机柜,需要”打电话”才取一个话筒,需要”录音”再加一个录音机。每件配件单独发货、单独计费。

它把所有跟链的交互拆成 三件可插拔的零件

import { createPublicClient, http } from "viem";
import { mainnet } from "viem/chains";
import { getBalance } from "viem/actions";
const client = createPublicClient({ chain: mainnet, transport: http() });
const bal = await getBalance(client, { address: "0xd8dA...96045" });
  • Client(PublicClient / WalletClient / TestClient):负责”我是谁、我要干嘛”
  • Transport(http / webSocket / custom / fallback):负责”我怎么把字节送出去”
  • Chain:负责”我连的是哪条链”——内置 200+ EVM 链定义

完全用 TypeScript 写,从合约 ABI(合约说明书)字面量自动推导方法签名和返回类型;官方 README 量级约 ~35kb 压缩 bundle,wagmi 2.0(2024)默认底座

不理解 viem,下面这些事都没法解释:

  • 为什么 wagmi 2.x 教程不再 import { ethers } from "ethers"——底层从 ethers 切到了 viem
  • 为什么新 DApp 项目首选不是用了 8 年的 ethers 而是这个 2 岁多的库——bundle 小 4 倍 + 类型强很多
  • 为什么写 contract.read.balanceOf([addr]) 编辑器能直接提示出 bigint 返回类型——viem 把 ABI 当编译期信息推
  • 为什么”按 action 引入”(functional)会重新成为前端库主流——tree-shake + 强类型组合拳

viem 的设计可以拆成四个对照决策

  1. Client 三联 vs ethers Provider/Signer 二联:像把”前台接待 / 出纳签字 / 沙盘演练”拆成三个工位。PublicClient 只读、WalletClient 签名发交易、TestClient 本地调试,职责更细。

  2. Transport 抽象(组合)vs ethers 内嵌:Transport 像快递渠道——http / webSocket / fallback 可换可叠。fallback([http(a), http(b)]) 自动切节点,测试时也能塞 mock。

  3. Actions(函数)vs ethers Methods(OO):调链用 getBalance(client, args),不是挂在对象上的方法。像自助柜只取你点的那一格——没用到的 action 不进打包(tree-shake)。

  4. ABI 字面量类型推导:ABI 是合约的”菜单说明书”。写 const abi = [...] as const 后,编辑器自己推出 balanceOf 的入参/返回类型,整站不用手写方法签名。

四点合起来:bundle 小、类型强、可组合、可测试

案例 1:连主网读余额(最小例子)

Section titled “案例 1:连主网读余额(最小例子)”

目标:问主网”这个地址有多少 ETH”。

import { createPublicClient, http, formatEther } from "viem";
import { mainnet } from "viem/chains";
const client = createPublicClient({ chain: mainnet, transport: http() });
const balance = await client.getBalance({ address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" });
console.log(formatEther(balance), "ETH");

三步:① createPublicClient 拼好链 + 通道;② getBalance 问余额(返回 bigint);③ formatEther 把 wei 换成可读 ETH。client.getBalance 是挂在 client 上的便捷写法;也可 import { getBalance } from "viem/actions"getBalance(client, {...}),更利 tree-shake。

案例 2:从 ABI 自动推合约方法类型

Section titled “案例 2:从 ABI 自动推合约方法类型”

目标:让编辑器自己知道 balanceOf 返回 bigint。承接案例 1 的 client

import { getContract } from "viem";
// client 同案例 1
const usdcAbi = [
{ type: "function", name: "balanceOf", stateMutability: "view",
inputs: [{ name: "owner", type: "address" }],
outputs: [{ type: "uint256" }] },
] as const; // 关键:const 断言
const usdc = getContract({ address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", abi: usdcAbi, client });
const bal = await usdc.read.balanceOf(["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"]);
// ^? bigint — 悬停可见类型

三步:① ABI + as const 冻成字面量;② getContract 织出 read.balanceOf;③ 调用时编辑器已知道入参/返回。忘了 as const 类型立刻退化——新人头号坑。

目标:转 0.01 ETH 并等到上链确认。私钥仅用于本地演示;生产用浏览器钱包。client 仍用案例 1 的 PublicClient 等回执。

import { createWalletClient, http, parseEther } from "viem";
import { mainnet } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const account = privateKeyToAccount("0x..." as `0x${string}`);
const wallet = createWalletClient({ account, chain: mainnet, transport: http() });
const hash = await wallet.sendTransaction({ to: "0x...", value: parseEther("0.01") });
const receipt = await client.waitForTransactionReceipt({ hash });

三步:① 私钥 → account → WalletClient;② sendTransaction 只返回 hash;③ PublicClient 的 waitForTransactionReceipt 才等到回执。新人常停在第 ② 步。

  1. import 路径多容易导错:viem / viem/actions / viem/chains / viem/accounts / viem/utils 各管一摊。新人常 import { mainnet } from "viem" 然后 undefined。记住:链定义在 viem/chains

  2. ABI 必须 as const:忘了断言,TypeScript 把数组当成 { type: string }[],所有方法签名退化成 any。配合 ESLint rule @wagmi/no-abi-without-as-const 兜底。

  3. writeContract 不自动等回执:拿到 hash 就返回,链上还没确认。要么显式 waitForTransactionReceipt,要么用 wagmi 的 useWaitForTransactionReceipt hook。

  4. RPC 单点风险:写死 http("https://mainnet.infura.io/v3/..."),节点挂了整个 DApp 挂。改用 fallback([http(infura), http(alchemy)]) 让 viem 自动切。

  5. bigint vs number:余额、wei、gas 全是 bigint。用 +/- 时不能跟 number 混算,balance + 1 报错,必须 balance + 1n。新人最容易在这翻车。

维度viem (2023+)ethers v6 (2023+)web3.js v4 (2023+,2025-03 archived)
Bundle(压缩,README 量级)~35 kb~144 kb~240 kb
API 风格actions 函数 + Client/Transport/Chain 组合OO 三件套:Provider / Signer / ContractWeb3 主入口 + 模块挂载
类型来源从 ABI 字面量自动推手写 TypedContract / typechain 生成类型基本手写或弱类型
大数原生 bigint原生 bigint(v6 改)原生 bigint(v4 改)
Tree-shake友好(按 action import)一般(OO 类难拆)较差(主入口聚合)
维护状态活跃,wagmi 2 默认活跃,事实标准仓库 archived,进入维护期

适用

  • 新 DApp 项目首选——wagmi 2 默认底座
  • 浏览器首屏 JS 预算紧(例如希望以太坊客户端库 <50kb 压缩)时优先 viem
  • 需要从 ABI 自动推合约方法类型,省手写
  • 想用 functional 风格、按 action 引入

不适用

  • 已有大量 ethers v5/v6 代码且无重构预算——先用 ethers 别折腾
  • 需要兼容大量 web3.js v1 老教程或第三方插件
  • 非 EVM 链(Solana/Aptos/Sui 各有自己的 SDK)
  • 2022 年:wagmi 团队在做 React DApp hook 库时发现 ethers v5 在前端 bundle 接近 200kb、TypeScript 类型不够强,于是决定造轮子
  • 2023 年:viem 1.0 发布,完全 TypeScript 重写,提出 “Client + Transport + Chain” 三件套
  • 2024 年:wagmi 2.0 把默认底层从 ethers 切到 viem,下游被动迁移
  • 2025 年:web3.js 归档后,常见脚手架/教程更多默认指向 viem;stars 持续上涨
  1. OO 不是唯一答案:actions(函数)+ tree-shake 在浏览器场景能砍掉 4× bundle,前提是有强类型撑住组合性
  2. 类型可以从数据推:ABI 是 JSON,加一个 as const 就成了编译期类型源——把”运行期数据”变”类型信息”是 TS 高级用法的核心范式
  3. Transport 解耦的好处:mock / fallback / 多 RPC 切换全在同一抽象下,写测试和写生产几乎一样
  4. 生态绑定 = 默认决策:wagmi 2 一切到 viem,整个 React DApp 圈跟着搬家——库的成败常常不在代码本身
  • ethers-js —— viem 的直接对手,OO 风格、~144kb
  • web3-js —— 最早一代,2025 archived;viem 是它隔代后继
  • uniswap-v3 —— DApp 端常用 viem 调用
  • aave-v3 —— 借贷合约,前端常配 viem
  • anchor —— Solana 端的 SDK 风格对照(非 EVM)
  • aave-v3 —— Aave V3 — 借贷协议旗舰
  • lodestar —— Lodestar — JS/TS 生态里的以太坊共识层客户端
  • thirdweb-sdk —— thirdweb SDK — 一站式 Web3 全家桶