☰
Solidity工程化开发入门:用Hardhat搭建智能合约编译、部署与测试全流程
2026/9/28 6:33:52 网站建设 项目流程

说个很多Solidity新手都会掉进去的坑:在Remix里写完一个合约,觉得万事大吉,等到真要上手项目,才发现根本不知道代码怎么组织、怎么自动化测试、怎么批量部署。我第一次接真实项目时就是这个状态,翻文档翻了整整一个周末,才搞明白Hardhat这套工程化工具链的玩法。如果你也准备认真学Solidity,我建议直接跳过"在网页里写合约"的阶段,从Hardhat开始。

这篇是Solidity入门系列的第一篇,聚焦Hardhat框架本身。我们会从环境搭建、项目结构、配置解析、合约编写、编译部署到测试调试,把一套完整的开发流程跑通。内容面向零基础读者,但即使你已经写过几个简单合约,这篇文章里关于配置文件解析、部署脚本演进、常见报错排除的部分,也值得你花几分钟扫一遍——毕竟很多坑不是新手才踩,老手一样会翻车。

1. 先把框架选对:Hardhat凭什么成为Solidity开发者默认选择

以太坊智能合约的开发流程,本质上是一条流水线:写Solidity代码、编译成字节码、部署到链上、调用合约验证逻辑、测试各种边界情况。问题是,这条流水线上每一环都有独立的工具,新手很容易迷失在工具选择里。

目前主流的开发框架主要有三个:Hardhat、Truffle和Foundry。我用一张表格带你快速比较它们的定位差异:

框架核心定位语言生态调试能力适合场景
Hardhat工程化开发环境JavaScript/TypeScript内置console.log、堆栈追踪绝大多数项目,资料最全
Truffle老牌一体化框架JavaScript较弱,依赖外部工具遗留项目维护,已停止维护
Foundry快速测试与链上交互Solidity原生基于Rust,速度快对性能要求高的测试场景

很多人会问:既然Foundry那么快,为什么不直接用它?答案很简单:生态成熟度差距太大。Hardhat背后有Nomic Foundation在维护,插件生态覆盖了部署、验证、覆盖率、Gas报告、升级等几乎所有需求,而且以太坊官方文档、OpenZeppelin的大量示例都基于Hardhat。对入门者来说,跟着一个生态最完整的框架走,能少踩无数坑。

再说说Hardhat名字的来历,它和"硬帽"这个词有关系,官方寓意是"让开发者在本地拥有一顶安全的头盔"——同样一个Solidity合约,在本地跑和在真实链上跑,成本天差地别。真实主网上每笔交易都要付Gas费,测试网虽然免费但有速率限制,而Hardhat内置的Hardhat Network直接在本地模拟了一个完整的以太坊节点,交易秒级确认、无费用、可随时重置状态,这就相当于给合约开发装了一台"本地练习机",等逻辑全部验证通过,再花钱上真实赛场。

Hardhat Network的核心价值,就是让"快速迭代"成为可能。我调试一个合约逻辑时,最频繁的操作就是"改一行代码、跑一次测试、看一眼输出、再改",这个循环如果放到真实链上,光是等待区块确认就足以让人崩溃。

还有一个被很多人忽略的点:Hardhat的优秀栈追踪能力。当合约revert时,Hardhat会直接从Solidity源码层面告诉你出错的行号和调用栈,而不再是一堆让人头大的字节码。这一点在复杂合约联调时几乎是救命级别的功能。

2. 从空目录到Hello World:Hardhat环境搭建与项目初始化

2.1 确认环境:Node.js版本是第一个坑

Hardhat运行在Node.js环境上,所以第一步是装Node.js。这里有一个很多新手会踩的坑:版本太老或太新的Node.js都可能让Hardhat安装失败。

我建议使用Node.js 16.14及以上版本(18.x和20.x的LTS版本都测试过没问题)。检查当前版本:

node -v

如果你机器上有多个Node版本,推荐用nvm(Node Version Manager)管理,切版本就一行命令,省去很多环境层面的混乱。

2.2 初始化项目并安装Hardhat

新建一个项目目录并进入:

mkdir hello-hardhat && cd hello-hardhat npm init -y

先执行npm init初始化package.json文件,再通过npm把Hardhat作为开发依赖安装:

npm install --save-dev hardhat

这里强调一个原则:永远使用本地安装,不要全局安装。全局安装看似方便,但不同项目对Hardhat版本要求不同,全局版本一旦升级可能直接拖垮旧项目。本地安装之后,用npx hardhat调用的一定是当前项目里的那个版本,可靠得多。

安装完成后,执行初始化命令:

npx hardhat init

