这篇从零开始的 Hardhat 学习笔记,主要讲基本使用和部署智能合约。我写的时候尽量按新人第一视角来,假设你对 Hardhat 完全没接触过,但对 Solidity 至少知道大概怎么写。Hardhat 是目前 EVM 系合约开发里最常用的框架之一,它解决的核心问题很直接:你写好的合约,怎么在本地快速编译、部署、调试、测试,而不是一上来就跑去有成本的链上试错。
如果你以前接触过其他开发框架,切到 Hardhat 会有一个明显感受:本地环境非常“真实”。它内置一条会在内存里自动挖矿的区块链节点,部署合约、调合约跟真链上的体验几乎一样,但不用花钱、不用等区块、不用求水龙头。对刚入门的人说,这条“先把本地跑通,再上测试网,最后才考虑主网”的路线,能帮你把每个环节分开踩坑,省下大量时间。
这篇会把你从空目录带到一条真实的本地链上,过程中你会完成安装、初始化、写合约、编译、部署、交互调用,最后把部署目标切换到公共测试网络。每一步我会解释为什么这么做,以及我实际踩过的坑。这篇文章不需要你预先装太多东西,一台电脑、一个 Node.js 环境就够。
1. 为什么选 Hardhat 而不是其他框架
1.1 Hardhat 到底是什么、解决什么问题
先别急着背概念。Hardhat 本质上是把智能合约开发里那些反复要做的动作统一封装起来的一套工具链。写合约只是其中一环,合约写完之后,你要决定用哪个版本的编译器、怎么把 Solidity 代码变成字节码和 ABI、怎么在本地模拟一个区块链节点、怎么发交易部署、怎么在脚本里调用合约函数。如果没有框架,这些步骤全部要自己想办法拼装。
Hardhat 把这些东西打包成了一个命令行工具,核心命令就几个:npx hardhat compile 负责编译,npx hardhat node 负责起本地节点,npx hardhat run 负责跑部署或交互脚本。最方便的是,Hardhat 有一个内置的本地网络,你在跑 run 命令时即使不起 node,它也会在内存里临时生成一条链给你用,脚本跑完网络就销毁,下一次再跑又是全新状态。
打个比方,真实链像是银行柜台,你拿合约部署就是递业务单。Hardhat 的本地网络就是一个模拟柜台,你的单据能不能被受理、流程哪里会卡壳,先在模拟柜台试清楚,最后再去真柜台办理。这个“模拟柜台”越像真的,你提前发现的坑就越多。
1.2 和早期主流框架、新兴方案的横向对比
我是从早期框架迁移过来的,对比感受比较深。早期主流框架强调的是“全家桶体验”,把部署、测试、监听打包得很完整,但升级维护速度经常跟不上网络变化。后来出现了一批直接用 Solidity 写测试和脚本的新派工具,执行效率高、测试体验很棒,但学习成本明显更高,对刚入门的人来说不太友好。Hardhat 正好卡在中间:脚本层用 JavaScript/TypeScript,测试生态成熟,同时它内部对 Solidity 错误堆栈的处理是目前我用过最清晰的。
我用表格给你一个直观对比:
| 维度 | Hardhat(脚本派) | 早期主流框架 | Solidity 原生风格工具 |
|---|---|---|---|
| 脚本语言 | JavaScript / TypeScript | JavaScript | Solidity |
| 本地节点 | 内置 Hardhat Network | 有,一般较早 | 有 |
| 调试体验 | console.log 直接打合约内部 | 相对有限 | 堆栈信息细 |
| 测试生态 | 集成 Chai/Mocha,资料多 | 成熟 | 上手偏难 |
| 学习曲线 | 低,文档多 | 中等 | 偏高 |
| 适合场景 | 新人学习、中小团队、快速原型 | 存量老项目 | 性能敏感、重测试的团队 |
我日常推荐 Hardhat 给新人的另一个原因,是它的插件体系。官方提供了一个插件集合包,装上之后 ethers、测试框架、合约验证这些高频能力全都有了,不需要自己一个个找库拼装。对刚起步的人来说,少一次版本匹配失败,就少一次劝退。
1.3 这篇能让你完成什么
我不喜欢那种“看完本文你将掌握很多知识”的空话。这篇结束之后,你手上会有一个能跑的 Hardhat 项目模板,包含一个极简合约、一个部署脚本、一个交互脚本。你自己能解释清楚 artifacts 和 cache 是干什么用的,知道怎么在本地起链部署,也知道要上测试网需要改哪几个配置。下一篇文章再往里面加测试、加验证插件,都是在今天这个项目骨架上长出来的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:从 Node.js 到项目初始化
2.1 先检查 Node.js 版本
Hardhat 是跑在 Node.js 上的,所以第一步不是装 Hardhat,而是确认你的 Node 版本够用。官方对版本有要求,如果你装的版本太老,npm install 可能直接失败,或者装完之后运行时报各种看不懂的模块错误。建议直接上长期支持版的 Node,不用追求最新版本,因为一些激进的新版本在生态兼容上反而容易翻车。
在命令行先跑一下:
bash复制node -v
npm -v
我常用的版本管理工具是 nvm,它可以在同一台机器上安装多个 Node 版本,切换方便。如果你之前系统里已经有 Node,但版本偏低,不要用 sudo 去硬装 Hardhat,正确的做法是用 nvm 装一个新版本,切过来再继续操作。sudo 装全局包的问题在于权限混乱,后面一旦遇到 EACCES 权限报错,排查起来非常浪费时间。
版本确认没问题之后,把项目目录建出来:
bash复制mkdir hardhat-first
cd hardhat-first
npm init -y
npm init -y 会生成一个 package.json,这就是你的项目容器。后面每装一个工具包,npm 都会把依赖信息往里写,别人拿到这个项目时可以通过它恢复环境。
2.2 安装 Hardhat 并初始化项目骨架
初始化之前先装 Hardhat 本身:
bash复制npm install --save-dev hardhat
注意我用的是 --save-dev,因为 Hardhat 属于开发工具链,只在本地开发、测试、部署脚本时使用,它不是合约自身的生产依赖。这个习惯虽然小,但对后续理解 package.json 里的依赖分类很有帮助。
装完之后执行:
bash复制npx hardhat init
这里 npx 会优先使用当前项目里安装的 Hardhat,而不是全局版本。交互界面会问你创建什么类型的项目,选 JavaScript 项目即可,TypeScript 可以先放一放,等熟悉了再切换不迟。初始化过程中它会问要不要安装一些依赖,选是。之后你的项目目录会变成这样:
text复制hardhat-first/
├── contracts/ # Solidity 合约文件放这里
│ └── Lock.sol # 官方自带示例合约
├── scripts/ # 部署和交互脚本
│ └── deploy.js
├── test/ # 测试文件
│ └── Lock.js
├── hardhat.config.js # Hardhat 核心配置文件
├── package.json
├── package-lock.json
└── .gitignore
如果你之前用的是别的框架,这个结构会感觉眼熟:源码、脚本、测试分开,配置在根部。这个约定最大的好处是工程项目之间切换成本低,你不需要重新适应目录习惯。
2.3 初始化模板里的 Lock.sol 是什么
模板自带的 Lock.sol 是一个带锁定期限制的提款合约,部署脚本会在部署后等待一段时间,再允许所谓的 owner 提款。它用来演示状态变量、构造函数、时间锁、错误处理这些基础写法是有意义的。
但我的建议是,新手入门阶段不要花太多时间研究它。模板合约是给你“能跑起来”用的,你要真正理解 Hardhat 的流程,最好把它换成自己写的一个极简合约。这样编译、部署、回调每个环节发生什么你都能对照上。可以先把模板文件删掉:
bash复制rm -rf contracts/Lock.sol scripts/deploy.js test/Lock.js
后面我们自己写文件。
3. 项目结构:理解 Hardhat 项目的骨架
3.1 每个目录是干什么的
先说过度的目录。contracts/ 只放 Solidity 源码,不要在里面混部署脚本。scripts/ 放你用 JavaScript 写的执行脚本,比如部署、调用、批量事务脚本。test/ 放自动化测试文件,Hardhat 会把这些文件交给内置的测试运行器执行。
真正容易让人犯迷糊的是 artifacts/ 和 cache/。这两个目录在第一次编译之后才会出现。cache/ 是 Hardhat 的缓存目录,它记录上次编译时的输入输出信息,用来加速二次编译。artifacts/ 是编译产物目录,里面包含每个合约的 ABI 和字节码等关键数据。它们都是自动生成的,不要手动改。如果你改了配置或者合约却不生效,可以删掉 cache/ 和 artifacts/ 再重新编译,比排查半天的灵异报错快得多。
.gitignore 也很重要。一个合格的项目应该默认把你本地的密钥文件、node_modules、cache、artifacts 都排除在版本库之外。初始化生成的 .gitignore 通常会覆盖常见情况,但后面我们要用的 .env 文件,你需要额外确认是否在里面。
3.2 hardhat.config.js 到底配置了什么
刚开始项目里的 hardhat.config.js 长这样:
javascript复制require("@nomicfoundation/hardhat-toolbox");
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.24",
};
它做了两件事:引入官方插件集合包,把编译器默认版本设置为 0.8.24。这就是一个最小可运行配置。实际项目里你大概率还要加上 networks 字段,告诉 Hardhat 除了内置网络之外还能连接哪些网络。
我给你一个加好注释的扩展版,后面部署本地节点和测试网都会用到:
javascript复制require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.24",
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
// 内置 Hardhat Network 的配置,链 ID 固定为 31337
hardhat: {
chainId: 31337,
},
// 手动执行 npx hardhat node 后,用 localhost 连接
localhost: {
url: "http://127.0.0.1:8545",
},
},
};
这里有一个很常见的混淆点:hardhat 网络的 chainId 是 31337,而 localhost 的默认 RPC 端口是 8545。很多人分不清 chainId 和端口,记住一句话:chainId 是区块链身份,端口是本机程序监听的地址。你起的那个本地节点,chainId 就是 31337,HTTP 监听端口默认才是 8545。
3.3 插件集合包:为什么装上它就能用 ethers
你在第 2 章初始化时,Hardhat 会问要不要安装官方推荐的依赖,那个东西我习惯叫插件集合包。它把日常开发用得最多的一批能力打成一个包,包括与 ethers 交互的能力、测试工具、覆盖率工具、合约验证能力等等。
在 hardhat.config.js 顶部写一行 require("@nomicfoundation/hardhat-toolbox"),Hardhat 启动时就会加载这些能力。以后如果你的使用场景超出默认集合,可以单独安装其他插件,在配置里再 require 一行即可。这个“按需加载”的设计是 Hardhat 生态扩展性好的核心原因。对新人来说,暂时不需要深究插件机制,只要知道默认集成的已经够你用很久了。
4. 写一个极简合约并用 Hardhat 编译
4.1 自己动手写一个 SimpleStorage
为了不掺入模板逻辑,我们自己写一个极简存储合约。它只有两个功能:存一个数字、读一个数字。虽然简单,但它覆盖了合约开发最核心的要素:状态变量、事件、读写函数。
在 contracts/ 目录下新建 SimpleStorage.sol:
solidity复制// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract SimpleStorage {
uint256 private storedNumber;
event NumberChanged(address indexed changer, uint256 oldValue, uint256 newValue);
function setNumber(uint256 _newNumber) public {
uint256 oldValue = storedNumber;
storedNumber = _newNumber;
emit NumberChanged(msg.sender, oldValue, _newNumber);
}
function getNumber() public view returns (uint256) {
return storedNumber;
}
}
这里 storedNumber 是状态变量,它会永久保存在链上。setNumber 是交易型函数,会改变链上状态,需要签名和 gas。getNumber 是 view 函数,只读不写,所以调用它不需要花 gas,在本地查询时几乎是立即返回。
事件 NumberChanged 是给链下世界看的信息。以后你用脚本或者前端监听这个事件,就能知道谁在什么时候把数字从多少改成了多少。刚开始你可能觉得事件没用,但它是智能合约对外通信的主要通道。
4.2 编译合约:npx hardhat compile
写完合约后,回到项目根目录执行:
bash复制npx hardhat compile
第一次编译时,Hardhat 会根据配置里的编译器版本,从编译器源下载对应的 solc 版本,所以第一次通常会慢一点。看到类似这样的输出,说明编译成功:
text复制Compiled 1 Solidity file successfully.
如果你把 pragma 写成 ^0.8.24,Hardhat 会优先使用配置里指定的 0.8.24 版本去编译。万一你的 pragma 范围不包含配置里的版本,就会报版本不匹配。所以最简单的方式是让 hardhat.config.js 里的版本和合约顶部的 pragma 保持一致,新手阶段不要刻意去玩版本范围。
4.3 编译之后生成了什么:ABI 和字节码
编译成功后,artifacts/contracts/SimpleStorage.sol/ 目录下会生成 SimpleStorage.json。这里面最核心的两部分是 ABI 和 bytecode。
ABI 是合约的接口说明书。它用 JSON 描述了合约里有哪些函数、每个函数接受什么参数、返回什么类型。以后脚本通过 ethers 调用合约,实际上就是拿 ABI 里的描述去编码调用数据。
bytecode 是合约编译后的机器码,部署合约时,这个字节码会被放进交易数据里发给区块链节点,节点执行后得到一个合约地址。你可以亲手打开 JSON 文件看一眼,搜一下 "abi" 和 "bytecode" 这两个字段,会对整个部署底层逻辑理解得更踏实。
4.4 新手最容易遇到的编译错误
| 报错片段 | 常见原因 |
|---|---|
| requires different compiler version | 合约 pragma 与配置里的 solc 版本不匹配 |
| ParserError: Expected ... | 语法错误,最常见是漏了分号或者括号不匹配 |
| DeclarationError: Identifier already declared | 变量名重复声明 |
| TypeError: Invalid type ... | 给函数传了错误类型,比如把字符串传给 uint 参数 |
编译报错时,Hardhat 会在终端里标注具体文件和行号,注意看 --> contracts/xxx.sol:行号 后面的内容,那里会画一个 ^ 指向出错位置。不要看完整段报错,先从第一行错误定位开始,改完再编译,一般两三轮就能过。
5. 本地部署:把合约“放上链”
5.1 部署脚本怎么写
部署的本质,是把包含合约字节码的交易发送给节点,节点执行构造函数后返回一个合约地址。Hardhat 脚本做的事情就是把这一步用 JavaScript 串起来。
在 scripts/ 目录下新建 deploy.js:
javascript复制const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("部署账户:", deployer.address);
const balance = await ethers.provider.getBalance(deployer.address);
console.log("账户余额:", ethers.formatEther(balance));
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const contract = await SimpleStorage.deploy();
await contract.waitForDeployment();
const contractAddress = await contract.getAddress();
console.log("SimpleStorage 部署到:", contractAddress);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
我逐行解释一下。ethers.getSigners() 返回当前网络里可用的签账户。在本地 Hardhat 网络里,默认会给你派生十几个带测试余额的账户,第一个就是 deployer。getContractFactory 会从 artifacts/ 里找到 SimpleStorage 的 ABI 和字节码,然后 deploy() 发出部署交易。
这里有个 Ethers v5 和 v6 的重要区别:如果你在网上搜到老教程,里面写的是 await contract.deployed(),但新版里这个方法已经废弃,正确写法是 await contract.waitForDeployment()。如果用老写法,会直接报 deployed is not a function。因为我们在工具箱里预装了新版本的 ethers,所以请统一使用 waitForDeployment()。
5.2 把部署脚本跑起来
在项目根目录执行:
bash复制npx hardhat run scripts/deploy.js
你会看到类似输出:
text复制部署账户: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
账户余额: 10000.0 ETH
SimpleStorage 部署到: 0x5FbDB2315678afecb367f032d93F642f6418aa770
这段输出意味着,Hardhat 在内存里临时启动了一条链,帮你部署好了合约,然后整个环境就销毁了。注意每次跑这个脚本,合约地址大概率是一样的,因为临时网络的初始账户和 nonce 都是从零开始,所以结果是确定性的。
如果你只想快速验证部署流程,这条命令就够了。它不需要你先起节点,是 Hardhat 内置网络自动创建的临时链。但如果你想保留链上状态,让它持续运行,方便你用其他终端反复查,就要用到 node 命令。
5.3 手动起一个能查询的本地节点
打开一个终端,执行:
bash复制npx hardhat node
这个命令会启动一个持续运行的本地节点,并在控制台打印一批账户地址和私钥。请注意,这些私钥是公开的固定测试私钥,任何人都会在文档里看到,所以绝不能用于生产环境。它们存在的意义就是方便你把测试交易发到本地链。
然后新开一个终端,注意这次要加 --network localhost:
bash复制npx hardhat run scripts/deploy.js --network localhost
为什么这里要加参数?因为默认的 run 使用的是内置的临时内存网络,跟手动启动的节点是两回事。加了 --network localhost 之后,Hardhat 会读取配置里 localhost 的地址,连到 http://127.0.0.1:8545 这个节点上。你会在第一个终端里看到一堆交易日志,那个就是节点在实时打印链上活动。
保留这个节点别关,下面交互脚本要连它。
5.4 读取合约、调用合约
部署之后,把上一步拿到的合约地址填到一个新脚本里。我习惯把这种操作写成一个独立脚本,方便反复运行。
在 scripts/ 下新建 interact.js:
javascript复制const { ethers } = require("hardhat");
async function main() {
const contractAddress = "0x5FbDB2315678afecb367f032d93F642f6418aa770";
const contract = await ethers.getContractAt("SimpleStorage", contractAddress);
const before = await contract.getNumber();
console.log("调用前 storedNumber:", before.toString());
const tx = await contract.setNumber(42);
const receipt = await tx.wait();
console.log("写入交易哈希:", receipt.hash);
const after = await contract.getNumber();
console.log("调用后 storedNumber:", after.toString());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
执行:
bash复制npx hardhat run scripts/interact.js --network localhost
你会看到 before 是 0,写入 42 之后 after 是 42。这里有个细节:getNumber() 返回的是一个 bigint 类型,直接 console.log 会得到可读的数字,但如果你在复杂表达式里参与字符串拼接,就必须调用 .toString() 转成字符串。这是 ethers v6 里最容易踩的小坑。
getContractAt 和 getContractFactory 的区别你要搞清楚:getContractFactory 是从 artifacts 里拿 ABI 和字节码,用来做全新部署;getContractAt 只需要 ABI 和已存在的地址,就可以对链上已有合约发起调用。后者非常适合前端或者运维脚本复用。
5.5 Hardhat 内嵌的 console.log 调试
如果不满足于链下打印,Hardhat 还允许你在 Solidity 合约里直接打日志。这在本地开发阶段非常爽。
在合约顶部加一行:
solidity复制import "hardhat/console.sol";
然后在函数里加:
solidity复制console.log("old value:", oldValue);
console.log("new value:", _newNumber);
重新编译、部署、调用,你会在终端里看到合约内部的打印输出。这个能力对排查合约业务逻辑很有帮助,因为它能在合约真正执行的地方输出状态,比在脚本里猜合约行为直观太多。
有一个点必须提醒:console.sol 是 Hardhat 提供的开发工具,不是链上的正式合约库。你如果把它留在合约里部署到真实网络,合约体积会变大,而且这个日志能力在生产环境没有意义。我一般会在代码准备上正式环境前,把 console.sol 的 import 和所有 console.log 调用清理干净。一个手写检查方法是用搜索功能全项目搜一下 console 关键字,确认没有遗漏。
6. 部署到公共测试网络:配置与注意事项
6.1 准备三样东西
本地跑通之后,下一步是部署到公共测试网络。测试网络跟本地网络的区别是:它是一个公开运行的真实区块链网络,区块由其他人共同维护,你需要真正的账户私钥和测试币来支付 gas。但测试币没有真实价格,所以你可以放心练习,不会亏钱。
部署到公共测试网络需要准备三样东西。第一,测试网络的 RPC 地址,也就是你通过哪个入口连上这条链,可以从节点服务商或者网络官方文档获得。第二,一个账户的私钥,这个账户里需要有测试币。第三,在 hardhat.config.js 里加上对应网络配置。
测试币的获取方式因网络而异,通常是去公共测试网的水龙头入口提交你的账户地址,它就会打一点测试币过来。如果水龙头不方便,也可以让身边有测试币的朋友直接转给你,金额不需要多,够部署几次合约就行。
6.2 私钥不能写死在代码里
很多人第一次接触测试网时,直接把私钥贴在配置文件里,这在学习环境里问题不大,但很容易养成坏习惯。你的配置文件可能会被提交到代码仓库,或者分享给别人,一旦私钥泄露,账户里的资产就危险了。正确的做法是用环境变量。
先装一个小工具:
bash复制npm install --save-dev dotenv
然后修改 hardhat.config.js:
javascript复制require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: "0.8.24",
networks: {
testnet: {
url: process.env.RPC_URL,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};
在项目根目录新建 .env 文件:
text复制RPC_URL=https://你的测试网RPC地址
PRIVATE_KEY=0x你的私钥
然后立刻检查你的 .gitignore 文件,确保里面有一条 .env。如果没有,补上。这一步不是可选项,是安全底线。我之前见过太多把私钥提交到公开仓库的教训,不要觉得“这次只是学习无所谓”。
6.3 一条命令部署到测试网
配置好之后,部署命令几乎不变,只是换一下网络参数:
bash复制npx hardhat run scripts/deploy.js --network testnet
这次不会像本地那样秒回,因为交易要广播到公共网络,等待真实节点确认,通常在几十秒内完成。脚本里 waitForDeployment() 会一直等到交易确认,然后打印合约地址。你可以去区块链浏览器上输入这个地址,看到合约已经挂在链上。
这个阶段最容易遇到的坑是余额不足。报错信息里通常会直接提示你账户余额不够支付 gas。遇到这种情况,先确认测试币是否到账,再看 RPC 地址是否填错,不要急着改代码。另一个坑是 nonce 冲突,如果你多个终端同时用同一个私钥发交易,网络会拒绝某些交易,报类似 nonce 已被使用的错误。解决办法是同一时间只用一个终端操作同一个账户。
6.4 部署验证:为什么不能只用一条地址来证明成功
部署成功之后,你可能会觉得事情已经结束了。其实公共测试网还有一个重要能力:合约源码验证。所谓验证,是把合约源码提交到区块链浏览器上,让浏览器核对你部署的字节码和源码是同一个东西。这步做完,合约代码才会被公开显示,而不是只显示一堆字节码。
验证这个功能在项目初始化时默认可用的工具包里就有。它是一个独立命令,需要传入合约地址和构造参数。我打算放在系列后面单独讲,因为牵扯到多文件合约、构造函数参数编码等问题,不是第一篇必须掌握的。但你要知道,部署只是开始,把自己写的东西公开透明地展示出来,才是合格的发布流程。
7. 常见问题与排查:实测踩过的坑
7.1 安装和初始化阶段
| 现象 | 原因与对策 |
|---|---|
| npm install 非常慢或失败 | 网络环境导致,切换你所在地区访问更快的镜像源后重试 |
| 提示 EACCES permission denied | 不要用 sudo 装,改用 nvm 管理 Node 版本 |
| 输入 npx hardhat 提示 command not found | 大概率没在项目目录执行,或 hardhat 未安装成功,检查当前目录和 package.json |
| 运行时报 Cannot find module '@nomicfoundation/hardhat-toolbox' | 插件集合包没装上,重新执行 npm install 并确认 package.json 里有对应依赖 |
安装阶段的问题八成是环境问题,不要跟合约代码较劲。先把 Node 版本、npm 源、项目目录三个变量稳定下来,后面会顺很多。
7.2 编译和运行阶段
| 现象 | 原因与对策 |
|---|---|
| 报错 Source file requires different compiler version | 合约 pragma 和 hardhat.config.js 里的 solidity 版本不一致,统一即可 |
| 报错 Invalid account or private key | 私钥格式不对,检查是否带 0x 前缀、有没有多余空格或引号 |
| 配置改完不生效 | 检查是否保存文件,或者执行一下 npx hardhat clean 再编译 |
| 部署时提示 Transaction ... was not mined within ... | 大概率连接的网络不对,确认本地节点是否在运行,链是否匹配 |
其中 npx hardhat clean 是一个兜底命令。Hardhat 的缓存机制有时会保留旧编译结果,你改了合约、换了配置但运行结果还是老的。执行 clean 会清除 cache 和 artifacts,然后重新编译部署,能解决很多莫名其妙的“我明明改了啊”问题。
7.3 测试网点名推荐的排查习惯
从本地切到测试网后,报错信息会变得更复杂,因为交易可能真的被网络抛弃,也可能只是 RPC 节点响应慢。我的排查顺序是固定的:
第一步,先确认账户余额。第二步,确认为 --network testnet 而不是忘记加参数跑了本地。第三步,直接看 RPC 地址能不能通,用一个简单的脚本连上去获取当前区块高度。第四步,检查构造参数是否填错,很多合约部署失败是构造函数参数类型和数量不对。第五步,确认 nonce 没有冲突。
这套顺序下来,90% 的部署报错都能定位。真正剩下 10% 的怪问题,大多可以通过换一个 RPC 节点解决,或者去社区搜索同样的报错关键字。一定要学会复制完整报错信息,不要只截最后一行,因为有用的定位信息往往在错误堆栈的中部。
7.4 给新人的一句话经验
我最想强调的一点是:遇到报错不要慌,报错不是对你个人的否定,而是工具在给你提示。你先读第一行,再读最后一行,找它指向的项目文件路径,顺着路径去看具体代码,大部分问题都能定位到。千万不要把整屏报错删掉然后凭记忆乱猜,那样只会越改越乱。
最后分享一个我个人的操作习惯:每次新建 Hardhat 项目,我都会先把 npx hardhat node 跑起来,再另开一个终端执行一次部署脚本。这个过程十秒钟就能完成,但它能验证 Node 版本、依赖安装、配置解析、账户派生、编译产物、本地网络连接这六段链路是否通畅。等你切到测试网、写复杂合约时,一旦出了问题,你会很清楚从哪段链路开始排查。这篇就到这里,下一篇文章我会接着讲如何在 Hardhat 里写合约测试,建议你把今天的部署流程亲手跑一遍,跑通之后再继续。
