在前几个入门示例中,我们了解了以太坊账户、智能合约开发、Truffle框架以及前端交互的基础知识,本示例将带大家构建一个简单的投票DApp,涵盖智能合约设计、合约部署、前端界面开发及完整交互流程,这个示例将帮助开发者巩固Solidity编程、Truffle部署流程和Web3.js前端交互,同时理解DApp中“合约-前端-用户”的协同工作模式。
项目准备与环境搭建
在开始之前,请确保已完成以下环境配置:
- Node.js(建议版本≥14.0):用于运行前端框架和Truffle工具。
- Truffle:全局安装,命令为
npm install -g truffle。 - Ganache:本地以太坊区块链,用于模拟区块链网络(提供10个测试账户,每个账户10000 ETH)。
- MetaMask:浏览器钱包插件,用于用户签名交易和管理私钥。
- 前端框架:本示例使用
HTML + CSS + JavaScript(原生),也可结合React/Vue简化开发。
创建项目目录并初始化:
mkdir ethereum-voting-dapp cd ethereum-voting-dapp truffle init npm install web3
智能合约设计:Voting.sol
投票DApp的核心是记录投票选项、统计票数、限制投票权限的智能合约,我们将其命名为Voting.sol,放在contracts/目录下。
合约功能需求
- 支持多个候选人(投票选项)。
- 每个地址只能投票一次(防止重复投票)。
- 查看当前各候选人票数。
- 合约所有者可结束投票(锁定投票功能)。
合约代码
// contracts/Voting.sol
pragma solidity ^0.8.0;
contract Voting {
// 候选人结构体:姓名和票数
struct Candidate {
string name;
uint voteCount;
}
// 候选人列表(固定数组,可根据需求改为动态数组)
Candidate[] public candidates;
// 记录地址是否已投票
mapping(address => bool) public hasVoted;
// 合约所有者(可结束投票)
address public owner;
// 构造函数:初始化候选人
constructor(string[] memory candidateNames) {
owner = msg.sender;
for (uint i = 0; i < candidateNames.length; i++) {
candidates.push(Candidate({
name: candidateNames[i],
voteCount: 0
}));
}
}
// 投票函数
function vote(uint candidateIndex) public {
require(!hasVoted[msg.sender], "You have already voted!");
require(candidateIndex < candidates.length, "Invalid candidate index!");
hasVoted[msg.sender] = true;
candidates[candidateIndex].voteCount++;
}
// 获取候选人票数
function getVoteCount(uint candidateIndex) public view returns (uint) {
require(candidateIndex < candidates.length, "Invalid candidate index!");
return candidates[candidateIndex].voteCount;
}
// 结束投票(仅所有者可调用)
function endVoting() public {
require(msg.sender == owner, "Only owner can end voting!");
// 这里可以添加逻辑锁定投票(如设置状态变量),示例中简单清空候选人列表
for (uint i = 0; i < candidates.length; i++) {
candidates.pop();
}
}
}
配置Truffle:迁移合约
Truffle通过migrations/目录下的脚本管理合约部署,创建2_deploy_contracts.js文件:
// migrations/2_deploy_contracts.js
const Voting = artifacts.require("Voting");
module.exports = function (deployer) {
// 部署合约时传入候选人列表
const candidateNames = ["Alice", "Bob", "Charlie"];
deployer.deploy(Voting, candidateNames);
};
部署合约到本地网络
- 启动Ganache(默认地址为
http://127.0.0.1:7545)。 - 在项目目录下运行Truffle部署命令:
truffle migrate --network development
成功后,Ganache会显示交易详情,控制台会输出合约部署地址(如
Voting deployed at: 0x123...)。
前端开发:连接合约与用户界面
创建前端文件结构
在项目根目录下创建src/文件夹,并添加以下文件:
index.html:投票界面。style.css:样式文件。app.js:前端逻辑(连接Web3、调用合约)。
编写HTML界面(index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">以太坊投票DApp</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>投票系统</h1>
<div class="voting-section">
<h2>选择候选人:</h2>
<div id="candidates"></div>
<button id="vote
Button" disabled>投票</button>
</div>
<div class="results-section">
<h2>投票结果:</h2>
<div id="results"></div>
</div>
<div class="status" id="status"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/web3@1.8.0/dist/web3.min.js"></script>
<script src="app.js"></script>
</body>
</html>
编写样式文件(style.css)
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #333;
}
.voting-section, .results-section {
margin: 20px 0;
}
.candidate {
margin: 10px 0;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.candidate.selected {
background-color: #e6f7ff;
border-color: #1890ff;
}
button {
width: 100%;
padding: 10px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
.status {
margin-top: 20px;
padding: 10px;
border-radius: 4px;
text-align: center;
}
.status.success {
background-color: #f6ffed;
color: #52c41a;
}
.status.error {
background-color: #fff2f0;
color: #ff4d4f;
}
编写前端逻辑(app.js)
核心功能包括:连接Web3、加载候选人列表、监听用户投票、实时更新结果。
// src/app.js
document.addEventListener('DOMContentLoaded', () => {
// 1. 初始化Web3
let web3;
let votingContract;
let accounts;
if (typeof window.ethereum !== 'undefined') {
web3 = new Web3(window.ethereum);
window.ethereum.request({ method: 'eth_requestAccounts' })
.then(acc => {
accounts = acc;
initApp();
})
.catch(err => console.error("连接钱包失败:", err));
} else {
document.getElementById('status').innerHTML =
"<div class='status error'>请安装MetaMask钱包!</div>";
}
// 2. 初始化应用
async function initApp() {
const networkId = await web3.eth.net.getId();
const contractAddress = "0x123..."; // 替换为实际部署的合约地址
const contractAbi = [
// 这里粘贴Voting.sol的ABI(可通过truffle compile生成)
{
"inputs": [],
"name": "candidates",
"outputs": [
{"internalType": "string", "name": "name", "type": "string"},
{"internalType": "uint256",