前端 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.js | Ethers.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 可大幅提升开发效率。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。