教程区块链区块链技术ch1414.1 前端库:ethers.js 与 Viem

本页目录

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 v5ethers.js v6Viem
包体积130 KB110 KB21 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 gas
  • UserRejectedRequestError 而非 Error: user rejected
  • ContractFunctionExecutionError 包含 decodedArgs

14.1.4 前端库选型建议

场景选择
新项目Viem — 更小、更快、更安全
ethers 遗产ethers v6 — 迁移成本低
需要最新功能Viem — 积极维护、新 EIP 支持快
多项目复用统一选 Viem,减少认知负担

> ← 13.x 开发工具 | 前往 → 14.2 钱包连接 |*

评论

0

评论加载中…

发表评论

0/2000