Ethers.js 是面向以太坊及 EVM 兼容链的 JavaScript/TypeScript 客户端库,由 Richard Moore(ricmoo)发起并持续维护。它把节点通信(JSON-RPC/WebSocket)、账户与签名(钱包、助记词、EIP-712 结构化签名)、合约部署与调用(ABI 编解码、事件监听)统一封装为 Provider、Signer、Contract 三层抽象,开发者无需手写底层 RPC 请求与加密原语。当前主线为 v6(2023 年 3 月发布),以原生 bigint 取代 BigNumber、移除 utils 命名空间,API 更现代、打包体积更小;v5 仍处于维护期,存量项目众多。本篇从安装配置讲起,覆盖读取链上数据、钱包签名、合约交互的完整实操,生产部署要点(多 RPC 容灾、事件监控、回执确认),并与 web3.js、viem、wagmi 做选型对比,最后附常见误区与面试题。
掌握 Ethers.js 是 Web3 全栈与链上数据工程的基础技能:无论 DApp 前端、交易脚本还是索引服务,合约交互与钱包管理都离不开这套核心 API 抽象。
目录
速览卡是工具类内容的固定首屏模块:30 秒判断「这工具是什么、能不能商用、适不适合我」。
为什么需要 Ethers.js:手写 JSON-RPC 与签名的成本太高
直接对接以太坊节点,开发者需要自己构造 JSON-RPC 请求(eth_call、eth_sendRawTransaction)、处理私钥签名(secp256k1 曲线、RLP 编码)、把函数签名转成 4 字节选择器并做 ABI 编解码。“把 1 ETH 转给某地址”这条最简单的路径,就要完成 gas 估算、nonce 管理、签名、序列化、广播、轮询回执六个步骤,每一步都容易出错,而且与业务逻辑无关。
Ethers.js 将上述过程收敛为三层抽象:Provider 负责与节点通信(读取链上数据、广播交易);Signer(通常由 Wallet 实现)负责账户与签名;Contract 负责合约的 ABI 编解码与调用。开发者只需 provider.getBalance(addr) 读取余额、wallet.sendTransaction({to, value}) 发送交易、contract.balanceOf(addr) 调用合约只读方法——底层细节全部由库处理,错误率与代码量同时下降。
与 web3.js 相比,Ethers.js 在 v6 后全面拥抱 TypeScript 与原生 bigint,类型推导更可靠、包体积更小;与 viem 相比,ethers 的面向对象风格更直观,历史教程与存量代码更多,至今仍是大多数课程、工具链(如 Hardhat 生态)的默认选择。
安装与环境配置:3 步就绪
Ethers.js 需要 Node.js 14+(建议 18+)。在项目目录内执行以下命令完成初始化、安装与验证。
步骤一:初始化项目并安装依赖。步骤二:打印版本号确认安装成功,v6 应输出 6.x.x。步骤三:连接公共 RPC 读取主网最新区块高度,验证网络通路可用。浏览器端也可用官方 CDN 的 UMD 包(https://cdn.ethers.io/lib/ethers-6.13.umd.min.js,版本号按需替换)直接引入。
npm init -ynpm install ethers # 验证版本(ESM 方式)node --input-type=module -e "import { ethers } from 'ethers'; console.log(ethers.version);" # 连接公共节点读取区块高度,验证链上通路node --input-type=module -e "import { ethers } from 'ethers';const provider = new ethers.JsonRpcProvider('https://cloudflare-eth.com');console.log('最新区块:', await provider.getBlockNumber());"
快速上手:5 分钟读取余额并发送一笔交易
最小可用流程:连接网络(Provider)→ 读取链上数据 → 用钱包(Wallet)签名广播交易 → 等待回执(wait)。
1. 在项目根目录创建 demo.mjs(ESM 模块),引入 ethers。
2. 创建 Provider:new ethers.JsonRpcProvider('https://cloudflare-eth.com'),也可替换为 Infura/Alchemy 的带 key 地址。
3. 用 provider.getBalance(地址) 读取余额,ethers.formatEther 把 wei(ETH 最小单位,1 ETH = 10^18 wei)显示为 ETH 字符串。
4. 用私钥创建 Wallet 并指定网络:new ethers.Wallet(process.env.PRIVATE_KEY, provider)。
5. 调用 wallet.sendTransaction({to, value}),value 用 ethers.parseEther('0.001') 转为 wei。
6. await tx.wait() 等待区块确认,从回执读取 status 与 gasUsed。
进阶技巧:主网发送会消耗真实 ETH,建议先在 Sepolia 测试网联调(RPC 地址与测试币 faucet 向 RPC 服务商索取),流程验证通过后再切主网。
import { ethers } from 'ethers'; // 1. 连接以太坊主网(也可替换为 Infura/Alchemy 私有地址)const provider = new ethers.JsonRpcProvider('https://cloudflare-eth.com');console.log('最新区块高度:', await provider.getBlockNumber()); // 2. 读取余额。formatEther 把 wei(bigint)转为 ETH 字符串const vitalik = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045';const wei = await provider.getBalance(vitalik);console.log('Vitalik 余额:', ethers.formatEther(wei), 'ETH'); // 3. 用私钥创建钱包(务必使用环境变量,勿硬编码)const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider); // 4. 发送交易。parseEther 把 ETH 字符串转为 wei(bigint)const tx = await wallet.sendTransaction({ to: '0xRecipientAddressHere', value: ethers.parseEther('0.001'),});console.log('交易广播成功,hash:', tx.hash); // 5. 等待确认。receipt.status === 1 表示执行成功const receipt = await tx.wait();console.log('已确认,区块:', receipt.blockNumber, 'gas 消耗:', receipt.gasUsed.toString());
核心能力详解:5 大支柱
支柱一·Provider 网络接入:Provider 抽象对节点的全部读写操作。v6 提供 JsonRpcProvider(HTTP 轮询)、WebSocketProvider(实时推送)、FallbackProvider(多路容灾,支持权重与超时配置)、BrowserProvider(包装浏览器注入的 EIP-1193 钱包)。常用读接口有 getBlockNumber、getBalance、getTransactionReceipt;写接口 broadcastTransaction 负责将签名后的交易广播上链。
支柱二·Wallet 账户与签名:Wallet 实现 Signer 接口,持有私钥并负责签名。创建方式:new Wallet(privateKey)、Wallet.createRandom()、HDNodeWallet.fromPhrase(mnemonic, path)(BIP-39/BIP-44 助记词派生,一条助记词可派生无限多账户)。签名能力包括 signMessage(消息签名)与 signTypedData(EIP-712 结构化签名,常用于登录授权与 Permit 免 gas 授权)。
支柱三·Contract 合约交互:new ethers.Contract(address, abi, runner),runner 传 Provider 则只读、传 Signer 则可写。view 函数直接返回结果值;写函数返回 TransactionResponse,必须 .wait() 拿回执;payable 函数可在调用参数中附带 { value }。v6 支持人类可读 ABI(如 ['function transfer(address to, uint256 amount) returns (bool)']),无需手抄 JSON ABI。
支柱四·事件监听与日志解析:contract.on('Transfer', cb) 订阅实时事件;provider.getLogs(filter) 按区块范围回溯历史日志;事件参数按 ABI 自动解码(如 from、to、value)。数据中心型应用常用 WebSocketProvider 配合 checkpoint 记录已处理区块,断线后回扫补漏。
支柱五·数据编解码与工具函数:formatUnits/parseUnits 完成任意 ERC-20 代币的小数与最小单位换算;getAddress 校验并统一地址校验和;Interface 负责函数选择器与事件 topic 的编解码;keccak256、hexlify 等用于文件哈希上链、构造原始数据等场景。
生产部署与监控:从上线到运维
多 RPC 容灾:单一路 RPC 在限流或宕机时直接导致服务不可用。生产建议用 FallbackProvider 组合多家供应商,按权重分配并设置单次请求的 stallTimeout(超时阈值):
事件监听服务:使用 WebSocketProvider 订阅合约事件,进程内维护 lastSyncedBlock,每处理一批日志更新 checkpoint 并持久化(Redis/数据库);断线重连后从 checkpoint 区块高度调用 getLogs 回扫,避免漏数据。服务保持无状态,把进度放到外部存储以便水平扩容。
可观测性:所有交易操作记录 hash、nonce、gas 上限、实际 gasUsed 与确认耗时;对 wait() 超时和 status !== 1 的交易上报告警;采集 Provider 层的请求失败率、429 限流次数等指标接入 Prometheus/Sentry。私钥只存于后端环境变量或 KMS 签名服务,前端永不接触。
<p class="para-label">避坑清单:</p>
<ul class="dash-list"><li>v5 代码直接迁移 v6 会大量报错:BigNumber 与 ethers.utils 命名空间已移除,wei 值变为原生 bigint,务必先读官方迁移指南再改业务代码。</li><li>只 await sendTransaction 不调 wait():交易仅代表广播成功,未确认不算成功,必须 await tx.wait() 并校验 receipt.status === 1。</li><li>私钥写死在前端或提交到 Git:任何拿到代码者都能转走资产,私钥应只放后端环境变量或托管签名服务。</li><li>生产裸用免费公共 RPC:限流频繁导致请求失败,建议 FallbackProvider 多路容灾或自建节点。</li><li>WebSocket 断线后事件会静默丢失:必须记录 lastSyncedBlock 检查点,重连后调用 getLogs 回扫补漏。</li></ul>
const provider = new ethers.FallbackProvider([ { provider: process.env.INFURA_URL, weight: 2, stallTimeout: 5000 }, { provider: process.env.ALCHEMY_URL, weight: 2, stallTimeout: 5000 }, { provider: 'http://localhost:8545', weight: 1, stallTimeout: 10000 },]);
| 维度 | Ethers.js | web3.js | viem | wagmi |
|---|---|---|---|---|
| 定位 | 通用以太坊 JS 库,DApp/脚本/后端通用 | 老牌通用以太坊 JS 库 | 类型安全的以太坊 JS 库(函数式) | 基于 viem 的 React Hooks 封装 |
| 编排范式 | 面向对象:Provider/Signer/Contract | 面向对象:web3.eth.* | 函数式 actions + 强类型推导 | 声明式 Hooks,组件内调用 |
| 类型约束 | TypeScript 编写,v6 类型较完善 | 弱类型,依赖社区类型补丁 | TypeScript 优先,类型最严格 | 继承 viem 的完整类型链 |
| 学习曲线 | 较低,教程与资料最多 | 低,历史教程丰富 | 中,需理解函数式风格 | 中低,React 体验好 |
| 生态集成 | Hardhat 默认依赖之一,插件多 | 老生态,课程引用多 | wagmi/viem 系新工具链 | RainbowKit/ConnectKit 主流 |
| 生产部署 | Provider 容灾/重试/事件监听能力完备 | 功能完备,维护节奏慢 | 活跃维护,工具链友好 | 专注前端,后端仍需 viem |
| License | MIT | LGPL-3.0 | MIT | MIT |
| 适合谁 | 全栈 Web3、脚本与后端、课程学习 | 维护老项目、历史代码 | 新项目、类型敏感团队 | React DApp 前端 |
选型建议:后端脚本、索引服务与课程学习选 Ethers.js,资料最多、心智模型最直观;新起的 React DApp 前端优先 wagmi + viem,类型体验更好;纯 Node 服务且追求最强类型推导选 viem;仅在维护已有 web3.js 项目时保留它,新模块可逐步迁移。
版本迭代
Ethers.js 自 2016 年发布 v1,经历 2019 年 v4 的 ABI/事件系统重写、2020 年 v5 全面 TypeScript 化与模块化、2023 年 v6 以 bigint 取代 BigNumber 并扁平化 API 的三次大版本演进。v5 进入长期维护,v6 自 2023 年起经多次小版本打磨,截至训练数据最新为 6.13.x(2024 年 10 月发布,后续持续有补丁更新,本文撰写时可能已有更新版本)。
| 版本 | 时间 | 变化 | 提升 |
|---|---|---|---|
| 4.0 | 2019 年 | 重写 ABI 解码器与 Contract 事件 API | 合约交互稳定性显著提升,事件订阅体验改善 |
| 5.0 | 2020-07 | 全面重写:TypeScript 源码、ESM 模块化 | 类型安全、包体积缩小,成为 DApp 与工具链事实标准 |
| 5.7 | 2022-01 | v5 系列收尾,修复安全与兼容问题 | v5 进入长期维护,存量项目可继续安全使用 |
| 6.0 | 2023-03 | 推翻式重构:BigNumber→原生 bigint、移除 utils 命名空间、Provider 架构重设计 | API 更现代,打包体积与解析性能明显优化 |
| 6.13 | 2024-10 | 6.x 系列持续维护:合约调用、钱包与 Provider 稳定性修复 | 当前主线推荐版本(截至训练数据,可能已有更新版本) |
Ethers.js 无官方跑分榜单,v6 相对 v5 的实测差异集中在工程层面:包体积显著下降(移除 utils 冗余代码与依赖)、wei 值使用原生 bigint 减少类型转换开销、Provider 层支持 stallTimeout 与优先级调度。选型建议:新项目直接用 v6;存量 v5 项目若无强需求不必急于迁移,但新功能与安全修复以 v6 为优先。
AI 应用场景
场景 1:DApp 前端连接钱包与合约交互
阶段:链上交互 / 合约调用层
解决的问题:浏览器接入 MetaMask 等钱包需要处理 EIP-1193(浏览器扩展注入钱包的标准接口)的窗口注入、账号切换、链切换等协议细节。用 new ethers.BrowserProvider(window.ethereum) 一步包装,getSigner() 后用 Signer 发起转账与合约调用,账号切换通过监听 EIP-1193 的 accountsChanged 事件刷新界面。
具体案例:某 DeFi 前端用 BrowserProvider 实现代币授权与交易流程,交易通过 tx.wait() 更新确认状态,典型落地中交互代码量较手写 RPC 封装缩减约 80%,用户侧交易失败率明显下降。
场景 2:批量空投与运营脚本
阶段:链上批量操作 / 运营工具化
解决的问题:人工在钱包逐个转账效率低且易错。用 Node 脚本加 Wallet 批量构造交易,结合 EIP-1559 参数与 nonce 管理,用 parseUnits 精确处理代币小数位,getAddress 统一校验收件地址。
具体案例:项目方发放 1000 份代币奖励:脚本读取 CSV 地址清单、逐笔构造 transfer 调用并等待回执,异常交易自动记录并重试,整体耗时从人工操作数天压缩到脚本数十分钟。
场景 3:链上事件监控与数据索引
阶段:实时事件流 / 数据管道
解决的问题:轮询区块获取事件延迟高且浪费 RPC 配额。用 WebSocketProvider 加 contract.on 做推送订阅,用 getLogs 做历史回扫,以 lastSyncedBlock 为断点实现增量索引。
具体案例:某 NFT 数据分析平台监控巨鲸转账与合约大额异动:事件推送相比轮询将监控延迟从分钟级降至秒级(P95 从约 60 秒降到 5 秒内),断线重连后从未处理区块回扫,未出现数据缺口。
常见误区
误区我项目里用的 ethers 5,升级到 6 只要把包版本改一下就行,API 差不多。
正解v6 是破坏性大版本:BigNumber 类被原生 bigint 取代(余额、交易值等从对象变为 bigint、不能用 .toString() 之外的旧方法),ethers.utils 命名空间被移除(parseEther 等变为顶层函数),providers 等子命名空间扁平化,Contract 创建方式也变了。v5 代码不迁移直接跑 v6 会大面积报错,需对照官方迁移指南逐项修改。
为何常见:v5 长期是 DApp 事实标准,教程与存量代码大多基于 v5,升级 v6 后教程没跟上,很多人误以为只是小版本更新。
误区反正是测试网或小金额,把私钥放在前端或仓库里没关系,方便调试。
正解私钥一旦出现在前端代码、Git 历史、日志等任何可被他人获取的位置,该地址资产就处于风险中。生产环境私钥应只存放于后端环境变量或托管签名服务(如 KMS、云钱包),前端通过 BrowserProvider 连接用户钱包、由用户授权签名。
为何常见:教程示例为了方便常把私钥写进代码,初学者照抄后形成了坏习惯。
误区sendTransaction 返回了 hash,交易就成功了,可以继续做下一步。
正解sendTransaction 只代表交易被节点接收并广播,尚未打包进区块;pending 状态可能持续,也可能因低估 gas 或 nonce 冲突被替换、丢弃。必须 await tx.wait() 拿到回执,并检查 receipt.status === 1(0 表示执行回滚)才能认为交易成功。
为何常见:返回的 hash 与回执长得很像,很多示例为了简洁略去 wait(),导致新手把两阶段过程当成一阶段。
误区直接用免费的公共 RPC 地址,不花钱也能跑生产服务。
正解免费公共 RPC 有严格速率限制,高峰期会频繁返回 429 或超时;部分公共节点还禁用写接口。生产环境应使用带配额的商业 RPC(Infura/Alchemy/QuickNode),并用 FallbackProvider 组合多路节点,自建节点可进一步降低成本与延迟。
为何常见:免费 RPC 在开发联调时体验尚可,容易让人低估生产流量的限流风险。
误区contract.on 注册之后就会一直收到事件,不用操心断线和遗漏。
正解WebSocket 连接会因网络抖动、节点重启等原因断开,断线期间的事件全部丢失。生产环境必须监听断线事件自动重连,并用 checkpoint(lastSyncedBlock)记录处理进度,重连后调用 getLogs 回扫补偿,防止漏数据。
为何常见:本地短时间测试看不到断线,容易让人误以为订阅是长期稳定可靠的。
面试题
Q1. ethers v6 中,ethers.formatEther(balance) 的返回值类型是什么?
- A.number 浮点数类型
- B.string 字符串类型← 正确答案
- C.bigint 大整数类型
- D.BigNumber 对象类型
答案:B。formatEther 把最小单位 wei(bigint)格式化为 ETH 字符串,便于界面精确展示、避免浮点精度丢失。number 会损失精度,bigint 是输入类型而非输出,BigNumber 类在 v6 中已移除。
Q2. ethers v5 中的 BigNumber 类,在 v6 中被什么替代?
- A.原生 number 类型
- B.原生 bigint 类型← 正确答案
- C.Uint8Array 字节数组
- D.Decimal 高精度小数
答案:B。v6 全面改用 JavaScript 原生 bigint 表示 wei 等大整数,移除了 BigNumber 类及 .mul()、.toString() 等方法,运算符改为原生的 +、*、/。bigint 与 number 直接运算会报错,需要显式转换。
Q3. 前端浏览器中,连接 MetaMask 等注入钱包应使用 ethers v6 的哪个类?
- A.JsonRpcProvider 类
- B.BrowserProvider 类← 正确答案
- C.WebSocketProvider 类
- D.FallbackProvider 类
答案:B。BrowserProvider 包装 window.ethereum(EIP-1193 注入对象),复用钱包内置的 RPC 连接与账户;JsonRpcProvider 直连固定 RPC 地址,WebSocketProvider 用于实时推送,FallbackProvider 用于多路容灾。
Q4. 以下哪些方式可在 ethers v6 中正确导入并使用库?
- A.
import { ethers } from "ethers"具名导入← 正确答案 - B.
import { JsonRpcProvider } from "ethers"具名导入← 正确答案 - C.
import ethers from "ethers"默认导入 - D.
const { ethers } = require("ethers")CJS 方式← 正确答案
答案:ABD。v6 只提供具名导出,默认导出已移除,因此 C 的默认导入会拿到 undefined;A、B 正确;D 在 CommonJS 环境中可用,require 返回的模块对象含 ethers 具名属性。
Q5. EIP-1559 类型交易中,用于控制用户最高支付费用的字段组合是?
- A.
gasPrice与gas旧式字段 - B.
maxFeePerGas与maxPriorityFeePerGas← 正确答案 - C.
nonce与gasLimit组合字段 - D.
value与data交易字段
答案:B。EIP-1559(以太坊的矿工小费费率机制)用 maxFeePerGas(单价上限)与 maxPriorityFeePerGas(验证者小费上限)取代单一 gasPrice。gasLimit 控制用量上限,nonce 是交易序号,均与费用定价无关。
Q6. 在 ethers v6 中,监听合约的 Transfer 事件可以怎么做?
- A.
contract.on("Transfer", cb)持续监听← 正确答案 - B.
contract.once("Transfer", cb)只监听一次← 正确答案 - C.
provider.getLogs(filter)拉取历史日志← 正确答案 - D.
contract.Transfer(cb)注册回调(事件)
答案:ABC。on/once 是实时订阅事件,getLogs 按过滤条件回溯历史日志。D 会把 Transfer 当作合约函数名的普通调用(若 ABI 中存在同名函数会触发交易),不是事件注册方式。
Q7. 在 ethers v6 中,调用 sendTransaction 后确认交易成功的正确做法是?
- A.直接 await tx 即拿到回执
- B.必须 await tx.wait() 等待← 正确答案
- C.同步读取 tx.receipt 属性
- D.provider.getTransaction 轮询
答案:B。sendTransaction 返回 TransactionResponse,仅代表广播成功;wait() 会等待打包并返回 TransactionReceipt,status 为 1 表示成功、0 表示回滚。tx 本身没有 receipt 属性,getTransaction 只是按 hash 查交易。
Q8. 使用助记词(BIP-39/BIP-44)创建并派生 HD 钱包,应使用 ethers v6 的哪个类?
- A.普通 Wallet 类实例
- B.HDNodeWallet 类实例← 正确答案
- C.Vault 保险库类实例
- D.SigningKey 密钥类实例
答案:B。HDNodeWallet 支持 fromPhrase(mnemonic, path) 按派生路径生成账户,是 HD 钱包(分层确定性钱包,一条助记词派生无限多账户)的实现;Wallet 仅持有单个私钥,SigningKey 只负责底层签名原语。
Q9. 在 ethers v6 中调用合约 view 函数(如 balanceOf)时,返回值是?
- A.
Promise<TransactionResponse>,需等待确认回执 - B.直接返回结果值(等待 Promise 解包)← 正确答案
- C.需要手动调用
.call()获取结果 - D.返回合约事件的原始日志字节数组
答案:B。view 函数只读不写链,v6 直接返回解码后的结果,async/await 即可拿到实际值,无需 wait()。A 描述的是写函数的行为,C 的 .call() 属于旧版调用方式,D 与事件日志无关。
Q10. 关于 ethers v5 迁移到 v6,以下说法正确的是?
- A.
ethers.utils.parseEther改为调用ethers.parseEther← 正确答案 - B.Contract 仍使用
getContract工厂方式创建实例 - C.
providers和utils命名空间均已移除并改为顶层导出← 正确答案 - D.原生
BigNumber类型仍需要在 v6 中显式手动使用
答案:AC。v6 将 utils、providers 等命名空间扁平化为顶层导出,A、C 正确;v6 用 new ethers.Contract(address, abi, runner) 创建实例,B 的 getContract 工厂已移除;BigNumber 被原生 bigint 取代,D 错误。
参考来源
- webpageEthers.js 官网项目入口与版本概览