引言:选择开发工具的重要性
进入以太坊DApp开发的第一步,往往不是学习Solidity或理解共识机制,而是选择一款合适的Web3开发库。这个选择将直接影响你的开发效率、代码可维护性,以及后续的扩展能力。
在当前的生态中,Web3.js和Ethers.js是最主流的两个选择。前者是以太坊官方JavaScript API的完整实现,后者则以轻量化和现代设计著称。两者都能完成与以太坊区块链交互的核心任务,但在API设计、使用体验和适用场景上存在显著差异。
本文将从实际开发者的视角,系统性地对比这两款工具,并提供实战代码示例,帮助你做出适合自己的选择。

一、Web3.js与Ethers.js概述
1.1 Web3.js:成熟的全能选手
Web3.js是以太坊官方维护的JavaScript API库,从以太坊项目创立之初就开始发展,可以说是以太坊开发生态的”元老级”工具。
核心特点:
- 功能完整:几乎涵盖以太坊所有JSON-RPC API,是最全面的以太坊JavaScript接口
- 版本稳定:经历了多个版本迭代,API相对稳定,文档详尽
- 社区成熟:大量的历史项目、教程、问答可供参考
- 官方背书:由以太坊基金会资助,与以太坊协议更新保持同步
适用场景:
- 需要与以太坊核心协议深度交互的项目
- 已有Web3.js历史积累,需要维护旧项目的团队
- 对功能全面性要求高于开发体验的场景
1.2 Ethers.js:轻盈的现代之选
Ethers.js由Richard Moore创建,以其优雅的API设计和轻量化著称,在2016年发布后迅速获得社区青睐。
核心特点:
- 体积小巧:压缩后仅约77KB(Web3.js约为1.5MB),更适合对加载速度敏感的Web应用
- 设计现代:采用ES6+语法,Promise原生支持,更符合现代前端开发习惯
- 钱包抽象:内置Wallet类,提供更灵活的密钥管理和签名功能
- 模块化架构:可以按需引入功能,减少不必要的依赖
适用场景:
- 注重开发体验和代码可读性的新项目
- 对应用加载性能有要求的Web DApp
- 需要复杂交易签名逻辑的项目
二、环境配置与安装
2.1 项目初始化
无论选择哪个库,首先需要初始化Node.js项目:
bash
mkdir my-dapp && cd my-dapp
npm init -y
npm install --save-dev vite
2.2 安装Web3.js
bash
npm install web3
基础配置:
javascript
// src/web3-config.js
import Web3 from 'web3';
// 连接到以太坊节点(使用Infura示例)
const web3 = new Web3(
new Web3.providers.HttpProvider('https://mainnet.infura.io/v3/YOUR_PROJECT_ID')
);
// 或者使用WebSocket连接(适合监听事件)
const web3Ws = new Web3(
new Web3.providers.WebsocketProvider('wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID')
);
export default web3;
2.3 安装Ethers.js
bash
npm install ethers
基础配置:
javascript
// src/ethers-config.js
import { ethers } from 'ethers';
// 连接到以太坊节点
const provider = new ethers.JsonRpcProvider(
'https://mainnet.infura.io/v3/YOUR_PROJECT_ID'
);
// 使用WebSocket连接
const providerWs = new ethers.WebSocketProvider(
'wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID'
);
export default provider;
三、钱包连接与账户管理
3.1 Web3.js连接MetaMask
javascript
// src/wallet-web3.js
import Web3 from 'web3';
class WalletService {
constructor() {
this.web3 = null;
this.accounts = [];
}
// 初始化Web3实例
async init() {
// 检查浏览器是否有Web3提供者(如MetaMask)
if (window.ethereum) {
this.web3 = new Web3(window.ethereum);
try {
// 请求用户授权
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts'
});
this.accounts = accounts;
return accounts[0];
} catch (error) {
console.error('用户拒绝授权:', error);
throw error;
}
} else {
throw new Error('请安装MetaMask或其他Web3钱包');
}
}
// 获取当前连接的账户
async getAccounts() {
if (!this.web3) await this.init();
const accounts = await this.web3.eth.getAccounts();
return accounts;
}
// 监听账户变化
onAccountsChanged(callback) {
window.ethereum.on('accountsChanged', (accounts) => {
this.accounts = accounts;
callback(accounts);
});
}
}
export default new WalletService();
3.2 Ethers.js连接MetaMask
javascript
// src/wallet-ethers.js
import { BrowserProvider } from 'ethers';
class WalletService {
constructor() {
this.provider = null;
this.signer = null;
}
// 初始化Ethers.js钱包提供者
async init() {
if (!window.ethereum) {
throw new Error('请安装MetaMask或其他Web3钱包');
}
// BrowserProvider封装了MetaMask的EIP-1193接口
this.provider = new BrowserProvider(window.ethereum);
this.signer = await this.provider.getSigner();
// 获取当前地址
const address = await this.signer.getAddress();
return address;
}
// 获取签名者
async getSigner() {
if (!this.provider) await this.init();
return this.provider.getSigner();
}
// 监听账户变化
onAccountsChanged(callback) {
window.ethereum.on('accountsChanged', async (accounts) => {
if (accounts.length === 0) {
// 用户断开连接
callback(null);
} else {
// 账户切换,重新获取签名者
this.signer = await this.provider.getSigner();
callback(accounts[0]);
}
});
}
// 监听链ID变化
onChainChanged(callback) {
window.ethereum.on('chainChanged', (chainId) => {
// 链变化后需要刷新页面以重置状态
window.location.reload();
callback(chainId);
});
}
}
export default new WalletService();
3.3 对比分析
| 特性 | Web3.js | Ethers.js |
|---|---|---|
| 提供者抽象 | 直接使用window.ethereum | BrowserProvider封装 |
| 签名获取 | web3.eth.getSigner() | provider.getSigner() |
| 类型 | 同步方法为主 | Promise/async/await |
| 事件监听 | 回调函数 | 事件发射器 |
从代码量可以看出,Ethers.js的API更加简洁直观,类型设计也更符合现代JavaScript习惯。
四、余额查询与Gas价格获取
4.1 Web3.js实现
javascript
// src/balance-web3.js
import web3 from './web3-config';
class BalanceService {
// 获取ETH余额(以Wei为单位)
async getBalanceInWei(address) {
const balance = await web3.eth.getBalance(address);
return balance;
}
// 获取ETH余额(以ETH为单位)
async getBalanceInEth(address) {
const balance = await web3.eth.getBalance(address);
return web3.utils.fromWei(balance, 'ether');
}
// 获取当前Gas价格
async getGasPrice() {
const gasPrice = await web3.eth.getGasPrice();
return gasPrice;
}
// 获取网络最新区块号
async getBlockNumber() {
return await web3.eth.getBlockNumber();
}
}
export default new BalanceService();
4.2 Ethers.js实现
javascript
// src/balance-ethers.js
import provider from './ethers-config';
class BalanceService {
// 获取ETH余额
async getBalance(address) {
const balance = await provider.getBalance(address);
// formatEther将Wei转换为ETH
return {
wei: balance.toString(),
eth: ethers.formatEther(balance)
};
}
// 获取当前Gas价格
async getGasPrice() {
const feeData = await provider.getFeeData();
return {
gasPrice: feeData.gasPrice, // 传统Gas价格
maxFeePerGas: feeData.maxFeePerGas, // EIP-1559最大费用
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas // 优先费用
};
}
// 获取网络最新区块号
async getBlockNumber() {
return await provider.getBlockNumber();
}
}
export default new BalanceService();
4.3 关键差异
余额格式处理:
- Web3.js:返回BN(Big Number)对象,需要用
web3.utils.fromWei()转换 - Ethers.js:返回BigInt类型,新增
formatEther()和parseEther()方法更直观
Gas价格:
- Web3.js:仅返回传统Gas价格
- Ethers.js:同时返回EIP-1559风格的maxFeePerGas和maxPriorityFeePerGas,对现代交易支持更好
五、智能合约交互
5.1 Web3.js调用合约
javascript
// src/contract-web3.js
import web3 from './web3-config';
// ERC-20代币ABI(简化版)
const ERC20_ABI = [
{
"constant": true,
"inputs": [{"name": "account", "type": "address"}],
"name": "balanceOf",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function"
},
{
"constant": false,
"inputs": [
{"name": "to", "type": "address"},
{"name": "amount", "type": "uint256"}
],
"name": "transfer",
"outputs": [{"name": "", "type": "bool"}],
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [{"name": "", "type": "uint256"}],
"type": "function"
}
];
class ContractService {
constructor() {
this.usdtAddress = '0xdAC17F958D2ee523a2206206994597C13D831ec7'; // USDT主网地址
this.contract = new web3.eth.Contract(ERC20_ABI, this.usdtAddress);
}
// 查询代币余额
async getBalance(account) {
const balance = await this.contract.methods.balanceOf(account).call();
return balance;
}
// 获取代币符号
async getSymbol() {
const symbol = await this.contract.methods.symbol().call();
return symbol;
}
// 转账交易
async transfer(to, amount, fromAddress) {
// 编码交易数据
const data = this.contract.methods.transfer(to, amount).encodeABI();
// 获取Gas估算
const gas = await this.contract.methods.transfer(to, amount).estimateGas({
from: fromAddress
});
// 获取Gas价格
const gasPrice = await web3.eth.getGasPrice();
// 构建交易对象
const txObj = {
from: fromAddress,
to: this.usdtAddress,
data: data,
gas: Math.floor(gas * 1.2), // 增加20%Gas缓冲
gasPrice: gasPrice,
value: '0'
};
return txObj;
}
// 监听Transfer事件
onTransfer(callback) {
this.contract.events.Transfer({
filter: {},
fromBlock: 'latest'
})
.on('data', (event) => {
callback({
from: event.returnValues.from,
to: event.returnValues.to,
value: event.returnValues.value,
transactionHash: event.transactionHash
});
})
.on('error', console.error);
}
}
export default new ContractService();
5.2 Ethers.js调用合约
javascript
// src/contract-ethers.js
import { ethers } from 'ethers';
import provider from './ethers-config';
// ERC-20代币ABI(简化版)
const ERC20_ABI = [
'function balanceOf(address account) view returns (uint256)',
'function transfer(address to, uint256 amount) returns (bool)',
'function symbol() view returns (string)',
'function totalSupply() view returns (uint256)',
'function decimals() view returns (uint8)'
];
class ContractService {
constructor() {
this.usdtAddress = '0xdAC17F958D2ee523a2206206994597C13D831ec7';
this.contract = new ethers.Contract(this.usdtAddress, ERC20_ABI, provider);
}
// 获取只读合约实例(用于查询)
getContract() {
return this.contract;
}
// 查询代币余额
async getBalance(account) {
const balance = await this.contract.balanceOf(account);
return balance;
}
// 获取代币精度(decimals)
async getDecimals() {
return await this.contract.decimals();
}
// 获取格式化的余额
async getFormattedBalance(account) {
const balance = await this.getBalance(account);
const decimals = await this.getDecimals();
return ethers.formatUnits(balance, decimals);
}
// 创建签名交易(用于发送交易)
async transfer(to, amount, signer) {
// 需要连接签名者
const contractWithSigner = this.contract.connect(signer);
// 格式化金额(假设USDT使用标准18位精度)
const formattedAmount = ethers.parseUnits(amount.toString(), 18);
// 发送交易
const tx = await contractWithSigner.transfer(to, formattedAmount);
console.log('交易已发送:', tx.hash);
// 等待交易确认
const receipt = await tx.wait();
return {
hash: tx.hash,
status: receipt.status === 1 ? '成功' : '失败',
confirmations: receipt.confirmations
};
}
// 监听Transfer事件(只读合约)
onTransfer(callback) {
this.contract.on('Transfer', (from, to, value, event) => {
callback({
from,
to,
value: value.toString(),
transactionHash: event.log.transactionHash,
blockNumber: event.log.blockNumber
});
});
}
// 异步迭代器方式监听事件
async *listenToTransfers() {
const filter = this.contract.filters.Transfer();
for await (const event of this.contract.queryFilter(filter)) {
yield {
from: event.args.from,
to: event.args.to,
value: event.args.value.toString()
};
}
}
}
export default new ContractService();
5.3 关键差异对比
ABI定义方式:
- Web3.js:使用JSON格式的完整ABI定义,包含完整的函数签名
- Ethers.js:可以使用简化的人类可读ABI字符串,如
'function transfer(address, uint256)'
合约实例化:
- Web3.js:
new web3.eth.Contract(abi, address) - Ethers.js:
new ethers.Contract(address, abi, provider/signer)
调用方式:
- Web3.js:链式调用
contract.methods.foo().call() - Ethers.js:直接调用
contract.foo()
事件监听:
- Web3.js:使用
.events.EventName()并配置过滤器 - Ethers.js:使用
.on('EventName', callback)或异步迭代器
六、离线交易签名
6.1 Web3.js离线签名
javascript
// src/OfflineSigner-web3.js
import web3 from './web3-config';
class OfflineSigningService {
// 构建离线交易
async buildRawTransaction(from, to, value, data = '0x') {
const nonce = await web3.eth.getTransactionCount(from);
const gasPrice = await web3.eth.getGasPrice();
const gas = await web3.eth.estimateGas({
from, to, value, data
});
return {
nonce: web3.utils.toHex(nonce),
gasPrice: web3.utils.toHex(gasPrice),
gas: web3.utils.toHex(gas),
to: to,
value: web3.utils.toHex(value),
data: data,
chainId: await web3.eth.getChainId()
};
}
// 签名交易(需要私钥)
signTransaction(txParams, privateKey) {
const signPromise = web3.eth.accounts.signTransaction(
txParams,
privateKey
);
return signPromise;
}
// 发送已签名的交易
async sendSignedTransaction(signedTx) {
const receipt = await web3.eth.sendSignedTransaction(
signedTx.rawTransaction
);
return receipt;
}
}
export default new OfflineSigningService();
6.2 Ethers.js离线签名
javascript
// src/OfflineSigner-ethers.js
import { ethers, Transaction } from 'ethers';
class OfflineSigningService {
// 使用钱包签名转账
async signTransfer(wallet, to, amount) {
const tx = await wallet.populateTransaction({
to: to,
value: ethers.parseEther(amount.toString())
});
const signedTx = await wallet.signTransaction(tx);
return signedTx;
}
// 使用HMAC密钥签名消息
signMessage(message, privateKey) {
const wallet = new ethers.Wallet(privateKey);
return wallet.signMessage(message);
}
// 验证签名
verifySignature(message, signature) {
const address = ethers.verifyMessage(message, signature);
return address;
}
// 创建和签名交易
async createAndSignTransaction(wallet, txParams) {
const tx = await wallet.populateTransaction(txParams);
const signedTx = await wallet.signTransaction(tx);
return signedTx;
}
}
export default new OfflineSigningService();
6.3 签名对比总结
| 场景 | Web3.js | Ethers.js |
|---|---|---|
| 私钥管理 | 需要外部导入web3.eth.accounts | 内置Wallet类,支持HD钱包 |
| 消息签名 | web3.eth.accounts.sign() | wallet.signMessage() |
| Typed Data签名 | 需要手动编码 | 支持EIP-712标准 |
| HD路径支持 | 基本支持 | 原生支持,标准HD路径 |
Ethers.js在签名方面提供了更完善的抽象,特别是对硬件钱包和HD钱包的支持更加友好。
七、实战项目结构建议
7.1 推荐的项目组织方式
plaintext
my-dapp/
├── src/
│ ├── config/
│ │ ├── web3-config.js # Web3.js配置
│ │ └── ethers-config.js # Ethers.js配置
│ ├── services/
│ │ ├── wallet.js # 钱包连接服务
│ │ ├── balance.js # 余额查询服务
│ │ └── contract.js # 合约交互服务
│ ├── hooks/
│ │ ├── useWallet.js # React钱包Hook
│ │ ├── useBalance.js # React余额Hook
│ │ └── useContract.js # React合约Hook
│ ├── utils/
│ │ ├── format.js # 格式化工具
│ │ └── errors.js # 错误处理
│ └── App.jsx
├── package.json
└── vite.config.js
7.2 React集成示例
javascript
// src/hooks/useWallet.js
import { useState, useEffect, useCallback } from 'react';
import walletService from '../services/wallet';
export function useWallet() {
const [address, setAddress] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
const connect = useCallback(async () => {
setIsConnecting(true);
setError(null);
try {
const addr = await walletService.init();
setAddress(addr);
} catch (err) {
setError(err.message);
} finally {
setIsConnecting(false);
}
}, []);
const disconnect = useCallback(() => {
setAddress(null);
}, []);
useEffect(() => {
walletService.onAccountsChanged((accounts) => {
setAddress(accounts?.[0] || null);
});
}, []);
return {
address,
isConnected: !!address,
isConnecting,
error,
connect,
disconnect
};
}
八、选型建议与总结
8.1 项目场景选择
选择Web3.js的场景:
- 需要与以太坊核心协议深度集成的项目
- 团队已有Web3.js的技术积累
- 需要使用
web3.eth.personal等较少使用的API - Node.js后端环境(非浏览器)
选择Ethers.js的场景:
- 新启动的前端DApp项目
- 对应用加载性能有较高要求
- 需要优雅的API和良好的开发体验
- 使用TypeScript的项目(类型支持更好)
8.2 性能对比
| 指标 | Web3.js | Ethers.js |
|---|---|---|
| 库大小 | ~1.5MB | ~77KB |
| 初始化时间 | 较慢 | 快速 |
| API响应 | 基本持平 | 基本持平 |
| 内存占用 | 较高 | 较低 |
8.3 未来趋势
以太坊开发工具正在向更模块化、更类型安全的方向发展。Ethers.js v6的发布带来了更多现代化改进,而Web3.js也在持续迭代。两者的核心功能差异正在缩小,最终的选择更多取决于团队偏好和项目需求。
我的建议:对于新项目,优先考虑Ethers.js;如果是维护现有项目,保持原有选择并逐步迁移。

发表回复