graph LR
前端[React/NextJS 前端] --> Viem[类型安全<br/>Viem]
前端 --> Ethers[传统标准<br/>ethers.js v6]
前端 --> Web3[Web3.js v4]
Viem --> 节点[节点 RPC<br/>WalletConnect JSON-RPC]
Ethers --> 节点
Web3 --> 节点
节点 --> 合约[智能合约读写<br/>ABI 绑定调用]
节点 --> 事件[事件订阅 / 过滤]
style Viem fill:#c8e6c9
style 节点 fill:#e3f2fd
智能合约的后端是 Solidity,前端是 TypeScript。 ethers.js 是传统标准,但 Viem 正在定义下一代——它更小、更快、更安全、类型更安全。
14.1.1 两代前端库的演进
| 特性 | ethers.js v5 | ethers.js v6 | Viem |
|---|---|---|---|
| 包体积 | 130 KB | 110 KB | 21 KB (gzip) |
| 类型安全 | 好 | 更好 | 内置严格类型 |
| 树摇优化 | 部分 | 更好 | 完美 |
| BigInt | 混合 (BN.js) | 原生 BigInt | 原生 BigInt |
| Provider/Separation | 清晰 | 简化 | one-shot calls |
| 错误处理 | 基础 | 更好 | 详尽的错误类型 |
| EIP-1559 支持 | 好 | 内置 | 内置 |
14.1.2 ethers.js v6 核心模式
typescript
/**
* ethers.js v6:Provider 读取 + Signer 写入
*/
import { ethers } from "ethers"; // 概念性展示,不依赖任何库
// 1. Provider:连接以太坊(只读)
const infuraUrl = "https://mainnet.infura.io/v3/YOUR_KEY";
const provider = new ethers.JsonRpcProvider(infuraUrl);
// 读取
const balance = await provider.getBalance("0xd8dA...");
const blockNumber = await provider.getBlockNumber();
// 2. Signer:签名交易(需要私钥或钱包)
const privateKey = "0x...";
const signer = new ethers.Wallet(privateKey, provider);
// 写入:转账 0.1 ETH
const tx = await signer.sendTransaction({
to: "0xRecipient...",
value: ethers.parseEther("0.1"), // 100000000000000000n
});
const receipt = await tx.wait(); // 等待确认
// 3. 合约交互:自动编码 ABI
const erc20ABI = [
"function balanceOf(address) view returns (uint256)",
"function transfer(address,uint256) returns (bool)",
"event Transfer(address,address,uint256)",
];
const token = new ethers.Contract(tokenAddress, erc20ABI, signer);
const myBalance = await token.balanceOf(signer.address);
const tx2 = await token.transfer("0xFriend...", 1000n);14.1.3 Viem:类型原生的极简设计
typescript
/**
* Viem:现代前端与链交互的范式
* 核心概念:Client = Provider + 可选的 Wallet(Account)
*/
// 纯 TypeScript 模拟,展示 Viem 的设计哲学
// 1. 创建可读的公共 client(无需钱包)
interface PublicClient {
chain: { id: number; name: string };
transport: { url: string };
async getBalance(address: string): Promise<bigint>;
async getBlockNumber(): Promise<bigint>;
async readContract(params: any): Promise<any>;
async getGasPrice(): Promise<bigint>;
}
// 2. 创建可写的 wallet client(需要 Account)
interface WalletClient {
account: { address: string; privateKey?: string };
chain: { id: number };
async sendTransaction(params: any): Promise<string>;
async writeContract(params: any): Promise<string>;
async signMessage(message: string): Promise<string>;
}
// 设计差异对比
function compareEthersVsViem() {
// ethers.js: 一个 Contract 对象,内部持有 provider/signer
// 所有操作都通过 Contract 对象,方便但类型松散
// Viem: 分离到函数级,每个操作独立调用
// 读合约:readContract({ address, abi, functionName, args })
// 写合约:writeContract({ ... })
// 纯粹函数式,更容易 tree-shake 和类型推断
return "Viem wins on: size(21KB), strict types, no class state, explicit function calls";
}
// Viem 风格的合约交互(TypeScript 模拟)
async function viemStyleRead(
client: PublicClient,
tokenAddress: string,
userAddress: string,
): Promise<bigint> {
return client.readContract({
address: tokenAddress as `0x${string}`,
abi: [
{
name: "balanceOf",
type: "function",
stateMutability: "view",
inputs: [{ name: "account", type: "address" }],
outputs: [{ type: "uint256" }],
},
],
functionName: "balanceOf",
args: [userAddress],
});
}
// 对比:读合约(不涉及状态变更)vs 写合约(需要签名)
// 在 Viem 中,这是两个完全独立的函数调用,<FunctionName> 是类型 infer 的
// 错误:如果写合约时用了 readContract,TypeScript 编译期就捕获
console.log(compareEthersVsViem());Viem 的核心优势:错误类型
Viem 为常见错误提供类型安全的错误对象:
InsufficientFundsError而非Error: insufficient funds for gasUserRejectedRequestError而非Error: user rejectedContractFunctionExecutionError包含decodedArgs
14.1.4 前端库选型建议
| 场景 | 选择 |
|---|---|
| 新项目 | Viem — 更小、更快、更安全 |
| ethers 遗产 | ethers v6 — 迁移成本低 |
| 需要最新功能 | Viem — 积极维护、新 EIP 支持快 |
| 多项目复用 | 统一选 Viem,减少认知负担 |
> ← 13.x 开发工具 | 前往 → 14.2 钱包连接 |*
评论
0评论加载中…