这会进入一个交互式引导界面,问你几个问题:

  • 是否创建示例项目:建议选Yes,里面有一个现成的合约和测试,跑通它你就知道整个流程长什么样了
  • 使用JavaScript还是TypeScript:第一次建议选JavaScript,少一层类型配置,专注理解核心概念

初始化完成后,项目里会自动装好下面这些依赖:

  • hardhat:框架本体
  • @nomicfoundation/hardhat-toolbox:全家桶插件,集成了ethers.js、测试工具、覆盖率等常用能力
  • chai和mocha:测试框架

2.3 验证安装:跑一次示例流程

项目初始化完成后,先别急着改代码,直接按顺序执行三个命令验证环境:

npx hardhat compile npx hardhat test npx hardhat node

compile编译合约、test跑测试、node启动本地节点,三关都过了,说明你的Hardhat环境是健康的。后面每遇到诡异问题,我都建议先跑一遍这三关,快速判断问题出在环境还是代码。

3. hardhat.config.js拆解:网络、编译器与插件到底在配置什么

初始化完成后,项目根目录会出现一个hardhat.config.js或hardhat.config.ts文件,这是整个项目的"总开关"。先看一个最小可用的配置:

require("@nomicfoundation/hardhat-toolbox"); module.exports = { solidity: "0.8.24", networks: { hardhat: { chainId: 31337 } } };

就这么几行,背后牵涉了三个层面的配置逻辑。

3.1 compiler配置:用什么版本的solc

solidity: "0.8.24"指定了Solidity编译器版本。为什么要单独指定?因为Solidity编译器每个版本之间可能存在细微行为差异,同一个合约在不同版本下编译出的字节码可能不同。为了保证"在本地编译和测试的结果,和CI服务器、队友电脑上一致",项目里必须锁定一个版本。

一个常见问题是:如果项目里引用了不同版本的库合约怎么办?Hardhat支持按目录指定编译器版本:

module.exports = { solidity: { compilers: [ { version: "0.8.24" }, { version: "0.7.6" } ], overrides: { "@openzeppelin/contracts/": { version: "0.8.24" } } } };

不过这个属于进阶用法,新手知道有这回事就行,见到版本冲突报错时知道来改这个配置。

3.2 networks配置:本地、测试网和主网的接线图

networks配置块定义了"你的合约可以部署到哪里"。默认配置中只有一个hardhat网络,也就是Hardhat内置的本地网络。除了它之外,最常见的还有两类:

一是本地独立节点,使用npx hardhat node启动一个持续运行的本地网络,配置起来就是指定url地址:

networks: { localhost: { url: "http://127.0.0.1:8545" } }

二是公开测试网,比如Sepolia。真实部署需要两类信息:RPC节点地址和部署账户私钥。RPC地址可以从Infura、Alchemy等节点服务商免费申请,私钥是你在测试网部署时用来签交易的账户密钥。

注意:这里的私钥是测试网私钥,即使泄露理论上损失也有限,但依然不建议直接写在配置文件里。我习惯用dotenv管理环境变量,具体做法在第5章部署部分详细说。

3.3 插件系统:什么功能都可以往里加

第3行的require("@nomicfoundation/hardhat-toolbox")加载了toolbox插件。这个插件是Hardhat官方推荐的实用工具集合,内部打包了:

  • ethers.js集成:提供hre.ethers对象,用于部署和交互
  • chai匹配器:让测试断言更贴合合约语义
  • Hardhat Network的vendor功能:支持模拟任意链的账户和状态
  • 合约大小检查:防止合约体积超出以太坊限制(24KB)

插件的意义在于解耦:核心框架只负责编译、部署、任务调度等基础设施,具体功能(比如覆盖率、Gas报告、合约验证)通过插件按需加载。这种设计让Hardhat的生态越滚越大,新功能出现时不需要等框架更新。

4. 合约编写与编译:从Solidity源码到ABI和Bytecode

4.1 第一个合约:一个带增减功能的计数器

初始化的示例项目里已经有一个Lock合约,但结构对入门者来说偏复杂。我们在contracts目录下新建一个Counter.sol,写一个最简单的合约:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; contract Counter { uint256 public count; function increment() public { count += 1; } function decrement() public { require(count > 0, "Counter: count cannot be negative"); count -= 1; } function getCount() external view returns (uint256) { return count; } }

逐行拆解一下核心结构:

  • SPDX-License-Identifier是许可证声明,以太坊社区要求开源合约标注许可证类型,MIT是最宽松的常见选择,不写会编译警告
  • pragma solidity ^0.8.24;声明了编译器版本范围。^0.8.24意味着"大于等于0.8.24且小于0.9.0",这样可以兼容不会破坏现有代码的更新
  • uint256 public count;是状态变量,public会自动生成一个同名的getter函数,方便外部读取
  • increment和decrement是修改合约状态的函数,会消耗Gas
  • getCount是view函数,只读取状态、不修改链上数据,不消耗Gas(在外部调用时)
  • require是Solidity的断言函数,条件不满足时回滚整个交易并抛出错误信息

