以太坊 DApp 开发:Web3.js 与 IPFS 存储
以太坊 DApp 全栈开发:Web3.js 与 IPFS 存储
本教程将带你从零开始构建一个完整的去中心化应用(DApp),前端使用 Web3.js 与以太坊交互,存储层使用 IPFS 分布式文件系统。你将掌握智能合约编写、本地测试链部署、前端集成以及 IPFS 文件上传与合约关联的全流程。
1. DApp 全栈概览
一个完整的以太坊 DApp 通常包含以下部分:
- 智能合约:运行在以太坊虚拟机(EVM)上的业务逻辑。
- 区块链交互库:前端通过
Web3.js或Ethers.js与合约通信。 - 去中心化存储:大文件或元数据存储于 IPFS,将内容哈希锚定到合约中。
- 前端界面:React / Vue / 纯 HTML 等,通过钱包(如 MetaMask)签名交易。
本教程将实现一个 去中心化记事本:用户可以上传一段文本存储到 IPFS,并将返回的 CID(内容标识符)记录到以太坊智能合约中,随时从合约读取自己的笔记历史。
2. 环境准备与工具安装
2.1 区块链开发环境
推荐使用 Hardhat 作为本地开发框架,它自带以太坊测试网络节点,支持 Solidity 编译与调试。
mkdir dapp-notepad && cd dapp-notepad
npm init -y
npm install --save-dev hardhat
npx hardhat init
选择 Create a JavaScript project,一路回车确认。
2.2 智能合约依赖
安装 OpenZeppelin 合约库以安全存取结构化数据:
npm install @openzeppelin/contracts
2.3 前端依赖
在项目根目录创建 client 目录用于前端:
mkdir client && cd client
npm init -y
npm install web3 ipfs-http-client
ipfs-http-client用于与 IPFS 节点 API 交互。你需要本地运行 IPFS 节点,或使用 Infura、Pinata 等第三方服务。
2.4 IPFS 节点配置
- 本地节点:下载 IPFS Desktop 或命令行运行
ipfs daemon。默认 API 地址http://127.0.0.1:5001。 - 远程节点:注册 Infura IPFS 获取项目 ID 和密钥,API 地址为
https://ipfs.infura.io:5001。
本教程使用本地节点,确保 ipfs daemon 已启动。
3. 智能合约开发
在 contracts 目录下创建 Notepad.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Notepad {
struct Note {
string cid; // IPFS 内容哈希
uint256 timestamp;
}
mapping(address => Note[]) private userNotes;
event NoteSaved(address indexed user, string cid, uint256 timestamp);
function addNote(string memory _cid) public {
userNotes[msg.sender].push(Note(_cid, block.timestamp));
emit NoteSaved(msg.sender, _cid, block.timestamp);
}
function getMyNotes() public view returns (Note[] memory) {
return userNotes[msg.sender];
}
}
合约功能:addNote 将 IPFS CID 与时间戳存入用户地址映射的数组,并触发事件;getMyNotes 返回该用户所有笔记。
3.1 编译合约
npx hardhat compile
3.2 编写部署脚本
在 ignition/modules 或自定义脚本中部署。简单起见,使用 scripts/deploy.js:
const hre = require("hardhat");
async function main() {
const Notepad = await hre.ethers.getContractFactory("Notepad");
const notepad = await Notepad.deploy();
await notepad.waitForDeployment();
console.log("Notepad deployed to:", await notepad.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
部署到本地网络:
# 启动本地节点(保持运行)
npx hardhat node
# 另开终端,部署合约
npx hardhat run scripts/deploy.js --network localhost
记录控制台输出的合约地址。
4. 前端集成:Web3.js 与钱包连接
在 client 目录下创建 app.js 和 index.html。
4.1 HTML 骨架
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>去中心化记事本</title>
</head>
<body>
<h2>我的笔记</h2>
<button id="connectBtn">连接钱包</button>
<p id="account"></p>
<textarea id="noteInput" rows="4" cols="50" placeholder="写下你想存储的内容..."></textarea>
<br/>
<button id="saveBtn">保存到 IPFS & 合约</button>
<h3>历史记录</h3>
<ul id="notesList"></ul>
<script src="app.js" type="module"></script>
</body>
</html>
4.2 Web3 初始化与合约 ABI
在 app.js 中引入 Web3,并获取部署的合约 ABI(将 Notepad.json 中的 abi 数组复制到代码中或通过 HTTP 加载)。
import Web3 from './node_modules/web3/dist/web3.min.js';
let web3;
let notepadContract;
const contractAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'; // 替换为实际的合约地址
const contractABI = [...]; // 贴入 Notepad.json 中的 abi 数组
document.getElementById('connectBtn').addEventListener('click', async () => {
if (window.ethereum) {
web3 = new Web3(window.ethereum);
await window.ethereum.request({ method: 'eth_requestAccounts' });
const accounts = await web3.eth.getAccounts();
document.getElementById('account').innerText = `已连接: ${accounts[0]}`;
notepadContract = new web3.eth.Contract(contractABI, contractAddress);
} else {
alert('请安装 MetaMask');
}
});
Web3 实例通过 MetaMask 注入的 window.ethereum 创建,获取用户授权后获得账户,并实例化合约对象。
5. IPFS 存储与合约交互
5.1 IPFS 客户端配置
import { create } from './node_modules/ipfs-http-client/dist/index.min.js';
const ipfs = create({ url: 'http://127.0.0.1:5001' }); // 使用本地节点
5.2 上传文本到 IPFS 并调用合约
点击“保存到 IPFS & 合约”时:
document.getElementById('saveBtn').addEventListener('click', async () => {
const text = document.getElementById('noteInput').value.trim();
if (!text || !notepadContract) return alert('请先连接钱包并输入内容');
// 将文本转换为 Buffer 并上传
const result = await ipfs.add(text);
const cid = result.path; // 例如 "QmX..." 格式的 CIDv0 或 CIDv1
console.log('IPFS CID:', cid);
// 调用合约 addNote
const accounts = await web3.eth.getAccounts();
await notepadContract.methods.addNote(cid).send({ from: accounts[0] });
alert('笔记已保存!');
document.getElementById('noteInput').value = '';
loadNotes(); // 刷新历史列表
});
5.3 从合约读取 CID 并展示
因为 IPFS 存储的是原始文本,我们可以通过公共网关访问,例如 https://ipfs.io/ipfs/<cid>。在 loadNotes 函数中:
async function loadNotes() {
if (!notepadContract) return;
const accounts = await web3.eth.getAccounts();
const notes = await notepadContract.methods.getMyNotes().call({ from: accounts[0] });
const listElement = document.getElementById('notesList');
listElement.innerHTML = '';
for (let i = notes.length - 1; i >= 0; i--) {
const note = notes[i];
const date = new Date(Number(note.timestamp) * 1000).toLocaleString();
// 通过 IPFS 网关获取内容
const response = await fetch(`https://ipfs.io/ipfs/${note.cid}`);
const content = await response.text();
const li = document.createElement('li');
li.innerHTML = `<strong>${date}</strong>: ${content} <br/><small>CID: ${note.cid}</small>`;
listElement.appendChild(li);
}
}
安全与性能提示:生产环境中应使用专用的 IPFS 网关或通过
ipfs-http-client直接读取,避免依赖公共网关的可用性。
6. 整合测试与运行
6.1 启动本地服务
由于浏览器无法直接加载本地 node_modules 的 ES 模块,使用简单的 HTTP 服务器,并考虑使用打包工具(如 webpack 或 Vite)优化。为了快速验证,可以用 live-server 或 http-server:
cd client
npx http-server .
打开浏览器访问(默认 http://127.0.0.1:8080),确保 MetaMask 已连接至本地 Hardhat 网络(自定义 RPC http://127.0.0.1:8545,链 ID 31337)。
6.2 完整流程验证
- 点击“连接钱包”,选择 Hardhat 本地测试账户。
- 在输入框输入一段话,点击“保存到 IPFS & 合约”。
- MetaMask 弹出交易确认,确认后等待交易上链。
- 页面自动刷新笔记列表,调用
getMyNotes并拉取 IPFS 内容展示。
如果此时 IPFS 本地节点已运行,浏览器会通过网关获取内容并渲染。
7. 进阶优化与部署指南
7.1 使用环境变量与前端构建工具
- 使用 Vite + React 可以更高效地管理依赖,通过
import.meta.env存储合约地址。 - 将
web3和ipfs-http-client集成到现代框架中,并处理异步状态。
7.2 迁移至测试网/主网
- 修改 Hardhat 配置,添加 Sepolia 或 Goerli 网络,使用 Infura 或 Alchemy 的节点。
- 部署合约后,在
.env中更新合约地址。 - 前端 IPFS 节点可切换至 Infura IPFS 节点(需要认证)。
// 示例:Infura IPFS 配置
const ipfs = create({
host: 'ipfs.infura.io',
port: 5001,
protocol: 'https',
headers: {
authorization: 'Basic ' + btoa(projectId + ':' + projectSecret)
}
});
7.3 合约事件监听
可使用 Web3.js 监听 NoteSaved 事件,实现笔记实时更新而不必刷新:
notepadContract.events.NoteSaved({
filter: { user: accounts[0] },
fromBlock: 0
}).on('data', event => {
console.log('新笔记:', event.returnValues.cid);
loadNotes();
});
8. 常见问题与排错
Q: ipfs.add 报错无法连接节点。
A: 确保 IPFS 守护进程运行,且 API 地址正确,CORS 已启用(ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin '["*"]' )。
Q: MetaMask 无法发送交易,提示 nonce 错误。 A: 在 Hardhat 本地网络中,重置账户的 nonce:在 MetaMask 设置 > 高级 > 重置账户。
Q: 公共网关无法加载 IPFS 内容。
A: 首次上传后可能需要一些时间广播,也可尝试使用本地网关 http://127.0.0.1:8080/ipfs/<cid>(需 IPFS 守护进程同时提供网关服务)。
9. 总结
通过本教程,你完成了一个以太坊全栈 DApp 的核心闭环:
- 使用 Solidity 编写了存储 IPFS CID 的智能合约。
- 用 Hardhat 编译部署至本地测试网络。
- 前端通过 Web3.js 连接钱包,调用合约方法。
- 利用 IPFS 去中心化存储文本内容,并将 CID 锚定在链上。
这为开发更复杂的 DApp(如 NFT 市场、去中心化社交媒体)打下了坚实基础。继续探索 Gas 优化、元交易、存储证明等高级主题,你的全栈之路将更加宽广。