Web3.js与Ethers.js开发库对比与实战指南:以太坊DApp开发者的选择之道

Web3.js与Ethers.js以太坊开发库对比,两大主流工具选型指南

引言:选择开发工具的重要性

进入以太坊DApp开发的第一步,往往不是学习Solidity或理解共识机制,而是选择一款合适的Web3开发库。这个选择将直接影响你的开发效率、代码可维护性,以及后续的扩展能力。

在当前的生态中,Web3.jsEthers.js是最主流的两个选择。前者是以太坊官方JavaScript API的完整实现,后者则以轻量化和现代设计著称。两者都能完成与以太坊区块链交互的核心任务,但在API设计、使用体验和适用场景上存在显著差异。

本文将从实际开发者的视角,系统性地对比这两款工具,并提供实战代码示例,帮助你做出适合自己的选择。

Web3.js体积1.5MB vs Ethers.js体积77KB,核心功能与选型建议对比

一、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.jsEthers.js
提供者抽象直接使用window.ethereumBrowserProvider封装
签名获取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.jsEthers.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.jsEthers.js
库大小~1.5MB~77KB
初始化时间较慢快速
API响应基本持平基本持平
内存占用较高较低

8.3 未来趋势

以太坊开发工具正在向更模块化、更类型安全的方向发展。Ethers.js v6的发布带来了更多现代化改进,而Web3.js也在持续迭代。两者的核心功能差异正在缩小,最终的选择更多取决于团队偏好和项目需求。

我的建议:对于新项目,优先考虑Ethers.js;如果是维护现有项目,保持原有选择并逐步迁移。

相关阅读

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注