4.2 编译:npx hardhat compile到底做了什么

执行编译:

npx hardhat compile

第一次编译会慢一点,因为需要下载对应的solc编译器。后续再编译,Hardhat会做增量编译,只重新编译有变更的文件,速度很快——这个缓存机制是默认开启的,不需要额外配置。

编译完成后,项目里会多出一个artifacts目录。这个目录里有每个合约的JSON文件,重点关注两个字段:

  • abi:一个描述合约接口的JSON数组,包含函数名、参数类型、返回类型、是否为view/payable等信息。外部程序依赖ABI来编码调用数据、解析返回结果,可以理解成"合约的函数接口说明书"
  • bytecode:Solidity编译生成的字节码,也就是最终要部署到链上的机器码,包含合约完整的逻辑和代码

还有一个细节:artifacts目录之外还有个cache目录,里面是Hardhat对编译信息做的索引。这两个目录都应该加入.gitignore,因为它们是从源码重新生成的产物,存在版本库里只会制造噪音。

4.3 编译期常见报错速查

我整理了几个高频编译错误,你看一眼心里有数:

报错信息常见原因解决办法
Source file requires different compiler version项目锁定的solc版本和合约的pragma声明的版本范围没有交集在config中调整solidity版本
Contract has not been fully specified有抽象函数未实现,或引用了未导入的合约检查继承关系和import语句
DeclarationError: Identifier not found变量或函数名写错到对应行检查拼写
TypeError: Function override specified but did not override anything标了override但父合约并没有这个函数检查继承合约函数签名
ParserError: Expected token语法错误,多半是缺括号或分号到报错行附近检查

编译报错时,Hardhat会直接给出Compilation failed加上具体文件路径和行号,定位成本很低。

5. 部署脚本实战:把合约送上本地网络和Sepolia测试网

5.1 为什么部署要单独写脚本

在Remix里部署就是点个按钮,在Hardhat里部署则要写脚本,很多新手在这里不习惯。但脚本化恰恰是工程化的价值:同一份部署逻辑,可以原样跑到本地网络、测试网、主网,只是切换网络配置而已,无需人工重复点击,而且过程可审计、可版本管理、可被CI集成。

在scripts目录下新建deploy.js:

const hre = require("hardhat"); async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address); const Counter = await hre.ethers.getContractFactory("Counter"); const counter = await Counter.deploy(); await counter.waitForDeployment(); console.log("Counter deployed to:", await counter.getAddress()); } main().catch((error) => { console.error(error); process.exitCode = 1; });

这段脚本做四件事:获取部署账户签名者、从合约工厂获取Counter的部署对象、部署合约、打印合约地址。注意waitForDeployment()是新版API,旧版常见写法是deployed(),如果你在别人的老教程里看到后者,记得换成新API,否则在最新版Hardhat里会报错。

执行部署:

npx hardhat run scripts/deploy.js

默认部署到内置Hardhat Network,每次运行后合约地址都会变,这是正常的——内置网络每次是全新状态。如果想要一个持续运行的网络,可以另开一个终端执行npx hardhat node,然后部署时指定网络名:

npx hardhat run scripts/deploy.js --network localhost

5.2 部署测试网:RPC、私钥和.env的最佳实践

部署到Sepolia测试网的流程和本地几乎一样,只是网络配置不同。首先申请一个RPC地址,以Alchemy为例,创建应用后会得到一个形如https://eth-sepolia.g.alchemy.com/v2/XXXX的URL。接着准备一个带有Sepolia测试币的钱包账户私钥。

我的做法是引入dotenv管理敏感信息:

npm install --save-dev dotenv

然后在hardhat.config.js最顶部加上require("dotenv").config();,再让配置引用环境变量:

require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config(); module.exports = { solidity: "0.8.24", networks: { hardhat: {}, sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [] } } };

项目根目录新建.env文件:

SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/你的key PRIVATE_KEY=你的账户私钥

同时把.env加入.gitignore,这个文件绝对不应该出现在Git仓库里。私钥一旦泄露,别人就能控制你的账户、转走测试币,极端情况下如果用了主网账户,后果是资产直接丢失。

配置完成后,执行:

npx hardhat run scripts/deploy.js --network sepolia

脚本不用改一行代码,网络切换完全靠配置驱动。这条"配置与逻辑分离"的思路,在真实项目中几乎天天用到。测试网上列出的合约,还可以通过npx hardhat verify --network sepolia <合约地址>验证源码,这用到了@socrates/hardhat-verify插件(toolbox里已包含),验证后合约就能在区块浏览器里看到源码,方便别人信任和审计。

