区块链应用程序编程实例,从零构建一个简单的去中心化投票应用
时间:
2026-03-13 18:03 阅读数:
8人阅读
区块链技术以其去中心化、透明不可篡改和智能合约自动执行等特性,正在深刻改变众多行业,理解区块链技术的最佳途径之一便是通过实际的编程实例,本文将以一个简单的“去中心化投票应用”(Decentralized Voting App)为例,带你走过从环境搭建、智能合约编写、前端交互到部署测试的全过程,为你揭开区块链应用开发的神秘面纱。
开发环境准备
在开始编写代码之前,我们需要搭建好基本的开发环境:
- Node.js 和 npm:JavaScript 运行时环境和包管理器,从 Node.js 官网 下载并安装 LTS 版本。
- Truffle Suite:最受欢迎的以太坊开发框架,包含智能合约编译、测试、部署等工具,通过 npm 安装:
npm install -g truffle
- Ganache:一款个人区块链,用于本地快速部署和测试以太坊网络,它会提供一系列默认的测试账户和私钥,从 Ganache 官网 下载桌面版或命令行版。
- MetaMask:浏览器钱包插件,用于与区块链交互(如发送交易、连接 DApp),从 MetaMask 官网 安装浏览器插件。
- 代码编辑器:如 VS Code,并安装 Solidity 插件。
项目初始化与智能合约编写
创建项目目录并初始化
mkdir decentralized-voting-app cd decentralized-voting-app truffle init
这会创建一个标准的 Truffle 项目结构,包括 contracts/(智能合约)、migrations/(部署脚本)、test/(测试文件)等目录。
编写智能合约
在 contracts/ 目录下创建一个新的 Solidity 文件 Voting.sol,智能合约是区块链应用的核心,定义了业务逻辑和数据结构。
// contracts/Voting.sol
pragma solidity ^0.8.0;
contract Voting {
// 候选人结构体
struct Candidate {
uint id;
string name;
uint voteCount;
}
// 主席官地址,用于添加候选人
address public chairperson;
// 候选人列表,键为候选人ID
mapping(uint => Candidate) public candidates;
// 投票人地址到是否已投票的映射
mapping(address => bool) public voters;
// 候选人ID计数器
uint public candidatesCount;
// 事件,用于前端监听投票事件
event VotedEvent(uint indexed candidateId, address indexed voter);
// 构造函数,部署时设置主席官
constructor() {
chairperson = msg.sender;
}
// 主席官添加候选人
function addCandidate(string memory _name) public {
require(msg.sender == chairperson, "Only chairperson can add candidates");
candidatesCount++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
}
// 投票函数
function vote(uint _candidateId) public {
// 确保投票人尚未投票
require(!voters[msg.sender], "You have already voted");
// 确保候选人ID有效
require(_candidateId > 0 && _candidateId <= ca
ndidatesCount, "Invalid candidate ID");
voters[msg.sender] = true;
candidates[_candidateId].voteCount++;
// 触发投票事件
emit VotedEvent(_candidateId, msg.sender);
}
// 获取候选人信息
function getCandidate(uint _candidateId) public view returns (uint id, string memory name, uint voteCount) {
Candidate storage candidate = candidates[_candidateId];
return (candidate.id, candidate.name, candidate.voteCount);
}
}
合约解析:
Candidate结构体存储候选人ID、姓名和得票数。chairperson是添加候选人的唯一地址,在构造函数中设置为合约部署者。candidates是一个映射,存储所有候选人信息。voters记录每个地址是否已投票,防止重复投票。addCandidate:仅主席官可调用,用于添加新候选人。vote:用户调用,为指定候选人投票,并更新投票状态和候选人得票数。VotedEvent:事件,方便前端监听投票行为。
编写迁移(部署)脚本
在 migrations/ 目录下创建一个新的迁移脚本,2_deploy_voting.js,用于部署我们的 Voting 合约。
// migrations/2_deploy_voting.js
const Voting = artifacts.require("Voting");
module.exports = function (deployer) {
// 部署 Voting 合约
deployer.deploy(Voting);
};
编写测试用例
在 test/ 目录下创建 voting.test.js,使用 Mocha 和 Chai 编写测试用例,确保合约逻辑正确。
// test/voting.test.js
const Voting = artifacts.require("Voting");
contract("Voting", (accounts) => {
let votingInstance;
const chairperson = accounts[0];
const voter1 = accounts[1];
const voter2 = accounts[2];
const candidateName = "Alice";
beforeEach(async () => {
votingInstance = await Voting.new();
await votingInstance.addCandidate(candidateName, { from: chairperson });
});
it("should initialize with correct chairperson", async () => {
const chair = await votingInstance.chairperson();
assert.equal(chair, chairperson, "Chairperson is not correct");
});
it("should allow chairperson to add a candidate", async () => {
const candidateCount = await votingInstance.candidatesCount();
assert.equal(candidateCount, 1, "Candidate count should be 1 after adding one");
const candidate = await votingInstance.getCandidate(1);
assert.equal(candidate[1], candidateName, "Candidate name is not correct");
});
it("should allow a voter to vote", async () => {
await votingInstance.vote(1, { from: voter1 });
const voter = await votingInstance.voters(voter1);
assert.equal(voter, true, "Voter should be marked as voted");
const candidate = await votingInstance.getCandidate(1);
assert.equal(candidate[2], 1, "Candidate vote count should be 1");
});
it("should not allow a voter to vote twice", async () => {
await votingInstance.vote(1, { from: voter1 });
try {
await votingInstance.vote(1, { from: voter1 });
assert.fail("Expected revert when voting twice");
} catch (error) {
assert.include(error.message, "You have already voted");
}
});
});
编译与测试合约
-
编译合约:
truffle compile
这会在
build/contracts/目录下生成 ABI(Application Binary Interface)和字节码文件。 -
运行测试: 确保 Ganache 正在运行(默认端口 7545)。
truffle test
测试应全部通过,验证合约逻辑的正确性。
开发前端交互界面
在项目根目录下创建 src/ 文件夹,用于存放前端代码,我们可以使用 HTML, CSS 和 JavaScript(结合 Web3.js 或 Ethers.js)来构建用户界面。
这里简单展示使用 Ethers.js 连接 MetaMask 并与合约交互的核心代码:
<!-- src/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">Decentralized Voting App</title>
<script src="https://cdn.ethers.io/lib/ethers-5.2.umd.min.js" type="application/javascript"></script>
</head>
<body>
<h1>Decentralized Voting</h1>
<div>
<h2>Add Candidate (Chairperson Only)</h2>
<input type="text" id="candidateName" placeholder="Candidate Name">
<button onclick="addCandidate()">Add Candidate</button>
</div>
<div>
<h2>Vote for Candidate</h2>
<select id="candidateSelect"></select>
<button onclick="vote()">Vote</button>
</div>
<div id="result"></div>
<script src="app.js"></script>
</body>
</html>
// src/app.js let contract; let signer; let provider; // 合约地址和ABI(部署后替换为实际值) const contractAddress = "0x..."; // 部署后获取的合约地址 const contractABI = [/* 从