Web3.js 与 Ethers.js:与区块链交互的 JavaScript SDK

对比 Web3.js 与 Ethers.js 的架构设计差异,详解 Provider/Signer 模型、合约读写、事件监听、交易发送等核心操作,以及常见开发模式。

前端 DApp 与区块链交互的核心是 JavaScript SDK。目前最主流的两个库是 Web3.js(以太坊基金会维护)和 Ethers.js(社区驱动的轻量库)。本文将对比两者设计理念,并覆盖从钱包连接到合约交互的完整开发链路。

一、Web3.js vs Ethers.js

架构对比

Web3.js (v4)                    Ethers.js (v6)
┌─────────────────┐            ┌─────────────────┐
│   Web3 Object   │            │  Provider       │  ← 网络连接(只读)
│  (monolithic)   │            │  (Abstracted)   │
└────────┬────────┘            ├─────────────────┤
         │                     │  Signer         │  ← 签名权限(私钥/钱包)
    ┌────┴────┐                │  (Separated)    │
    │ Web3    │                ├─────────────────┤
    │ .eth    │                │  Contract       │  ← 合约实例
    │ .utils  │                │  (Bound to both)│
    │ ...     │                └─────────────────┘
    └─────────┘
维度Web3.jsEthers.js
体积较大(~500KB+)较小(~120KB)
API 设计命令式、单一入口模块化、Provider/Signer 分离
类型支持TypeScript 支持较好原生 TypeScript,类型更精确
错误处理有时不透明更详细的错误信息
生态成熟度更老,文档丰富现代架构,Viem 正在替代

⚠️ 趋势:Ethers.js 作者 Richard Moore 推出了新一代库 Viem(更轻量、类型更严格),Wagmi 2.0 已将默认底层从 Ethers 切换到 Viem。

二、Ethers.js 核心概念

Provider(提供者)

Provider 是连接到区块链网络的只读接口,无需私钥。

import { ethers } from "ethers";

// 方式1:浏览器钱包(MetaMask 等)
const provider = new ethers.BrowserProvider(window.ethereum);

// 方式2:RPC 节点(只读,无签名能力)
const rpcProvider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");

// 方式3:WebSocket(实时监听事件)
const wsProvider = new ethers.WebSocketProvider("wss://eth-mainnet.ws.alchemy.com/v2/YOUR_KEY");

Signer(签名者)

Signer 代表一个有权签名的以太坊账户,可以是:

  • 浏览器钱包用户(通过 MetaMask 授权)
  • 私钥(后端/脚本)
  • 硬件钱包
// 从 BrowserProvider 获取 Signer(需用户授权)
const signer = await provider.getSigner();

// 从私钥创建钱包(仅后端脚本使用)
const wallet = new ethers.Wallet(PRIVATE_KEY, provider);

// 查询签名者地址
const address = await signer.getAddress();

🔒 安全提醒:私钥永远不应该出现在前端代码中。前端 DApp 应使用 BrowserProvider 通过钱包签名。

三、基础操作

查询链上数据(只读,无需签名)

import { ethers } from "ethers";

const provider = new ethers.JsonRpcProvider("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY");

// 查询当前区块号
const blockNumber = await provider.getBlockNumber();
console.log("当前区块:", blockNumber);

