区块链应用程序编程实例,从零构建一个简单的去中心化投票应用

时间: 2026-03-13 18:03 阅读数: 8人阅读

区块链技术以其去中心化、透明不可篡改和智能合约自动执行等特性,正在深刻改变众多行业,理解区块链技术的最佳途径之一便是通过实际的编程实例,本文将以一个简单的“去中心化投票应用”(Decentralized Voting App)为例,带你走过从环境搭建、智能合约编写、前端交互到部署测试的全过程,为你揭开区块链应用开发的神秘面纱。

开发环境准备

在开始编写代码之前,我们需要搭建好基本的开发环境:

  1. Node.js 和 npm:JavaScript 运行时环境和包管理器,从 Node.js 官网 下载并安装 LTS 版本。
  2. Truffle Suite:最受欢迎的以太坊开发框架,包含智能合约编译、测试、部署等工具,通过 npm 安装:
    npm install -g truffle
  3. Ganache:一款个人区块链,用于本地快速部署和测试以太坊网络,它会提供一系列默认的测试账户和私钥,从 Ganache 官网 下载桌面版或命令行版。
  4. MetaMask:浏览器钱包插件,用于与区块链交互(如发送交易、连接 DApp),从 MetaMask 官网 安装浏览器插件。
  5. 代码编辑器:如 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");
        }
    });
});

编译与测试合约

  1. 编译合约

    truffle compile

    这会在 build/contracts/ 目录下生成 ABI(Application Binary Interface)和字节码文件。

  2. 运行测试: 确保 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 = [/* 从