本文以一个完整的投票 DApp 项目为例,带你从合约开发、部署到前端集成 Web3.js,体验 Web3 全栈开发流程。
一、项目结构与部署流程时序图
sequenceDiagram
participant D as 开发者
participant H as Hardhat
participant F as 前端
participant U as 用户
D->>H: 编写 Voting.sol 合约
D->>H: npx hardhat compile
D->>H: npx hardhat node
D->>H: npx hardhat ignition deploy
H->>F: 生成 ABI/地址 (contractInfo.json)
U->>F: 打开 vote.html
F->>F: 读取 contractInfo.json
F->>H: 通过 Web3.js 连接本地节点
U->>F: 投票/查询操作
F->>H: 调用合约方法
H-->>F: 返回结果
F-->>U: 展示投票结果
- contracts/:Solidity 智能合约源码
- ignition/modules/:合约部署脚本
- artifacts/:合约 ABI、字节码等编译产物
- frontend/:前端页面与合约信息
二、合约开发与部署
1. 编写 Voting 合约
solidity
// contracts/Voting.sol
pragma solidity ^0.7.4;
contract Voting {
struct Candidate { uint id; string name; uint voteCount; }
mapping(uint => Candidate) public candidates;
mapping(address => bool) public voters;
uint public candidatesCount;
event CandidateAdded(uint id, string name);
event Voted(uint indexed candidateId);
constructor() {
addCandidate("Alice");
addCandidate("Bob");
}
function addCandidate(string memory _name) private {
candidatesCount++;
candidates[candidatesCount] = Candidate(candidatesCount, _name, 0);
emit CandidateAdded(candidatesCount, _name);
}
function vote(uint _candidateId) public {
require(!voters[msg.sender], "You have already voted.");
require(_candidateId > 0 && _candidateId <= candidatesCount, "Invalid candidate ID.");
voters[msg.sender] = true;
candidates[_candidateId].voteCount++;
emit Voted(_candidateId);
}
function getResult() public view returns (string memory winnerName, uint winnerVoteCount) {
uint winningVoteCount = 0;
for (uint i = 1; i <= candidatesCount; i++) {
if (candidates[i].voteCount > winningVoteCount) {
winningVoteCount = candidates[i].voteCount;
winnerName = candidates[i].name;
}
}
winnerVoteCount = winningVoteCount;
}
}
2. Hardhat 编译与本地链部署
bash
npm install
npx hardhat compile
npx hardhat node
部署合约:
bash
npx hardhat ignition deploy ./ignition/modules/Voting.js --network localhost
部署成功后,终端会输出合约地址:

三、前端集成 Web3.js
1. 前端页面结构
html
<!-- frontend/vote.html -->
<script src="https://cdn.jsdelivr.net/npm/web3/dist/web3.min.js"></script>
<script>
async function loadContractInfo() {
const res = await fetch('contractInfo.json');
return await res.json();
}
let contract, web3;
loadContractInfo().then(({ abi, address }) => {
web3 = new Web3(Web3.givenProvider || 'http://localhost:8545');
contract = new web3.eth.Contract(abi, address);
loadCandidates();
});
async function loadCandidates() {
const candidatesCount = await contract.methods.candidatesCount().call();
// ...渲染候选人
}
async function vote() {
const candidateId = document.getElementById('candidateId').value;
const accounts = await web3.eth.getAccounts();
await contract.methods.vote(candidateId).send({ from: accounts[0] });
alert('投票成功!');
loadCandidates();
}
async function getResults() {
const result = await contract.methods.getResult().call();
// ...显示结果
}
</script>
2. 合约 ABI 和地址自动同步 (这里需要手动修改json文件)
json
// frontend/contractInfo.json
{
"abi": [ ... ],
"address": "0x1234567890abcdef..."
}
3. 启动本地前端服务
bash
cd frontend
python3 -m http.server 8080
浏览器访问 http://localhost:8080/vote.html

四、Web3.js 交互流程图
sequenceDiagram
participant U as 用户
participant F as 前端(vote.html)
participant W as Web3.js
participant C as 合约(Voting)
U->>F: 输入候选人ID点击投票
F->>W: 调用 contract.methods.vote(id).send()
W->>C: 发送交易到 Voting 合约
C-->>W: 交易回执/事件
W-->>F: 返回投票结果
F-->>U: 页面提示"投票成功"
五、常见问题与调试
- web3.js 加载慢/超时:可切换为本地 web3.min.js 文件
- 合约地址不对/前端报错:请确保 contractInfo.json 里的地址和最新部署一致
- 投票失败:请确保钱包连接本地节点,或本地节点已启动
六、参考资料
建议:
在实际项目中,建议将合约 ABI/地址自动同步到前端,并用脚本化方式管理部署和前端集成,提升开发效率。