// 查询 ETH 余额
const balance = await provider.getBalance("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
console.log("余额:", ethers.formatEther(balance), "ETH");  // 自动从 Wei 转换

// 查询交易详情
const tx = await provider.getTransaction("0x...");
console.log(tx);

// 查询区块详情
const block = await provider.getBlock("latest");
console.log("Gas limit:", block.gasLimit);

发送交易(需 Signer)

const signer = await provider.getSigner();

// 发送 ETH 转账
const tx = await signer.sendTransaction({
    to: "0xRecipientAddress...",
    value: ethers.parseEther("0.1"),  // 0.1 ETH = 10¹⁷ Wei
});

// 等待交易确认(1 个区块确认)
const receipt = await tx.wait();
console.log("交易已确认,Gas 消耗:", receipt.gasUsed);

EIP-1559 交易(推荐)

const feeData = await provider.getFeeData();

const tx = await signer.sendTransaction({
    to: "0x...",
    value: ethers.parseEther("0.1"),
    maxFeePerGas: feeData.maxFeePerGas * 120n / 100n,        // 上浮 20%
    maxPriorityFeePerGas: feeData.maxPriorityFeePerGas,
});

四、智能合约交互

读取合约状态(仅 Provider)

// ERC-20 合约 ABI(简化版)
const abi = [
    "function balanceOf(address owner) view returns (uint256)",
    "function totalSupply() view returns (uint256)",
    "function decimals() view returns (uint8)",
    "event Transfer(address indexed from, address indexed to, uint256 value)"
];

const contract = new ethers.Contract("0xA0b86a33E6Cb19d3C91d8C8c3D0fE", abi, provider);

// 读取余额(view 函数,不消耗 Gas)
const balance = await contract.balanceOf("0x...");
console.log("代币余额:", ethers.formatUnits(balance, 18));

// 读取总供应量
const total = await contract.totalSupply();
console.log("总供应量:", ethers.formatUnits(total, 18));

写入合约状态(需 Signer)

// 连接 Signer 创建可写合约实例
const writeContract = contract.connect(signer);

// 发送代币转账交易
const tx = await writeContract.transfer("0xRecipient...", ethers.parseUnits("100", 18));
await tx.wait();
console.log("转账成功!");

事件监听

// 监听 Transfer 事件(实时)
contract.on("Transfer", (from, to, amount, event) => {
    console.log(`从 ${from} 转账 ${ethers.formatUnits(amount, 18)}${to}`);
});

// 查询历史事件(筛选条件)
const filter = contract.filters.Transfer("0xSenderAddress...");
const events = await contract.queryFilter(filter, -10000, "latest");  // 最近 10000 个区块

五、Viem 简介(下一代 SDK)

Viem 是 Ethers.js 作者推出的现代化替代方案:

import { createPublicClient, http, formatEther } from "viem";
import { mainnet } from "viem/chains";

const client = createPublicClient({
    chain: mainnet,
    transport: http("https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"),
});

const balance = await client.getBalance({
    address: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
});
console.log(formatEther(balance));

Viem 的优势

  • 更小的打包体积(tree-shaking 友好)
  • 更严格的 TypeScript 类型
  • 原生支持 Account Abstraction (ERC-4337)
  • 所有 API 均为纯函数,副作用明确

六、常见开发模式

连接钱包 + 检测网络切换

async function connectWallet() {
    if (!window.ethereum) {
        alert("请安装 MetaMask!");
        return;
    }
    
    const provider = new ethers.BrowserProvider(window.ethereum);
    
    // 请求连接
    await provider.send("eth_requestAccounts", []);
    const signer = await provider.getSigner();
    const address = await signer.getAddress();
    
    // 检测网络切换
    window.ethereum.on("chainChanged", (chainId) => {
        window.location.reload();
    });
    
    // 检测账户切换
    window.ethereum.on("accountsChanged", (accounts) => {
        if (accounts.length === 0) {
            console.log("钱包已断开连接");
        } else {
            console.log("切换到账户:", accounts[0]);
        }
    });
    
    return { provider, signer, address };
}

估算 Gas + 处理交易失败

async function safeTransfer(contract, to, amount) {
    try {
        // 先估算 Gas
        const gasEstimate = await contract.transfer.estimateGas(to, amount);
        console.log("预估 Gas:", gasEstimate.toString());
        
        // 实际发送(Gas limit 上浮 20%)
        const tx = await contract.transfer(to, amount, {
            gasLimit: gasEstimate * 120n / 100n,
        });
        
        const receipt = await tx.wait();
        console.log("交易成功,区块:", receipt.blockNumber);
    } catch (err) {
        if (err.code === "INSUFFICIENT_FUNDS") {
            console.error("ETH 余额不足支付 Gas");
        } else if (err.code === "CALL_EXCEPTION") {
            console.error("合约调用失败(revert)");
        } else {
            console.error("未知错误:", err);
        }
    }
}

七、本章小结

Web3.js 与 Ethers.js 是与以太坊区块链交互的两大主流 JavaScript SDK。Ethers.js 凭借清晰的 Provider/Signer 分离设计和更精确的 TypeScript 支持,已成为大多数现代 DApp 的首选。而 Viem 作为新一代工具,正在快速崛起。在选择时,建议新项目和前端 DApp 优先使用 Ethers.js 或 Viem,配合 Wagmi/React Query 可大幅提升开发效率。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「区块链 Web3」更多文章

  1. Web3 全栈 DApp 开发实战:从前端到智能合约的完整链路
  2. 企业级区块链:Hyperledger Fabric 架构与链码开发
  3. 区块链安全:合约审计、攻击模式与防御体系