以太坊DApp开发的入门示例6,构建去中心化投票应用

博主:neragonerago 2026-10-12 19:16:54 5

在本系列的前几篇文章中,我们已经学习了开发环境搭建、智能合约编写、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);
    }
}

代码要点解析:

  1. mapping(address => bool) 实现了“一人一票”的限制
  2. onlyAdmin 修饰器确保只有管理员能添加候选人
  3. 事件 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();
    });
}

测试运行

  1. 启动本地节点并部署合约
  2. 配置MetaMask连接到 http://localhost:8545
  3. 导入Hardhat测试账户的私钥到MetaMask
  4. 用浏览器打开 index.html,连接钱包
  5. 点击“投票”按钮,观察票数变化

注意事项:

The End

发布于:2026-10-12,除非注明,否则均为区块链社区- 欧亿APP下载原创文章,转载请注明出处。