以太坊 DApp 开发:Web3.js 与 IPFS 存储

FreeGuideOnline 最新 2026-06-12

以太坊 DApp 全栈开发:Web3.js 与 IPFS 存储

本教程将带你从零开始构建一个完整的去中心化应用(DApp),前端使用 Web3.js 与以太坊交互,存储层使用 IPFS 分布式文件系统。你将掌握智能合约编写、本地测试链部署、前端集成以及 IPFS 文件上传与合约关联的全流程。

1. DApp 全栈概览

一个完整的以太坊 DApp 通常包含以下部分:

  • 智能合约:运行在以太坊虚拟机(EVM)上的业务逻辑。
  • 区块链交互库:前端通过 Web3.jsEthers.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.jsindex.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-serverhttp-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 完整流程验证

  1. 点击“连接钱包”,选择 Hardhat 本地测试账户。
  2. 在输入框输入一段话,点击“保存到 IPFS & 合约”。
  3. MetaMask 弹出交易确认,确认后等待交易上链。
  4. 页面自动刷新笔记列表,调用 getMyNotes 并拉取 IPFS 内容展示。

如果此时 IPFS 本地节点已运行,浏览器会通过网关获取内容并渲染。

7. 进阶优化与部署指南

7.1 使用环境变量与前端构建工具

  • 使用 Vite + React 可以更高效地管理依赖,通过 import.meta.env 存储合约地址。
  • web3ipfs-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 优化、元交易、存储证明等高级主题,你的全栈之路将更加宽广。