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);
        }
    }
}

七、高级开发模式

批量读取(Multicall)

链上多次 eth_call 会产生同样数量的 RPC 请求,Multicall 合约将它们合并为单次调用:

import { Contract, Interface } from "ethers";

// Multicall3 合约地址(几乎所有链都已部署)
const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
const ABI = [
    "function aggregate3(tuple(address target, bool allowFailure, bytes callData)[] calls) public view returns (tuple(bool success, bytes returnData)[] returnData)"
];

async function multicall(provider, calls) {
    const multicall = new Contract(MULTICALL3, ABI, provider);
    
    const results = await multicall.aggregate3(calls.map(c => ({
        target: c.target,
        allowFailure: true,  // 单个失败不影响其他
        callData: c.data
    })));
    
    return results.map((r, i) => {
        if (!r.success) return { success: false, error: "CALL_FAILED" };
        return { success: true, data: r.returnData };
    });
}

// 使用:同时查询 100 个地址的 ERC20 余额
const balances = await multicall(provider, addresses.map(addr => ({
    target: tokenAddress,
    data: erc20Interface.encodeFunctionData("balanceOf", [addr])
})));
// 仅需 1 次 RPC 调用而非 100 次
场景原始方式Multicall节省
查询 100 个余额100 RPC1 RPC99%
获取区块内所有事件分页查询单次聚合90%+
构建 MEV 机器人串行调用并行仿真时间关键

MEV 防护与抢跑保护

发送高价值交易时需要防范 MEV 攻击:

// Flashbots Protect(无需修改合约)
const flashbotsProvider = new JsonRpcProvider("https://rpc.flashbots.net");
const tx = await signer.sendTransaction({
    to: "0x...",
    value: parseEther("10"),
    data: "0x...",
    maxFeePerGas: feeData.maxFeePerGas * 120n / 100n,
});

// 私有内存池:交易不会出现在公共 mempool 中
// Flashbots 区块构建者将其打包, sandwich bot 无法提前看到
防护策略实现方式适用
私有内存池Flashbots Protect / MEV Blocker普通用户
Commit-Reveal两阶段提交拍卖、治理投票
时间锁TimelockController 合约大额治理操作
滑点保护Uniswap minAmountOutDEX 交易

事件索引与 The Graph

自建事件监听在链重组时不可靠,The Graph 提供去中心化索引服务:

# subgraph schema
query GetUserSwaps($user: String!) {
  swaps(where: { sender: $user }, orderBy: timestamp, orderDirection: desc) {
    id
    amountIn
    amountOut
    timestamp
  }
}
// 前端直接查询 GraphQL
const response = await fetch("https://api.thegraph.com/subgraphs/name/...", {
    method: "POST",
    body: JSON.stringify({ query, variables: { user: address } }),
});
const { data } = await response.json();

The Graph vs 自建索引:

维度The Graph自建索引
开发成本低(定义 schema 即可)高(自建 parser + DB)
延迟1-5 区块实时
可靠性多索引者竞争单点
成本按查询付费 / GRT 质押基础设施成本
灵活性受限于子图能力完全定制

八、本章小结

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

关键选型建议:

  • 新项目:直接使用 Viem + Wagmi,享受最佳类型安全和最小包体积
  • 维护中的 Ethers.js 项目:逐步迁移至 Viem,两者 API 设计理念相通
  • ** legacy Web3.js 项目**:如无特殊需求,可保持现状;新模块优先用 Ethers/Viem
  • 后端脚本:Ethers.js 的 Wallet 类或 Viem 的 createWalletClient 都足够健壮

继续阅读

探索更多技术文章

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

全部文章 返回首页

「blockchain」更多文章

  1. DePIN 去中心化物理基础设施网络
  2. DeFi 衍生品:期权、永续合约与合成资产
  3. 智能合约形式化验证:Certora 与 K 框架