作者:米都督

web3.0时代正悄悄来临。如果你想开发一个web3项目,或者想要未来从事web3工作,充实自己的web3简历,本文就是一篇很好的DApp开发技术入门教程。
这篇文章的主要内容包括:
智能合约创建和部署
Solidity语言入门
hardhat框架使用及本地环境搭建
本文较长,包括 20 余张图片、1 个 Hello-World 版本的 web3项目示例,以及完整的项目代码及注释。如果现在你正在使用手机阅读,建议先收藏本文;之后打开电脑,沿着本文的思路,仔细阅读。
一、Hardhat 是什么?
Hardhat框架,是一个以太坊DApp开发的本地集成开发套件。经常与之对比的是 Hardhat vs Truffle。相比其它作为web3基础设施的开发工具,Hardhat 更加轻量,采用插件化的思想,非常适合作为新手dapp开发入门的工具。
二、环境准备 —— 安装 Node.js
在使用 Hardhat 进行开发以前,首先需要在本地安装 Node 环境。
这里是官网的下载地址,根据自己操作系统的配置,选择合适的版本下载。
这里要注意,如果电脑上已经安装过,记得升级到 node v14 及以上的版本,更早的版本不支持 Hardhat。
看一下是否安装成功,分别输入:
node -vnpm -vnpx -v
我这里是 Windows 系统,用 PowerShell 执行一下上面几条命令,可以看到已经安装成功了。
三、开始第一个web3项目
1. 初始化
首先创建一个目录 hardhat-demo,进到这个目录里,执行
npx hardhat init
命令:
mkdir hardhat-democd hardhat-demonpx hardhat init
这时会问你几个问题,
一路回车即可。
npx hardhat init 的作用是,按照模板创建了一个示例项目。当出现 “Project created” 字样时,代表项目创建成功。
可以看到,在上面的截图里,提示了我们要安装依赖。为了能让这个示例项目跑起来,还需要安装几个依赖模块:
npm install --save-dev "hardhat@^2.9.1" "@nomiclabs/hardhat-waffle@^2.0.0" "ethereum-waffle@^3.0.0" "chai@^4.2.0" "@nomiclabs/hardhat-ethers@^2.0.0" "ethers@^5.0.0"
安装完以后,这个目录下的文件应该有这些:
这样,你就成功完成了第一个步骤。
2. 项目中的文件作用
创建好这个示例项目以后,这里来分析一下这些文件的作用。这一部分略微有一些枯燥,你也可以先阅读“
3. 智能合约的编译、部署和测试
”这一部分,之后再来看这些文件的作用。
① node_modules/、package-lock.json 和 package.json
这些是 node 项目的必须组成部分,包括了项目的配置信息、安装的依赖模块等,这里可以先无视。
② contracts/Greeter.sol
这个文件是项目中的重点,叫
智能合约文件。什么是智能合约呢?你可以把智能合约,同样理解为一种可以运行的程序。只不过这个程序比较特殊,它是运行在以太坊的 EVM 虚拟机环境上。并且,程序本身、程序的输入输出、运行结果,对所有人可见。
Solidity语言,是智能合约开发的主流语言之一。Solidity语言的入门教程,不是本文涉及的重点,这里只是简单提一下。
简单用 Java 类比一下:原来你写了一段 Java 代码,放进一个源文件叫作 HelloWorld.java,用 javac 执行编译,最终在自己的电脑或者服务器上,被 JVM 虚拟机执行;现在写了一段 Greeter.sol 代码,它也可以被编译,被执行,只不过运行环境变成了以太坊的 EVM 虚拟机。
初始化项目以后,自动生成的代码是这样的:
//SPDX-License-Identifier: Unlicense // 声明 licensepragma solidity ^0.8.0; // 定义版本号import "hardhat/console.sol";// 导入其它智能合约文件contract Greeter { // 定义一个合约,合约名字叫 Greeter string private greeting; // 这个合约的一个私有变量 constructor { // 合约的构造函数, // 当且仅当合约被部署时,会被执行一次 console.log; greeting = _greeting; } function greet public view returns { // 可以被外部调用的合约方法,view 声明了这是个只读方法, // 不会改变合约的状态 return greeting; } function setGreeting public { // 这是一个写方法,会改变合约的状态,且外部调用时会消耗 gas console.log; greeting = _greeting; }}第 1 行声明了 License,比如 GPL-3.0;
第 2 行声明了编译器版本,这里面强制指定了版本不得低于 0.8.0,也不能高于 0.9.0;
第 4 行导入了其它合约文件,这里面的
hardhat/console.sol
文件,是 Hardhat 框架自带的,是一个用于方便调试的合约文件,源码在这里;
第 6 行开始,就是合约文件的主体部分。从结构上看,它很像是在 C++ 或者 Java 中定义一个类,也包括了字段变量、构造方法、读方法、写方法等。这里面出现了几个特殊的关键字:
contract :声明一个合约;
memory:和 storage 关键字相反,代表了变量只会临时放在内存中,不会存储在合约的状态中;
view:声明该方法为只读方法,不会改变合约本身的状态。
③ scripts/sample_scripts.js
这个 Javascript 文件的作用,是将刚才的
Greeter.sol
智能合约编译并部署到链上。
const hre = require; // 声明依赖库async function main { // 定义 main 函数 // 获取合约 const Greeter = await hre.ethers.getContractFactory; // 部署合约,并得到一个合约的实例 Greeterconst greeter = await Greeter.deploy; await greeter.deployed; // 等待合约部署完成 // 打印日志,记录合约的地址 console.log; }// 执行 main 函数main.then => process.exit).catch => {console.error;process.exit;} );注释里写上了各行代码的作用。这里要注意几点:
合约的部署需要一定的时间,因为使用 await greeter.deployed 异步方法,等待合约部署完成。
用 Java 代码类比一下:Greeter 这个合约像是一个类,而每次部署得到的 greeter 像是 new 了一个类的对象。因此每次部署,都会生成出不同的对象,得到的合约地址也就不同。
④ test/sample_test.js
单元测试对于 web3开发来说同样重要。
sample_test.js
这个文件就是一个单元测试的文件,使用了
chai
这个测试框架:
// 引入依赖const { expect } = require;const { ethers } = require;// describe 和 it 指示了在进行 case testdescribe { it { // 这里和 scripts/sample_scripts.js 中的代码一样, // 也是获取合约 + 部署合约 + 等待部署完成 const Greeter = await ethers.getContractFactory; const greeter = await Greeter.deploy; await greeter.deployed; // 测试点 1 expect).to.equal; // 执行合约的写方法 const setGreetingTx = await greeter.setGreeting; await setGreetingTx.wait; // 测试点 2 expect).to.equal; } );});⑤ hardhat.config.js
接下来,我们会启动 HardHat,进行区块链本地环境的搭建部署。在正式启动之前,需要进行一些配置。
hardhat.config.js
这个文件,就是在 Hardhat 启动时,默认要读取的配置文件。
// 声明依赖require;// 声明 Hardhat 启动时执行的任务,下面这个任务的作用是打印账户的信息task => { const accounts = await hre.ethers.getSigners; for { console.log; } });// 声明配置项module.exports = {solidity: "0.8.4",};这里可以看到,文件的内容可以分为 3 个部分:

声明依赖的模块。
声明启动时执行的任务:在官方文档 file/tupian/20220910/create-task 里面有更加详细的说明,这里的示例 task 的作用是:启动 Hardhat 时,打印出本地这条链的默认账户。
声明配置项:更详细的说明可以参考官方文档 file/tupian/20220910/config 和 file/tupian/20220910/reference data-from-paste="1" data-diagnose->module.exports = { solidity: "0.8.4", // solidity 编译器的版本 networks: { // 网络配置情况,下面可以添加多个网络的配置 localhost: { // 本地网络 url: "http://127.0.0.1:8545" // 本地网络的 url }, hardhat: { // Hardhat 网络配置 chainId: 31337, // 链 ID,默认 31337 gasPrice: "auto" // gas 价格,默认 auto }, rinkeby: { // rinkeby 网络 // rinkeby 结点的 url url: "https://eth-rinkeby.alchemyapi.io/v2/123abc123abc123abc123abc123abcde", // 使用的账户列表 accounts: [privateKey1, privateKey2, ...] } },};
在“1. 初始化”中,执行了 npx hardhat init 以后,会生成出一个默认的配置文件。
一般来说,使用这个默认配置即可,不需要更改。
这里面有一个问题,上面的配置中,同时出现了 localhost 和 hardhat 两种网络。它们有什么不同呢?这里暂时先不提,在 “3. 智能合约的编译、部署和测试”这一部分中,你就会找到答案。
这样,项目中各个文件的作用就介绍完毕。可能略微有点枯燥,马上就可以进入实战环节了。
3. 智能合约的编译、部署和测试
① metaMask 钱包下载、安装和新建账户
metaMask 是什么呢?它是一个以太坊生态下的钱包,可以管理你的账户,支持多种网络。
metaMask 钱包本质上是一个浏览器插件
,这里有它的下载地址,然后像正常添加其它浏览器插件一样,添加上去就好。
接下来可以通过助记词的方式,生成一个新账户;也可以通过粘贴私钥的方式,导入你原有的账户。
这样,我们的 metaMask 钱包就准备好了。
② 在本地网络上部署合约
接下来我们要启动一个本地区块链网络节点。
首先打开一个 Terminal/Powershell 窗口,执行
npx hardhat node
命令:
这个命令的意思是,按照 hardhat.config.js 中声明的配置,启动一个本地区块链网络的节点。在每次启动时,默认会提供 20 个钱包账户和私钥,每个钱包提供 10000 个 ETH 做测试。
可以看到,Hardhat 启动时,把这 20 个账户的信息打印了出来。为什么打印账户信息呢?因为在 hardhat.config.js 中定义了这个执行任务;如果你忘记了,可以回过头看一下。
启动成功了,还可以用 metaMask 这个钱包再验证一下。
新增一个 localhost 网络,注意链 ID 要和
hardhat.config.js
中的 chainId 相同,默认是 31337.
导入一个账户,这里我选择了那 20 个自动账户中的第 1 个,把私钥粘贴进去。
这里注意一下,这些账户仅仅是为了本地测试使用,千万不要在以太坊主网使用这些账户地址!切记!
现在,本地网络环境已经就绪,我们把
contracts/Greeter.sol
部署到本地网络上。
打开另一个 Terminal/PowerShell,执行 npx hardhat run .scriptssample-script.js --network localhost:
于是会执行 scripts/sample_scripts.js 中的 main 方法,并且打印出了 Greeter 这个合约的部署地址。
同样地,在启动本地网络的窗口中,也可以看到刚才这个合约的部署情况:
在部署合约时,会执行 constructor 构造函数,所以 console.log; 这句被执行并打印出来。
部署合约需要消耗 gas,所以可以看到第一个账户中的余额不再是 10000 个 ETH 了:
再执行一次 npx hardhat run .scriptssample-script.js --network localhost,会发现虽然是同一份合约代码,但是合约地址发生了变化:
正如在 “
③ scripts/sample_scripts.js” 中所说,用 Java 代码类比一下:Greeter 这个合约像是一个类,而每次部署得到的 greeter 像是一个类的对象,因此每次部署,生成出了不同的对象,得到的合约地址也就不同。
我们把 scripts/sample_scripts.js 稍作修改,就可以得到web3版本的 Hello-World 了:
const hre = require;async function main {const Greeter = await hre.ethers.getContractFactory;const greeter = await Greeter.deploy;await greeter.deployed;console.log;}main.then => process.exit).catch => {console.error;process.exit;});到这步为止,我们的 Greeter 合约就成功在本地网络上部署好了,也成功使用 metaMask 钱包,进行了本地网络连接、测试账户导入。
③ Hardhat 其它常见的命令
刚才我们学习了 Hardhat框架的 2 个命令:
npx hardhat node 命令,启动区块链网络一个本地节点
npx hardhat run .scriptssample-script.js --network localhost 命令,把合约部署到了本地网络上。
这一部分,会说明一下其它 Hardhat 的实用命令。
npx hardhat test 这个命令用于执行单元测试,在示例中,也就是运行了 这个文件: 这代表测试成功。我们稍微修改一下 test/sample_test.jssample_test.js 文件:const { expect } = require;const { ethers } = require;describe { it { const Greeter = await ethers.getContractFactory; const greeter = await Greeter.deploy; await greeter.deployed; expect).to.equal; // wait until the transaction is mined const setGreetingTx = await greeter.setGreeting; await setGreetingTx.wait; expect).to.equal; }) ;});
再次执行命令,就会发现测试失败了:
你可能会问一个问题:在 sample_test.js 的代码中,同样出现了合约的部署语句,那么这个合约是被部署到了哪里呢?再次被部署到了 localhost 这个本地网络上吗?
答案是否定的。
还记得在 “⑤ hardhat.config.js” 一节中提过,有两个很相似的网络配置: localhost 和 hardhat。当执行 npx hardhat test 命令时,会内置创建 hardhat 网络,并在 hardhat 网络上完成合约部署、方法调用等,不会部署在 localhost 网络上。这一点一定要注意。
npx hardhat console 启动一个控制台程序,方便交互式输入输出。例如输入 config 查看配置情况: 在 npx hardhat compile 在执行 编译。 我们把 artifacts 和 cache 目录删掉,现在的目录结构应该是这样: 现在我们执行 所以,这个命令的作用就是 编译。cache 是编译出来的缓存文件夹。artifacts 目录下的文件很重要,看一下 这个文件和原始文件 npx hardhat clean 这个命令和 最后,还有一个重要的命令,叫作 四、结语 这就到了结语部分? 是的。跟随本文,你已经掌握了 Hardhat 开发框架的基本用法;创建了一个web3项目;编写修改了智能合约;执行了单元测试;启动运行了本地区块链节点;将合约文件成功进行编译和部署;还安装了 metaMask 钱包,并连接了本地网络和测试账户进行测试。 但是,现在这个项目,和我们编写的第一个 C++ Hello-World 控制台程序一样,还缺乏一个对用户友好的前端页面。如何快速编写一个前端页面?如何连接用户钱包?如果调用智能合约的方法?这些会放在下一篇文章中进行讲解。 有不清楚的地方,欢迎在评论区留言讨论;如果觉得本文不错的话,欢迎点赞、收藏、转发。感谢支持! 作者:米都督sample_scripts.js 中的代码语句,可以换成在 console 中执行:npx hardhat run .scriptssample-script.js --network localhost 部署合约的时候,其实 hardhat 偷偷帮你做了一件事情:npx hardhat compile 命令,执行成功后,会发现刚才删掉的 artifacts 和 cache 目录,又重新生成出来了。artifacts/contracts/Greeter.sol/Greeter.json 这个文件:{"_format": "hh-sol-artifact-1","contractName": "Greeter","sourceName": "contracts/Greeter.sol","abi": [{"inputs": [{"internalType": "string","name": "_greeting","type": "string"}],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "greet","outputs": [{"internalType": "string","name": "","type": "string"}],"stateMutability": "view","type": "function"},{"inputs": [{"internalType": "string","name": "_greeting","type": "string"}],"name": "setGreeting","outputs": [],"stateMutability": "nonpayable","type": "function"}],"bytecode": "0x......","deployedBytecode": "0x......","linkReferences": {},"deployedlinkReferences": {}}contracts/Greeter.sol 有点儿像,又有点儿不像。它就是源文件编译出来的样子。这个文件很重要,我们之后还会用到它。npx hardhat compile 的作用恰好相反,是把编译出来的文件清理一下。npx hardhat help,这条命令会展示 npx hardhat 系列命令的用法:当你忘记的时候,就可以用这条命令查询一下。