5.3 Gas费用是个绕不开的话题

说到部署,Gas是无法回避的概念。在以太坊上,每笔交易耗费的Gas乘以Gas价格就是交易费用。部署合约是链上计算密集型操作,代价通常不小。在主网部署时,代码越复杂的合约Gas费越高,所以Hardhat才会提供合约大小检查插件——如果字节码超过24KB上限,合约就无法部署。这里是Hardhat在帮你在"上链之前"就把问题暴露出来,而不是等部署失败才着急。

6. 测试与调试:console.log之外的排错手段

6.1 为什么合约必须有自动化测试

Solidity合约部署后不可篡改,一旦上线,Bug就是永久的。这意味着测试不是可有可无,而是上线之前的最后一道防线。

在test目录下新建Counter.test.js,跑一次完整的测试流程:

const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("Counter", function () { let counter; beforeEach(async function () { const Counter = await ethers.getContractFactory("Counter"); counter = await Counter.deploy(); await counter.waitForDeployment(); }); it("初始值应该为0", async function () { expect(await counter.getCount()).to.equal(0); }); it("increment后count应该变为1", async function () { await counter.increment(); expect(await counter.getCount()).to.equal(1); }); it("decrement不能把count减到负数", async function () { await expect(counter.decrement()).to.be.revertedWith( "Counter: count cannot be negative" ); }); });

测试代码读起来像自然语言:"初始值应该为0"、"increment后count应该变为1"。这就是chai匹配器的功劳,配合hardhat-toolbox内置的ethers.js,测试里可以用真实账户签名发起交易,完整模拟链上行为。

执行测试:

npx hardhat test

输出中每个it都是一个测试用例,绿色对勾表示通过。如果在某条用例中合约报了错,Hardhat会把错误信息、回放栈和Gas使用情况一并打在终端里,这比"测试失败"四个大字有用得多。

6.2 console.log:合约里的临时调试窗口

Hardhat最受欢迎的功能之一,就是在合约中使用JavaScript风格的console.log。使用方式很简单,合约里直接调用:

// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import "hardhat/console.sol"; contract Counter { uint256 public count; function increment() public { count += 1; console.log("count is now:", count); } }

编译后用任意方式调用increment函数,终端就会打印出count is now: 1。这个功能只对Hardhat Network生效,不会污染正式链上的逻辑,因为正式环境不会运行在Hardhat里。

这里要强调一点:console.log是调试工具,不是日志系统。调试完后记得移除import,否则合约代码里残留调试信息,既不专业也可能引入意想不到的Gas开销(console的字节码在正式链上没什么意义但会占据合约体积)。

6.3 测试覆盖率的查看

toolbox里还集成了Solidity覆盖率工具,执行:

npx hardhat coverage

它会跑一遍测试并生成覆盖率报告,显示每个合约的函数、行、分支覆盖情况。覆盖率不是越高越好,但一个"核心逻辑覆盖率低于70%"的项目,上线前我都会打个问号——不是所有的路径都被测试到,就意味着有些边界情况还等着在真实链上爆出来。

7. 写在最后:给新手的几个实战建议

整个流程跑通之后,你已经具备了用Hardhat开发Solidity合约的基本能力。最后分享几个我认为最值得记住的实战建议。

第一,不要跳过测试直接部署。在Hardhat里写测试的成本其实很低,几行断言就能覆盖一个函数的核心路径。但一旦合约上链,任何Bug的修复代价都是一次新的部署、一次新地址分发、所有使用者位置的迁移。测试不是给框架看的,是给你自己上一份保险。

第二,写合约前先看看OpenZeppelin的模板。Counter这种玩具合约可以自己写,但真实项目里涉及Token、权限管理、升级机制,直接用经过审计的OpenZeppelin合约比自己造轮子安全得多。Hardhat项目里引入OpenZeppelin就是一条npm install的事:

npm install --save-dev @openzeppelin/contracts

第三,遇到报错先读原文,再复制去搜。Hardhat的报错信息通常已经精确到文件和行号,很多问题读一遍报错自己就明白了。直接复制报错去搜索引擎,反而会把你引到过时教程里去。

这个系列的下一篇文章,我会带你基于Hardhat实现一个完整的ERC-20代币合约,并把它部署到测试网,讲清楚合约构造函数参数、事件日志和合约交互这些进阶内容。在你动手把这篇文章里的Counter合约部署出去之前,建议先把Hardhat的编译、测试、部署三个循环多跑几遍,让肌肉记忆先形成——后面所有更复杂的东西,都是在这条循环里逐步叠加的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询