以太坊DApp开发的入门示例6,构建去中心化投票应用
在本系列的前几篇文章中,我们已经学习了开发环境搭建、智能合约编写、Web3交互等基础知识,今天我们将综合运用这些知识,构建一个完整的去中心化投票DApp,这个示例将涵盖合约设计、事件监听、前端交互等核心技能,是迈向真实DApp开发的重要一步。
我们要构建的投票DApp具备以下功能:
- 管理员可以创建投票议题并添加候选人
- 用户可以为候选人投票(每人限投一票)
- 实时显示每位候选人的得票数
- 通过事件通知投票结果
技术栈:Solidity + Hardhat + ethers.js + HTML/JavaScript
项目初始化
mkdir voting-dapp cd voting-dapp npm init -y npm install --save-dev hardhat npx hardhat init
选择"Create a JavaScript project",然后安装依赖:
npm install ethers
编写投票智能合约
在 contracts 目录下创建 Voting.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Voting {
// 候选人结构体
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 合约部署者(管理员)
address public admin;
// 候选人列表
Candidate[] public candidates;
// 记录已投票的地址
mapping(address => bool) public voters;
// 投票事件
event Voted(address indexed voter, uint indexed candidateId);
// 添加候选人事件
event CandidateAdded(uint indexed candidateId, string name);
constructor() {
admin = msg.sender;
}
// 修饰器:仅管理员可操作
modifier onlyAdmin() {
require(msg.sender == admin, "Only admin can call this");
_;
}
// 添加候选人
function addCandidate(string memory _name) public onlyAdmin {
candidates.push(Candidate(candidates.length, _name, 0));
emit CandidateAdded(candidates.length - 1, _name);
}
// 投票函数
function vote(uint _candidateId) public {
// 检查是否已投过票
require(!voters[msg.sender], "You have already voted");
// 检查候选人是否存在
require(_candidateId < candidates.length, "Invalid candidate");
voters[msg.sender] = true;
candidates[_candidateId].voteCount++;
emit Voted(msg.sender, _candidateId);
}
// 获取候选人总数
function getCandidatesCount() public view returns (uint) {
return candidates.length;
}
// 获取候选人信息
function getCandidate(uint _candidateId) public view
returns (uint, string memory, uint)
{
Candidate memory c = candidates[_candidateId];
return (c.id, c.name, c.voteCount);
}
}
代码要点解析:
mapping(address => bool)实现了“一人一票”的限制onlyAdmin修饰器确保只有管理员能添加候选人- 事件
Voted和CandidateAdded让前端可以实时监听链上变化
部署脚本
在 scripts 目录下创建 deploy.js:
const hre = require("hardhat");
async function main() {
const Voting = await hre.ethers.getContractFactory("Voting");
const voting = await Voting.deploy();
await voting.deployed();
console.log("投票合约已部署到:", voting.address);
// 初始化两个候选人
await voting.addCandidate("Alice");
await voting.addCandidate("Bob");
console.log("候选人添加成功!");
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
启动本地测试网络并部署:
npx hardhat node # 终端1:启动本地节点 npx hardhat run scripts/deploy.js --network localhost # 终端2:部署合约
前端页面开发
创建 index.html:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">去中心化投票DApp</title>
<style>
body { font-family: sans-serif; max-width: 600px; margin: 50px auto; }
.candidate {
border: 1px solid #ddd; padding: 15px;
margin: 10px 0; border-radius: 8px;
display: flex; justify-content: space-between;
}
button { cursor: pointer; padding: 8px 16px; }
</style>
</head>
<body>
<h1>🗳️ 去中心化投票系统</h1>
<button id="connectBtn">连接MetaMask</button>
<div id="candidateList"></div>
<script src="https://cdn.ethers.io/lib/ethers-5.7.umd.min.js"></script>
<script src="app.js"></script>
</body>
</html>
创建 app.js:
const contractAddress = "你的合约地址";
const contractABI = [/* 从 artifacts/Voting.json 复制 abi */];
let provider, signer, contract;
// 连接钱包
document.getElementById("connectBtn").onclick = async () => {
await window.ethereum.request({ method: "eth_requestAccounts" });
provider = new ethers.providers.Web3Provider(window.ethereum);
signer = provider.getSigner();
contract = new ethers.Contract(contractAddress, contractABI, signer);
loadCandidates();
listenToEvents();
};
// 加载候选人列表
async function loadCandidates() {
const count = await contract.getCandidatesCount();
const listDiv = document.getElementById("candidateList");
listDiv.innerHTML = "";
for (let i = 0; i < count; i++) {
const [id, name, voteCount] = await contract.getCandidate(i);
const div = document.createElement("div");
div.className = "candidate";
div.innerHTML = `
<span>${name} - 得票数:${voteCount}</span>
<button onclick="vote(${id})">投票</button>
`;
listDiv.appendChild(div);
}
}
// 投票
async function vote(candidateId) {
try {
const tx = await contract.vote(candidateId);
await tx.wait();
alert("投票成功!");
loadCandidates();
} catch (err) {
alert("投票失败:" + err.error?.message || err.message);
}
}
// 监听链上事件,实时更新
function listenToEvents() {
contract.on("Voted", (voter, candidateId) => {
console.log(`地址 ${voter} 投给了候选人 ${candidateId}`);
loadCandidates();
});
}
测试运行
- 启动本地节点并部署合约
- 配置MetaMask连接到
http://localhost:8545 - 导入Hardhat测试账户的私钥到MetaMask
- 用浏览器打开
index.html,连接钱包 - 点击“投票”按钮,观察票数变化
注意事项:
The End
发布于:2026-10-12,除非注明,否则均为原创文章,转载请注明出处。
