Web3 开发者入门完全指南:从零开始构建你的第一个 DApp
Web3 正在重新定义互联网的未来。作为一名开发者,掌握区块链和智能合约开发技能已经成为一项极具竞争力的能力。本文将带你从零开始,一步步搭建 Web3 开发环境,编写智能合约,部署到测试网,并构建一个完整的去中心化应用(DApp)。
无论你是前端开发者、后端工程师,还是对区块链技术充满好奇的初学者,这篇指南都能帮你快速入门。
一、Web3 开发环境搭建
1.1 安装 Node.js
Web3 开发工具链大量依赖 Node.js 生态,因此首先需要安装 Node.js(推荐 v18+)。
# 使用 nvm 安装 Node.js(推荐方式)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 20
nvm use 20
# 验证安装
node -v # 应输出 v20.x.x
npm -v # 应输出 10.x.x
1.2 安装 Hardhat(推荐)
Hardhat 是目前最流行的以太坊开发框架,提供了编译、测试、部署等一站式工具。
# 创建项目目录
mkdir my-web3-project && cd my-web3-project
# 初始化 npm 项目
npm init -y
# 安装 Hardhat 及其依赖
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
# 初始化 Hardhat 项目
npx hardhat
选择 "Create a JavaScript project",然后按回车使用默认配置。初始化完成后,项目目录结构如下:
my-web3-project/
├── contracts/ # 智能合约目录
│ └── Lock.sol # 示例合约
├── scripts/ # 部署脚本
│ └── deploy.js
├── test/ # 测试文件
│ └── Lock.js
├── hardhat.config.js # Hardhat 配置文件
└── package.json
1.3 可选:安装 Foundry
Foundry 是用 Rust 编写的高性能 Solidity 开发工具,速度极快,深受高级开发者喜爱。
# 安装 Foundry
curl -L https://foundry.paradigm.xyz | bash
foundryup
# 初始化 Foundry 项目
forge init my-foundry-project
cd my-foundry-project
# 编译合约
forge build
# 运行测试
forge test
💡 建议:初学者优先使用 Hardhat,熟悉后再学习 Foundry。两者都是优秀的工具,可以根据项目需求选择。
1.4 安装 VS Code 插件
推荐安装以下 VS Code 扩展来提升开发体验:
- Solidity (Juan Blanco):Solidity 语法高亮和智能提示
- Hardhat VSCode:Hardhat 官方插件
- Prettier:代码格式化
二、Solidity 基础语法:Hello World 合约
Solidity 是以太坊上最主流的智能合约编程语言,语法类似于 JavaScript 和 C++。
2.1 第一个智能合约
在 contracts/ 目录下创建 HelloWeb3.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @title HelloWeb3
* @dev 一个简单的智能合约示例
*/
contract HelloWeb3 {
// 状态变量(存储在区块链上)
string public greeting;
address public owner;
uint256 public createdAt;
// 事件(用于前端监听)
event GreetingChanged(address indexed changer, string newGreeting);
// 构造函数:部署时自动执行一次
constructor(string memory _greeting) {
greeting = _greeting;
owner = msg.sender;
createdAt = block.timestamp;
}
// 修改状态的函数(需要发送交易)
function setGreeting(string memory _greeting) public {
greeting = _greeting;
emit GreetingChanged(msg.sender, _greeting);
}
// 只读函数(不需要 gas)
function getGreeting() public view returns (string memory) {
return greeting;
}
// 获取合约信息
function getContractInfo() public view returns (
string memory,
address,
uint256
) {
return (greeting, owner, createdAt);
}
}
2.2 Solidity 核心概念速览
| 概念 | 说明 | 示例 |
|---|---|---|
state variable |
存储在区块链上的变量 | string public greeting |
msg.sender |
调用函数的钱包地址 | owner = msg.sender |
event |
可被前端监听的日志 | emit GreetingChanged(...) |
view |
只读函数,不消耗 gas | function getGreeting() public view |
modifier |
函数的访问控制 | onlyOwner |
mapping |
键值对数据结构 | mapping(address => uint256) |
2.3 编译合约
npx hardhat compile
成功编译后,artifacts/ 目录会生成 ABI 和字节码文件,这些是部署和交互的必需品。
三、部署到测试网(Sepolia)
在部署到主网之前,我们先在 Sepolia 测试网上进行测试。测试网使用的是没有实际价值的测试 ETH。
3.1 获取测试 ETH
前往以下水龙头领取 Sepolia 测试 ETH:
你需要至少 0.1 ETH 来进行测试。
3.2 配置 Hardhat 网络
安装 dotenv 来管理私钥:
npm install dotenv
创建 .env 文件(⚠️ 绝对不要将此文件提交到 Git):
# .env
PRIVATE_KEY=your_wallet_private_key_here
SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/your-api-key
ETHERSCAN_API_KEY=your_etherscan_api_key
更新 hardhat.config.js:
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();
module.exports = {
solidity: "0.8.20",
networks: {
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
chainId: 11155111,
},
},
};
3.3 编写部署脚本
创建 scripts/deploy-hello.js:
const hre = require("hardhat");
async function main() {
console.log("🚀 开始部署 HelloWeb3 合约...");
// 获取合约工厂
const HelloWeb3 = await hre.ethers.getContractFactory("HelloWeb3");
// 部署合约,传入构造函数参数
const helloWeb3 = await HelloWeb3.deploy("你好,Web3 世界!");
// 等待部署确认
await helloWeb3.waitForDeployment();
const address = await helloWeb3.getAddress();
console.log(`✅ HelloWeb3 已部署到: ${address}`);
// 读取合约状态
const greeting = await helloWeb3.getGreeting();
console.log(`📝 当前问候语: ${greeting}`);
const [greet, owner, timestamp] = await helloWeb3.getContractInfo();
console.log(`👤 合约所有者: ${owner}`);
console.log(`⏰ 创建时间: ${new Date(Number(timestamp) * 1000).toLocaleString()}`);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error("❌ 部署失败:", error);
process.exit(1);
});
3.4 执行部署
# 部署到本地 Hardhat 网络(用于开发测试)
npx hardhat run scripts/deploy-hello.js
# 部署到 Sepolia 测试网
npx hardhat run scripts/deploy-hello.js --network sepolia
部署成功后,你会看到类似输出:
🚀 开始部署 HelloWeb3 合约...
✅ HelloWeb3 已部署到: 0x1234...abcd
📝 当前问候语: 你好,Web3 世界!
👤 合约所有者: 0xYourAddress
⏰ 创建时间: 2024/1/15 14:30:00
📌 记下合约地址,后面连接前端时会用到!
3.5 在 Etherscan 上验证合约
npx hardhat verify --network sepolia DEPLOYED_CONTRACT_ADDRESS "你好,Web3 世界!"
验证后,用户可以在 Etherscan 上直接查看源码和交互合约。
四、前端连接钱包(ethers.js)
现在让我们构建一个 React 前端来连接用户的钱包并与合约交互。
4.1 创建 React 项目
cd ..
npx create-react-app web3-frontend
cd web3-frontend
# 安装 ethers.js
npm install ethers
💡 也可以使用 Vite + React 来获得更快的开发体验:
npm create vite@latest web3-frontend -- --template react
4.2 连接 MetaMask 钱包
创建 src/hooks/useWallet.js:
import { useState, useCallback } from 'react';
import { BrowserProvider } from 'ethers';
export function useWallet() {
const [account, setAccount] = useState(null);
const [provider, setProvider] = useState(null);
const [chainId, setChainId] = useState(null);
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState(null);
const connectWallet = useCallback(async () => {
if (!window.ethereum) {
setError('请安装 MetaMask 钱包!');
return;
}
setIsConnecting(true);
setError(null);
try {
// 请求连接钱包
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
const provider = new BrowserProvider(window.ethereum);
const network = await provider.getNetwork();
setAccount(accounts[0]);
setProvider(provider);
setChainId(Number(network.chainId));
} catch (err) {
setError(err.message || '连接钱包失败');
} finally {
setIsConnecting(false);
}
}, []);
const disconnectWallet = useCallback(() => {
setAccount(null);
setProvider(null);
setChainId(null);
}, []);
return {
account,
provider,
chainId,
isConnecting,
error,
connectWallet,
disconnectWallet,
};
}
4.3 创建钱包连接组件
创建 src/components/ConnectWallet.jsx:
import React from 'react';
import { useWallet } from '../hooks/useWallet';
function ConnectWallet() {
const { account, isConnecting, error, connectWallet, disconnectWallet } = useWallet();
const formatAddress = (addr) => {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
};
if (error) {
return (
<div className="wallet-error">
<p>⚠️ {error}</p>
<a href="https://metamask.io" target="_blank" rel="noopener noreferrer">
下载 MetaMask
</a>
</div>
);
}
if (account) {
return (
<div className="wallet-connected">
<span className="wallet-address">
🔗 {formatAddress(account)}
</span>
<button onClick={disconnectWallet} className="btn-disconnect">
断开连接
</button>
</div>
);
}
return (
<button
onClick={connectWallet}
disabled={isConnecting}
className="btn-connect"
>
{isConnecting ? '连接中...' : '🦊 连接 MetaMask'}
</button>
);
}
export default ConnectWallet;
五、读写合约数据
5.1 创建合约实例工具
创建 src/utils/contract.js:
import { Contract, parseUnits } from 'ethers';
// 合约 ABI(从编译产物中获取,这里展示关键部分)
const CONTRACT_ABI = [
"function greeting() public view returns (string)",
"function owner() public view returns (address)",
"function createdAt() public view returns (uint256)",
"function setGreeting(string memory _greeting) public",
"function getGreeting() public view returns (string memory)",
"function getContractInfo() public view returns (string memory, address, uint256)",
"event GreetingChanged(address indexed changer, string newGreeting)"
];
// ⚠️ 替换为你实际部署的合约地址
const CONTRACT_ADDRESS = "0xYourContractAddressHere";
export function getContract(signerOrProvider) {
return new Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signerOrProvider);
}
export { CONTRACT_ABI, CONTRACT_ADDRESS };
5.2 读取合约数据
import { useEffect, useState } from 'react';
import { getContract } from '../utils/contract';
function useContractData(provider) {
const [greeting, setGreeting] = useState('');
const [owner, setOwner] = useState('');
const [loading, setLoading] = useState(true);
const fetchData = async () => {
if (!provider) return;
try {
setLoading(true);
const contract = getContract(provider);
// 读取数据(不需要 gas)
const currentGreeting = await contract.getGreeting();
const contractOwner = await contract.owner();
setGreeting(currentGreeting);
setOwner(contractOwner);
} catch (err) {
console.error('读取合约数据失败:', err);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [provider]);
return { greeting, owner, loading, refresh: fetchData };
}
5.3 写入合约数据
import { useState } from 'react';
import { getContract } from '../utils/contract';
function useContractWrite(signer) {
const [txPending, setTxPending] = useState(false);
const [txHash, setTxHash] = useState('');
const updateGreeting = async (newGreeting) => {
if (!signer) {
alert('请先连接钱包');
return;
}
try {
setTxPending(true);
const contract = getContract(signer);
// 发送交易(需要 gas)
const tx = await contract.setGreeting(newGreeting);
setTxHash(tx.hash);
console.log('📤 交易已发送,等待确认...');
// 等待交易被确认
const receipt = await tx.wait();
console.log('✅ 交易已确认!区块号:', receipt.blockNumber);
return receipt;
} catch (err) {
console.error('写入合约失败:', err);
throw err;
} finally {
setTxPending(false);
}
};
return { updateGreeting, txPending, txHash };
}
5.4 监听合约事件
// 在 React 组件中监听实时事件
useEffect(() => {
if (!provider) return;
const contract = getContract(provider);
// 监听 GreetingChanged 事件
const onGreetingChanged = (changer, newGreeting, event) => {
console.log(`📢 事件: ${changer} 更新了问候语为 "${newGreeting}"`);
// 刷新数据
fetchData();
};
contract.on("GreetingChanged", onGreetingChanged);
// 清理监听器
return () => {
contract.off("GreetingChanged", onGreetingChanged);
};
}, [provider]);
六、实战:构建一个完整的投票 DApp
让我们把所学的知识整合起来,构建一个完整的去中心化投票应用。
6.1 投票智能合约
创建 contracts/Voting.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Voting {
struct Proposal {
string name;
string description;
uint256 voteCount;
bool exists;
}
address public owner;
string public title;
bool public votingActive;
mapping(uint256 => Proposal) public proposals;
mapping(address => bool) public hasVoted;
mapping(address => uint256) public voterChoice;
uint256 public proposalCount;
event ProposalCreated(uint256 indexed id, string name);
event VoteCast(address indexed voter, uint256 indexed proposalId);
event VotingStatusChanged(bool active);
modifier onlyOwner() {
require(msg.sender == owner, "只有管理员可以执行此操作");
_;
}
constructor(string memory _title) {
owner = msg.sender;
title = _title;
votingActive = true;
}
function createProposal(
string memory _name,
string memory _description
) public onlyOwner returns (uint256) {
require(votingActive, "投票已结束");
uint256 proposalId = proposalCount++;
proposals[proposalId] = Proposal({
name: _name,
description: _description,
voteCount: 0,
exists: true
});
emit ProposalCreated(proposalId, _name);
return proposalId;
}
function vote(uint256 _proposalId) public {
require(votingActive, "投票已结束");
require(!hasVoted[msg.sender], "你已经投过票了");
require(proposals[_proposalId].exists, "提案不存在");
hasVoted[msg.sender] = true;
voterChoice[msg.sender] = _proposalId;
proposals[_proposalId].voteCount++;
emit VoteCast(msg.sender, _proposalId);
}
function getProposal(uint256 _proposalId) public view returns (
string memory name,
string memory description,
uint256 voteCount
) {
require(proposals[_proposalId].exists, "提案不存在");
Proposal storage p = proposals[_proposalId];
return (p.name, p.description, p.voteCount);
}
function toggleVoting() public onlyOwner {
votingActive = !votingActive;
emit VotingStatusChanged(votingActive);
}
function checkVoted(address _voter) public view returns (bool) {
return hasVoted[_voter];
}
}
6.2 投票 DApp 前端组件
创建 src/components/VotingDApp.jsx:
import React, { useState, useEffect } from 'react';
import { BrowserProvider, Contract } from 'ethers';
const VOTING_ABI = [
"function title() view returns (string)",
"function votingActive() view returns (bool)",
"function proposalCount() view returns (uint256)",
"function proposals(uint256) view returns (string, string, uint256, bool)",
"function hasVoted(address) view returns (bool)",
"function createProposal(string, string) returns (uint256)",
"function vote(uint256)",
"function toggleVoting()",
"event VoteCast(address indexed voter, uint256 indexed proposalId)",
];
const CONTRACT_ADDRESS = "0xYourVotingContractAddress";
function VotingDApp() {
const [proposals, setProposals] = useState([]);
const [title, setTitle] = useState('');
const [isActive, setIsActive] = useState(true);
const [userVoted, setUserVoted] = useState(false);
const [signer, setSigner] = useState(null);
const [loading, setLoading] = useState(false);
// 连接钱包
const connect = async () => {
const provider = new BrowserProvider(window.ethereum);
const s = await provider.getSigner();
setSigner(s);
loadData(provider, s);
};
// 加载数据
const loadData = async (provider, signer) => {
const contract = new Contract(CONTRACT_ADDRESS, VOTING_ABI, provider);
const t = await contract.title();
const active = await contract.votingActive();
const count = await contract.proposalCount();
const voted = await contract.hasVoted(await signer.getAddress());
setTitle(t);
setIsActive(active);
setUserVoted(voted);
const loadedProposals = [];
for (let i = 0; i < Number(count); i++) {
const [name, desc, votes] = await contract.proposals(i);
loadedProposals.push({
id: i,
name,
description: desc,
voteCount: Number(votes),
});
}
setProposals(loadedProposals);
};
// 投票
const castVote = async (proposalId) => {
if (!signer) return alert('请先连接钱包');
setLoading(true);
try {
const contract = new Contract(CONTRACT_ADDRESS, VOTING_ABI, signer);
const tx = await contract.vote(proposalId);
await tx.wait();
alert('投票成功!🎉');
const provider = new BrowserProvider(window.ethereum);
loadData(provider, signer);
} catch (err) {
alert('投票失败: ' + err.message);
} finally {
setLoading(false);
}
};
return (
<div className="voting-dapp">
<h1>🗳️ {title || '去中心化投票系统'}</h1>
{!signer ? (
<button onClick={connect} className="btn-connect">
🦊 连接钱包开始投票
</button>
) : (
<>
<div className="status">
<span className={`badge ${isActive ? 'active' : 'closed'}`}>
{isActive ? '🟢 投票进行中' : '🔴 投票已结束'}
</span>
{userVoted && <span className="badge voted">✅ 你已投票</span>}
</div>
<div className="proposals">
{proposals.map((p) => (
<div key={p.id} className="proposal-card">
<h3>{p.name}</h3>
<p>{p.description}</p>
<div className="vote-section">
<span className="vote-count">票数: {p.voteCount}</span>
<button
onClick={() => castVote(p.id)}
disabled={!isActive || userVoted || loading}
className="btn-vote"
>
{loading ? '处理中...' : '投票'}
</button>
</div>
</div>
))}
</div>
</>
)}
</div>
);
}
export default VotingDApp;
七、安全最佳实践
智能合约一旦部署就无法修改,安全问题可能导致巨额资金损失。以下是必须遵守的安全准则:
7.1 常见安全漏洞
// ❌ 重入攻击漏洞
function withdraw() public {
uint256 balance = balances[msg.sender];
(bool success, ) = msg.sender.call{value: balance}(""); // 外部调用在更新状态之前
require(success, "转账失败");
balances[msg.sender] = 0; // 状态更新在外部调用之后,有重入风险
}
// ✅ 使用 checks-effects-interactions 模式
function withdraw() public {
uint256 balance = balances[msg.sender];
require(balance > 0, "余额不足");
balances[msg.sender] = 0; // 先更新状态
(bool success, ) = msg.sender.call{value: balance}(""); // 再进行外部调用
require(success, "转账失败");
}
7.2 安全检查清单
- 使用 OpenZeppelin 合约库:经过审计的标准化实现
- 整数溢出保护:Solidity 0.8+ 默认开启
-
访问控制:使用
onlyOwner等 modifier 限制敏感操作 - 输入验证:检查所有外部输入
-
重入保护:使用
ReentrancyGuard - 事件日志:所有关键操作都应发出事件
# 安装 OpenZeppelin 合约库
npm install @openzeppelin/contracts
// 使用 OpenZeppelin 的 Ownable 和 ReentrancyGuard
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract SecureContract is Ownable, ReentrancyGuard {
// 你的安全合约代码
function safeWithdraw() external nonReentrancy onlyOwner {
// 安全的提款逻辑
}
}
7.3 测试的重要性
// test/Voting.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Voting", function () {
let voting, owner, voter1, voter2;
beforeEach(async function () {
[owner, voter1, voter2] = await ethers.getSigners();
const Voting = await ethers.getContractFactory("Voting");
voting = await Voting.deploy("测试投票");
await voting.waitForDeployment();
});
it("应该成功创建提案", async function () {
await voting.createProposal("提案一", "这是第一个提案");
const [name, desc, votes] = await voting.getProposal(0);
expect(name).to.equal("提案一");
expect(votes).to.equal(0);
});
it("应该允许用户投票", async function () {
await voting.createProposal("提案一", "描述");
await voting.connect(voter1).vote(0);
const [, , votes] = await voting.getProposal(0);
expect(votes).to.equal(1);
});
it("不应该允许重复投票", async function () {
await voting.createProposal("提案一", "描述");
await voting.connect(voter1).vote(0);
await expect(
voting.connect(voter1).vote(0)
).to.be.revertedWith("你已经投过票了");
});
});
八、进阶学习路线图
完成本文的学习后,你已经掌握了 Web3 开发的基础。以下是一条推荐的进阶路线:
初级阶段 ✅ (你在这里)
- [x] Solidity 基础语法
- [x] Hardhat 开发框架
- [x] 合约部署与交互
- [x] ethers.js 前端集成
中级阶段 📚
- [ ] ERC-20 代币标准
- [ ] ERC-721 NFT 标准
- [ ] ERC-1155 多重代币标准
- [ ] DeFi 协议原理(Uniswap、Aave)
- [ ] The Graph 子图查询
- [ ] IPFS / Arweave 去中心化存储
高级阶段 🚀
- [ ] Solidity Assembly / Yul
- [ ] 代理合约与升级模式
- [ ] 跨链桥开发
- [ ] Layer 2 方案(Optimism、Arbitrum、zkSync)
- [ ] 形式化验证
- [ ] MEV 和闪电贷
推荐学习资源
| 资源 | 类型 | 链接 |
|---|---|---|
| CryptoZombies | 交互式教程 | cryptozombies.io |
| Solidity by Example | 文档 | solidity-by-example.org |
| Ethereum.org | 官方文档 | ethereum.org/developers |
| OpenZeppelin Docs | 合约库 | docs.openzeppelin.com |
| Ethernaut | 安全挑战 | ethernaut.openzeppelin.com |
| Speed Run Ethereum | 实战挑战 | speedrunethereum.com |
工具推荐
- 开发框架:Hardhat、Foundry
- 前端库:ethers.js v6、wagmi、RainbowKit
- 测试工具:Hardhat Network、Tenderly
- 安全工具:Slither、Mythril、Aderyn
- 合约库:OpenZeppelin、Solmate
总结
恭喜你完成了这篇 Web3 开发入门指南!让我们回顾一下你学到的内容:
- ✅ 搭建了完整的 Web3 开发环境
- ✅ 编写了你的第一个 Solidity 智能合约
- ✅ 将合约部署到了 Sepolia 测试网
- ✅ 使用 ethers.js 连接了前端和区块链
- ✅ 构建了一个完整的投票 DApp
- ✅ 了解了智能合约的安全最佳实践
Web3 开发是一个快速发展的领域,新的工具和技术层出不穷。保持学习,多动手实践,加入开发者社区,你会发现自己进步飞速。
🚀 觉得有帮助吗?关注我获取更多 Web3 开发教程!点赞 + 收藏,让更多人看到这份指南。
有任何问题欢迎在评论区留言,我会逐一回复。让我们一起探索 Web3 的无限可能!
Top comments (0)