初识Web3之从零构建许愿墙DApp

发布于 | 分类于 杂项|本文包含AIGC内容

春节假期闲来无聊,打算了解一下web3,借助AI构建了一个简单的许愿墙 DApp,学习Web2应用和Web3应用有什么差异。

这篇文章主要整理一下整个过程,包括遇到的坑和解决方案。

项目概述

许愿墙 DApp 是一个去中心化应用,用户可以:

  • 连接 MetaMask 钱包
  • 在区块链上发布愿望
  • 查看所有人的愿望
  • 愿望永久存储,不可篡改

技术栈:

  • 前端:React + Vite + ethers.js
  • 智能合约:Solidity
  • 开发工具:Remix IDE
  • 测试网络:Sepolia

第一步:理解 DApp 与传统应用的区别

在开始动手之前,我们先来看看 DApp 和传统应用到底有什么不同。

传统 Web2 应用架构

用户 → 前端 → 后端服务器 → 数据库(MySQL/MongoDB)
  • 数据存储在中心化服务器
  • 公司可以随意修改/删除数据
  • 需要信任运营方
  • 服务器宕机则应用不可用

Web3 DApp 架构

用户 → 前端 → 区块链网络(全球分布式节点)
  • 数据存储在去中心化网络
  • 一旦写入,任何人都无法篡改
  • 不需要信任中心化机构
  • 没有单点故障
  • 每次写入需要支付 Gas 费

什么是 ETH?

ETH(以太币)是以太坊区块链的原生加密货币,类似于比特币网络中的 BTC。它主要有两个用途:

  1. 价值存储和转账:可以像传统货币一样在用户之间转账
  2. 支付 Gas 费:在以太坊上执行任何操作(比如发布愿望)都需要消耗计算资源,需要用 ETH 支付费用

什么是 Gas 费?

Gas 费是使用以太坊网络的"手续费"。可以这样理解:

  • 你的愿望需要被全球成千上万台电脑记录和验证
  • 这些电脑(节点)需要消耗电力和计算资源
  • Gas 费就是对这些节点运营者的补偿

为什么需要 Gas 费?

  1. 激励节点运营者:补偿他们维护网络的成本
  2. 防止垃圾交易:如果免费,恶意用户可能发送大量无意义交易堵塞网络
  3. 资源分配:Gas 费高的交易会被优先处理

Gas 费的组成:

Gas 费 = Gas Used(消耗量) × Gas Price(单价)

例如:
发布一个愿望消耗 50,000 Gas
Gas Price 是 20 Gwei
总费用 = 50,000 × 20 Gwei = 0.001 ETH ≈ $2-3(主网)

在测试网上,ETH 是免费的,所以可以放心实验!

第二步:环境准备

1. 安装 MetaMask

MetaMask 是你的数字钱包,相当于"身份证 + 银行账户"。

安装步骤:

  1. 访问 https://metamask.io/
  2. 选择你的浏览器(Chrome/Firefox/Edge)
  3. 点击"添加到浏览器"
  4. 创建新钱包,设置密码
  5. 重要:备份助记词(12 个单词,务必妥善保管,丢了就找不回来了)

什么是钱包?

在 Web3 中,钱包不是存储加密货币的地方,而是管理你的"数字身份"的工具。

钱包 vs 账户:

钱包(Wallet)= 管理工具
├── 账户 1(Account 1): 0x1234...
├── 账户 2(Account 2): 0x5678...
└── 账户 3(Account 3): 0xabcd...
  • 钱包:像一个"钥匙串",可以管理多个账户(MetaMask 就是一个钱包)
  • 账户:一个具体的区块链地址,有自己的余额和交易历史

钱包的核心功能:

  1. 管理私钥

    • 私钥是一串随机数字,证明你拥有某个账户
    • 类似银行账户的密码,但丢失无法找回
    • 助记词(12 个单词)可以恢复私钥
  2. 签署交易

    • 当你发布愿望时,钱包用私钥"签名"
    • 签名证明这个操作确实是你本人发起的
    • 就像手写签名,但无法伪造
  3. 与 DApp 交互

    • 连接网站(如我们的许愿墙)
    • 授权交易
    • 查看余额和历史

关键概念:

公钥/地址(Public Key/Address)
- 类似银行账号:0x1234...5678
- 可以公开,别人可以给你转账
- 显示在区块浏览器上

私钥(Private Key)
- 类似银行密码
- 绝对不能告诉任何人
- 丢失 = 永久失去资产

助记词(Seed Phrase)
- 12 个英文单词
- 可以恢复私钥
- 务必手写备份,不要截图或保存在电脑上(血的教训)

重要提醒:

  • 你的加密货币不是"存在"钱包里,而是记录在区块链上
  • 钱包只是存储私钥,让你能够控制区块链上的资产
  • 私钥 = 所有权,谁有私钥谁就拥有资产

2. 切换到测试网络

学习阶段必须使用测试网,避免花费真实的 ETH。

切换步骤:

  1. 打开 MetaMask
  2. 点击左上角网络下拉菜单
  3. 如果看不到测试网:设置 → 高级 → 显示测试网络(开启)
  4. 选择 "Sepolia 测试网络"

3. 获取测试 ETH

在测试网上需要免费的测试币来支付 Gas 费。

水龙头网站:

每次可领取 0.05-0.5 ETH 测试币,足够发布很多愿望。

第三步:编写智能合约

智能合约是运行在区块链上的代码,定义了应用的核心逻辑。我们来看看怎么写一个简单的合约。

智能合约 vs 比特币区块链

比特币和以太坊都是区块链,但功能差异巨大:

比特币区块链(Bitcoin):

功能:转账
A 给 B 转 1 BTC → 记录在区块链上 → 完成

特点:
- 只能转账,不能运行程序
- 脚本语言非常有限
- 像一个"分布式账本"

以太坊区块链(Ethereum):

功能:转账 + 运行程序(智能合约)
A 调用合约 → 合约执行逻辑 → 改变状态 → 记录在区块链上

特点:
- 可以运行复杂程序
- 图灵完备的编程语言(Solidity)
- 像一个"世界计算机"

形象比喻:

比特币以太坊
计算器电脑
只能加减乘除可以运行各种程序
转账功能转账 + DeFi + NFT + 游戏...

智能合约能做什么?

  1. 去中心化金融(DeFi)

    • 借贷平台(Aave, Compound)
    • 去中心化交易所(Uniswap)
    • 稳定币(USDC, DAI)
  2. NFT(非同质化代币)

    • 数字艺术品
    • 游戏道具
    • 域名(ENS)
  3. DAO(去中心化自治组织)

    • 链上投票
    • 社区治理
    • 资金管理
  4. 其他应用

    • 供应链追踪
    • 身份认证
    • 预测市场

我们的许愿墙就是一个简单的智能合约应用,它在区块链上存储和管理数据,这在比特币上是没法实现的。

合约代码

Solidity 语言简介

Solidity 是以太坊智能合约的主要编程语言,专门为区块链设计。

语言特点:

  1. 类似 JavaScript

    • 语法接近 JavaScript/C++
    • 容易上手
    • 静态类型(需要声明变量类型)
  2. 面向合约

    • 使用 contract 关键字定义合约(类似 class)
    • 支持继承、接口、库
  3. 区块链特性

    • msg.sender:调用者地址
    • block.timestamp:当前区块时间
    • payable:可以接收 ETH
    • view/pure:只读函数,不消耗 Gas

基本语法对比:

solidity
// Solidity
contract MyContract {
    uint256 public count;  // 状态变量,存储在区块链上
    
    function increment() public {
        count += 1;
    }
}
javascript
// JavaScript(对比)
class MyContract {
    constructor() {
        this.count = 0;  // 存储在内存中
    }
    
    increment() {
        this.count += 1;
    }
}

关键区别:

  • Solidity 的状态变量永久存储在区块链上
  • 每次修改都需要支付 Gas 费
  • 代码一旦部署,没法修改

常用数据类型:

solidity
uint256    // 无符号整数(0 到 2^256-1)
address    // 以太坊地址(0x1234...)
string     // 字符串
bool       // 布尔值
mapping    // 类似哈希表/字典
array[]    // 数组
struct     // 结构体

OK,现在我们来看看许愿墙的合约代码:

solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract WishWall {
    struct Wish {
        address author;
        string content;
        uint256 timestamp;
    }
    
    Wish[] public wishes;
    
    event WishCreated(address indexed author, string content, uint256 timestamp);
    
    function createWish(string memory _content) public {
        require(bytes(_content).length > 0, unicode"愿望不能为空");
        require(bytes(_content).length <= 280, unicode"愿望太长了");
        
        wishes.push(Wish({
            author: msg.sender,
            content: _content,
            timestamp: block.timestamp
        }));
        
        emit WishCreated(msg.sender, _content, block.timestamp);
    }
    
    function getWishCount() public view returns (uint256) {
        return wishes.length;
    }
    
    function getWish(uint256 _index) public view returns (address, string memory, uint256) {
        require(_index < wishes.length, unicode"愿望不存在");
        Wish memory wish = wishes[_index];
        return (wish.author, wish.content, wish.timestamp);
    }
    
    function getAllWishes() public view returns (Wish[] memory) {
        return wishes;
    }
}

关键概念解释

struct(结构体): 定义愿望的数据结构,包含作者、内容、时间戳。

array(数组):wishes[] 存储所有愿望。

event(事件): 记录链上事件,方便前端监听和查询。

require(断言): 输入验证,确保数据合法。

unicode 字符串: Solidity 0.8+ 需要使用 unicode"..." 来支持中文,这里需要注意一下。

第四步:部署智能合约

使用 Remix IDE 在线部署,无需本地环境。

部署步骤

  1. 打开 Remix

  2. 创建合约文件

    • 点击"文件浏览器"
    • 创建新文件 WishWall.sol
    • 粘贴合约代码
  3. 编译合约

    • 点击"Solidity 编译器"图标
    • 选择编译器版本 0.8.0+
    • 点击"Compile WishWall.sol"
    • 看到绿色对勾表示成功

  4. 部署合约

    • 点击"部署和运行交易"图标
    • ENVIRONMENT 选择 "Injected Provider - MetaMask"
    • 确认 MetaMask 连接到 Sepolia 测试网
    • 点击 "Deploy" 按钮
    • MetaMask 弹出确认,点击"确认"
    • 等待交易确认(约 10-30 秒)

  5. 复制合约地址

    • 部署成功后,在 "Deployed Contracts" 下方会显示合约
    • 点击复制按钮,保存合约地址(类似 0x1234...abcd

常见问题

问题 1:编译错误 "Invalid character in string"

错误:require(bytes(_content).length > 0, "愿望不能为空");
正确:require(bytes(_content).length > 0, unicode"愿望不能为空");

中文字符串必须使用 unicode 前缀。

问题 2:MetaMask 连接的是主网

部署前务必确认:

  • MetaMask 左上角显示 "Sepolia 测试网络"
  • 如果显示 "Ethereum Mainnet",立即切换!
  • 主网需要真实 ETH,非常昂贵

第五步:构建前端应用

接下来我们来看看前端部分,这里跟传统 Web 开发还是有些区别的。

Web3 前端 vs 传统 Web 前端

Web3 前端应用和传统 Web 应用在架构上有本质区别:

传统 Web2 前端:

前端(React)
    ↓ HTTP/REST API
后端服务器(Node.js/Python)
    ↓ SQL
数据库(MySQL/MongoDB)

Web3 DApp 前端:

前端(React)
    ↓ ethers.js/web3.js
钱包(MetaMask)
    ↓ JSON-RPC
区块链网络(以太坊节点)

主要区别:

特性传统 Web2Web3 DApp
后端自己的服务器区块链网络
数据库MySQL/MongoDB智能合约
身份认证用户名/密码钱包签名
API 调用REST/GraphQL合约函数调用
数据读取免费免费
数据写入免费需要 Gas 费
响应速度毫秒级秒级(需要确认)

Web3 前端的核心功能:

  1. 连接钱包

    javascript
    // 请求用户授权连接钱包
    await window.ethereum.request({ 
      method: 'eth_requestAccounts' 
    })
  2. 读取区块链数据

    javascript
    // 调用合约的 view 函数(免费)
    const wishes = await contract.getAllWishes()
  3. 发送交易

    javascript
    // 调用合约的写入函数(需要 Gas)
    const tx = await contract.createWish("我的愿望")
    await tx.wait()  // 等待交易确认
  4. 监听事件

    javascript
    // 监听合约事件
    contract.on("WishCreated", (author, content) => {
      console.log(`${author} 发布了愿望: ${content}`)
    })

Web3 前端的特殊之处:

  • 无需后端服务器:智能合约就是后端
  • 用户自己支付费用:每次写入操作用户支付 Gas
  • 数据公开透明:任何人都能读取区块链数据
  • 异步确认:交易需要等待 10-30 秒确认
  • 不可撤销:交易一旦确认,没法回滚

开发体验对比:

javascript
// 传统 Web2
async function createPost(content) {
  const response = await fetch('/api/posts', {
    method: 'POST',
    headers: { 'Authorization': 'Bearer token' },
    body: JSON.stringify({ content })
  })
  return response.json()  // 立即返回
}

// Web3 DApp
async function createWish(content) {
  const tx = await contract.createWish(content)  // 发送交易
  await tx.wait()  // 等待 10-30 秒确认
  // 交易已永久记录在区块链上
}

OK,现在我们来看看如何构建 Web3 前端:

项目结构

wish-wall-dapp/
├── src/
│   ├── App.jsx          # 主应用组件
│   ├── App.css          # 样式文件
│   ├── main.jsx         # 入口文件
│   └── index.css        # 全局样式
├── contracts/
│   └── WishWall.sol     # 智能合约
├── index.html
├── package.json
└── vite.config.js

核心代码

javascript
import { useState } from 'react'
import { ethers } from 'ethers'
import './App.css'

// 合约 ABI
const CONTRACT_ABI = [
  "function createWish(string _content) public",
  "function getWishCount() public view returns (uint256)",
  "function getWish(uint256 _index) public view returns (address, string, uint256)",
  "function getAllWishes() public view returns (tuple(address author, string content, uint256 timestamp)[])",
  "event WishCreated(address indexed author, string content, uint256 timestamp)"
]

// 替换为你的合约地址
const CONTRACT_ADDRESS = "0xYourContractAddress"

function App() {
  const [account, setAccount] = useState('')
  const [contract, setContract] = useState(null)
  const [wishes, setWishes] = useState([])
  const [newWish, setNewWish] = useState('')
  const [loading, setLoading] = useState(false)

  // 连接钱包
  const connectWallet = async () => {
    try {
      if (!window.ethereum) {
        alert('请安装 MetaMask!')
        return
      }

      // 检查并切换到 Sepolia 测试网
      const chainId = await window.ethereum.request({ method: 'eth_chainId' })
      const sepoliaChainId = '0xaa36a7'
      
      if (chainId !== sepoliaChainId) {
        try {
          await window.ethereum.request({
            method: 'wallet_switchEthereumChain',
            params: [{ chainId: sepoliaChainId }],
          })
        } catch (switchError) {
          if (switchError.code === 4902) {
            await window.ethereum.request({
              method: 'wallet_addEthereumChain',
              params: [{
                chainId: sepoliaChainId,
                chainName: 'Sepolia 测试网络',
                nativeCurrency: {
                  name: 'SepoliaETH',
                  symbol: 'ETH',
                  decimals: 18
                },
                rpcUrls: ['https://rpc.sepolia.org'],
                blockExplorerUrls: ['https://sepolia.etherscan.io']
              }]
            })
          } else {
            alert('请手动切换到 Sepolia 测试网络')
            return
          }
        }
      }

      const accounts = await window.ethereum.request({ 
        method: 'eth_requestAccounts' 
      })
      setAccount(accounts[0])

      const provider = new ethers.BrowserProvider(window.ethereum)
      const signer = await provider.getSigner()
      const wishWallContract = new ethers.Contract(
        CONTRACT_ADDRESS, 
        CONTRACT_ABI, 
        signer
      )
      setContract(wishWallContract)

      loadWishes(wishWallContract)
    } catch (error) {
      console.error('连接钱包失败:', error)
      alert('连接失败: ' + error.message)
    }
  }

  // 加载所有愿望
  const loadWishes = async (contractInstance) => {
    try {
      const allWishes = await contractInstance.getAllWishes()
      setWishes(allWishes.map(wish => ({
        author: wish.author,
        content: wish.content,
        timestamp: Number(wish.timestamp)
      })).reverse())
    } catch (error) {
      console.error('加载愿望失败:', error)
    }
  }

  // 创建新愿望
  const createWish = async () => {
    if (!newWish.trim()) {
      alert('请输入你的愿望')
      return
    }

    if (!contract) {
      alert('请先连接钱包')
      return
    }

    try {
      setLoading(true)
      
      console.log('准备发送交易...')
      const tx = await contract.createWish(newWish, {
        gasLimit: 300000
      })
      
      console.log('交易已发送,哈希:', tx.hash)
      alert('交易已发送,等待确认...')
      
      const receipt = await tx.wait()
      console.log('交易已确认,Gas 使用:', receipt.gasUsed.toString())
      
      setNewWish('')
      await loadWishes(contract)
      alert('愿望发布成功!')
    } catch (error) {
      console.error('完整错误:', error)
      
      let errorMsg = '发布失败: '
      if (error.reason) {
        errorMsg += error.reason
      } else if (error.message) {
        errorMsg += error.message
      } else {
        errorMsg += '未知错误'
      }
      
      alert(errorMsg)
    } finally {
      setLoading(false)
    }
  }

  // 格式化地址
  const formatAddress = (address) => {
    return `${address.slice(0, 6)}...${address.slice(-4)}`
  }

  // 格式化时间
  const formatTime = (timestamp) => {
    return new Date(timestamp * 1000).toLocaleString('zh-CN')
  }

  return (
    <div className="app">
      <header className="header">
        <h1>🌟 许愿墙 DApp</h1>
        <p className="subtitle">在区块链上许下你的愿望</p>
        
        {!account ? (
          <button onClick={connectWallet} className="connect-btn">
            连接钱包
          </button>
        ) : (
          <div className="account-info">
            已连接: {formatAddress(account)}
          </div>
        )}
      </header>

      {account && (
        <div className="wish-form">
          <textarea
            value={newWish}
            onChange={(e) => setNewWish(e.target.value)}
            placeholder="写下你的愿望..."
            maxLength={280}
            rows={4}
          />
          <div className="form-footer">
            <span className="char-count">{newWish.length}/280</span>
            <button 
              onClick={createWish} 
              disabled={loading}
              className="submit-btn"
            >
              {loading ? '发布中...' : '发布愿望'}
            </button>
          </div>
        </div>
      )}

      <div className="wishes-container">
        <h2>愿望墙 ({wishes.length})</h2>
        {wishes.length === 0 ? (
          <p className="empty-message">还没有愿望,快来许下第一个愿望吧!</p>
        ) : (
          <div className="wishes-grid">
            {wishes.map((wish, index) => (
              <div key={index} className="wish-card">
                <p className="wish-content">{wish.content}</p>
                <div className="wish-meta">
                  <span className="wish-author">
                    {formatAddress(wish.author)}
                  </span>
                  <span className="wish-time">
                    {formatTime(wish.timestamp)}
                  </span>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  )
}

export default App

关键实现

1. 自动切换网络

代码会自动检测当前网络,如果不是 Sepolia,会提示用户切换。

2. Gas 限制

设置 gasLimit: 300000 确保交易有足够的 Gas。

3. 错误处理

详细的错误信息帮助调试问题。

4. 交易确认

使用 tx.wait() 等待交易被打包进区块。

第六步:运行项目

安装依赖

bash
npm install

配置合约地址

src/App.jsx 中,将 CONTRACT_ADDRESS 替换为你部署的合约地址:

javascript
const CONTRACT_ADDRESS = "0xYourContractAddress"

启动开发服务器

bash
npm run dev

访问 http://localhost:5173

使用流程

  1. 点击"连接钱包"
  2. MetaMask 弹出,确认连接
  3. 自动切换到 Sepolia 测试网(如果需要)
  4. 输入愿望内容
  5. 点击"发布愿望"
  6. MetaMask 弹出交易确认,查看 Gas 费用
  7. 点击"确认"
  8. 等待 10-30 秒交易确认
  9. 愿望显示在墙上

完整效果

最终界面效果如下所示:

在这个过程中,可以深刻体会到 Web3 应用与传统应用的本质区别。

1. 数据存储方式的革命

  • 传统应用:数据存储在公司的数据库服务器中,公司可以随意修改或删除
  • Web3 应用:数据永久存储在区块链上,一旦发布就没法被任何人(包括开发者)修改或删除
  • 实际体验:你发布的每一个愿望都会被全球成千上万个节点记录,即使这个网站关闭了,你的愿望依然存在于区块链上

2. 用户身份系统的变革

  • 传统应用:需要注册账号、设置密码、绑定邮箱/手机,用户数据存储在公司服务器
  • Web3 应用:通过钱包地址作为唯一身份标识,无需注册,连接钱包即可使用
  • 实际体验:你的身份(钱包地址)是全球通用的,可以在任何 DApp 中使用同一个身份

3. 信任模型的转变

  • 传统应用:需要信任运营公司不会作恶、不会泄露数据、不会随意修改规则
  • Web3 应用:不需要信任任何中心化机构,智能合约代码公开透明,规则由代码保证
  • 实际体验:你可以在 Etherscan 上查看合约代码,验证它确实按照承诺的方式运行

4. 成本和激励机制

  • 传统应用:用户免费使用,公司承担服务器成本,通过广告或增值服务盈利
  • Web3 应用:用户为每次写入操作支付 Gas 费,直接补偿网络维护者
  • 实际体验:发布愿望需要支付少量 ETH,但这保证了网络的可持续运行

5. 数据所有权

  • 传统应用:你创建的内容属于平台,平台可以随时删除或限制访问
  • Web3 应用:你创建的内容永久属于你,任何人都没法剥夺
  • 实际体验:你的愿望永远与你的钱包地址关联,这是不可篡改的所有权证明

6. 应用的持久性

  • 传统应用:如果公司倒闭或服务器关闭,应用和数据都会消失
  • Web3 应用:只要区块链网络存在,应用就能继续运行,前端可以托管在任何地方
  • 实际体验:即使原开发者停止维护,任何人都可以部署新的前端继续访问合约

7. 透明度和可验证性

  • 传统应用:后端逻辑是黑盒,用户没法验证系统是否按承诺运行
  • Web3 应用:智能合约代码完全公开,所有交易记录可追溯
  • 实际体验:你可以在区块浏览器上查看每一笔交易的详细信息,包括 Gas 费用、执行结果等

这些特性共同构成了 Web3 的核心价值:去中心化、透明、不可篡改、用户拥有数据所有权。虽然目前 Web3 还有性能、成本、用户体验等方面的挑战,但它代表了互联网发展的一个重要方向。

前端主要是做查询、展示和提交,跟传统的 web 前端开发差别不太大。

调试问题记录

在开发过程中踩了一些坑,这里记录一下。

问题 1:交易失败 - 销毁地址警告

现象:

您正在将资产发送至销毁地址。若继续操作,您将损失该笔资产。

原因: 合约地址还是默认的 0x0000000000000000000000000000000000000000(零地址)。

解决: 在 Remix 中部署合约,复制真实的合约地址,替换代码中的 CONTRACT_ADDRESS

问题 2:Gas 费用异常高(0.09 ETH)

现象: 发布愿望时,Gas 费用显示 0.09 ETH,远超测试币余额。

原因: MetaMask 连接到了以太坊主网,而不是 Sepolia 测试网。

解决:

  1. 打开 MetaMask
  2. 点击左上角网络名称
  3. 切换到 "Sepolia 测试网络"
  4. 刷新页面,重新连接

正常情况下,Sepolia 的 Gas 费用应该是 0.0001-0.003 ETH。

问题 3:Remix 中测试合约失败

现象: 在 Remix 的 "Low level interactions" 区域看到错误:

Both 'receive' and 'fallback' functions are not defined

原因: 这个警告是因为合约不能接收 ETH,但这不是问题,因为我们的合约本来就不需要接收 ETH。

解决: 忽略这个警告,直接点击橙色的 createWish 按钮测试功能就行。

问题 4:交易发送后失败

调试步骤:

  1. 打开浏览器控制台(F12)
  2. 查看详细错误信息
  3. 增加 Gas 限制到 300000
  4. 确认合约地址正确
  5. 在 Remix 中测试合约函数是否正常

Web3 核心概念深入理解

区块链 vs Git

区块链和 Git 有很多相似之处:

相似点:

  • 分布式存储(每个节点/开发者都有完整副本)
  • 不可篡改的历史(每个区块/commit 有唯一 hash)
  • 透明可见(所有记录公开)
  • 防止单点篡改(修改历史会被发现)

关键区别:

特性Git区块链
权限模型有管理员无权限,任何人可提交
同步机制手动 pull自动实时同步
冲突解决手动解决算法自动解决(共识)
验证机制信任为主零信任,全部验证
共识机制强制共识(PoW/PoS)
成本免费付费(Gas)

本质理解:

区块链 = Git + 自动同步 + 共识机制 + 经济激励 - 中心化权限

主网的本质

主网不是部署在某个服务器上,而是由全球成千上万台电脑(节点)组成的网络。

结构:

🖥️ 美国节点 (~2000)
🖥️ 欧洲节点 (~1500)
🖥️ 亚洲节点 (~1000)

所有节点存储完整区块链
通过 P2P 网络同步

不同的区块链 = 不同的主网:

  • 以太坊主网(Chain ID: 1)
  • Polygon 主网(Chain ID: 137)
  • BNB Chain(Chain ID: 56)
  • Arbitrum(Chain ID: 42161)

每个主网都有对应的测试网用于开发。

Gas 费用详解

为什么需要 Gas 费?

  • 激励矿工/验证者处理交易
  • 防止垃圾交易(spam)
  • 你的每个愿望都需要全球成千上万台电脑记录

Gas 费计算:

Gas 费 = Gas Used × Gas Price

例如:
Gas Used: 50,000(发布愿望消耗的计算量)
Gas Price: 20 Gwei(你愿意支付的单价)
Gas 费 = 50,000 × 20 = 1,000,000 Gwei = 0.001 ETH

单位换算:

1 ETH = 1,000,000,000 Gwei (10^9)
1 Gwei = 0.000000001 ETH

交易生命周期

1. 发起交易

2. MetaMask 签名(证明是你本人)

3. 广播到网络(发送给多个节点)

4. 进入交易池(等待打包)

5. 验证者选择交易打包进区块

6. 区块广播到全网

7. 所有节点验证并接受

8. 交易确认完成

整个过程大约 10-30 秒。

智能合约的不可变性与升级策略

一旦部署,无法修改:

  • 代码永久存在链上
  • 无法删除或更新
  • 这是特性,不是 bug(保证了去中心化和可信任性)

这是智能合约最重要的特性之一,但也带来了挑战:如果发现 bug 或需要添加新功能怎么办?

方案 1:部署新合约(最简单)

适用场景:

  • 合约逻辑简单
  • 历史数据不重要
  • 用户量较小

操作步骤:

  1. 修改合约代码
solidity
// 新版本:添加点赞功能
contract WishWallV2 {
    struct Wish {
        address author;
        string content;
        uint256 timestamp;
        uint256 likes;  // 新增
    }
    
    // ... 其他代码
    
    function likeWish(uint256 _index) public {
        wishes[_index].likes++;
    }
}
  1. 在 Remix 中部署新合约

    • 编译新版本合约
    • 部署到区块链
    • 获得新的合约地址
  2. 更新前端代码

javascript
// 旧地址
const OLD_CONTRACT_ADDRESS = "0x旧合约地址"

// 新地址
const CONTRACT_ADDRESS = "0x新合约地址"

// 更新 ABI(添加新函数)
const CONTRACT_ABI = [
  "function createWish(string _content) public",
  "function getAllWishes() public view returns (...)",
  "function likeWish(uint256 _index) public"  // 新增
]
  1. 通知用户
    • 发布公告说明升级
    • 旧合约数据仍然存在,但不再使用

优点:

  • 简单直接
  • 完全控制新合约

缺点:

  • 旧数据丢失(除非手动迁移)
  • 用户需要重新授权
  • 合约地址改变

方案 2:数据迁移(保留历史)

如果需要保留旧数据,可以在新合约中读取旧合约的数据。

实现方式:

solidity
// 新合约引用旧合约
contract WishWallV2 {
    WishWall public oldContract;  // 旧合约实例
    
    constructor(address _oldContractAddress) {
        oldContract = WishWall(_oldContractAddress);
    }
    
    // 获取所有愿望(包括旧数据)
    function getAllWishes() public view returns (Wish[] memory) {
        // 先获取旧合约的数据
        Wish[] memory oldWishes = oldContract.getAllWishes();
        
        // 合并新旧数据
        Wish[] memory allWishes = new Wish[](oldWishes.length + wishes.length);
        
        for (uint i = 0; i < oldWishes.length; i++) {
            allWishes[i] = oldWishes[i];
        }
        
        for (uint i = 0; i < wishes.length; i++) {
            allWishes[oldWishes.length + i] = wishes[i];
        }
        
        return allWishes;
    }
}

优点:

  • 保留历史数据
  • 用户体验连续

缺点:

  • 读取旧数据会消耗更多 Gas
  • 实现复杂度增加

方案 3:代理模式(Proxy Pattern)- 专业方案

代理模式是最灵活的升级方案,被 DeFi 项目广泛使用。

核心思想:

用户 → 代理合约(地址不变)→ 逻辑合约(可升级)

架构:

solidity
// 代理合约(地址永不改变)
contract Proxy {
    address public implementation;  // 指向逻辑合约
    
    // 将所有调用转发到逻辑合约
    fallback() external payable {
        address impl = implementation;
        assembly {
            // 转发调用
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
    
    // 升级函数(只有管理员可调用)
    function upgradeTo(address newImplementation) external {
        require(msg.sender == admin, "Only admin");
        implementation = newImplementation;
    }
}

// 逻辑合约 V1
contract WishWallV1 {
    // 业务逻辑
}

// 逻辑合约 V2(升级版)
contract WishWallV2 {
    // 新的业务逻辑
}

升级流程:

  1. 部署代理合约(一次性)
  2. 部署逻辑合约 V1
  3. 代理合约指向 V1
  4. 用户使用代理合约地址
  5. 需要升级时:
    • 部署逻辑合约 V2
    • 调用代理合约的 upgradeTo(V2地址)
    • 用户无感知,继续使用相同地址

优点:

  • 合约地址永不改变
  • 可以无限次升级
  • 数据保留在代理合约中
  • 用户体验最好

缺点:

  • 实现复杂
  • 需要仔细设计存储布局
  • 有安全风险(管理员权限过大)

使用 OpenZeppelin 的代理合约:

bash
npm install @openzeppelin/contracts-upgradeable
solidity
// 使用 OpenZeppelin 的可升级合约
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract WishWall is Initializable {
    function initialize() public initializer {
        // 初始化逻辑(替代 constructor)
    }
}

方案 4:预留升级接口(设计时考虑)

在设计合约时就考虑未来的扩展性。

示例:

solidity
contract WishWall {
    address public admin;
    bool public paused;
    
    // 暂停功能(发现 bug 时紧急暂停)
    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }
    
    function pause() external {
        require(msg.sender == admin, "Only admin");
        paused = true;
    }
    
    function unpause() external {
        require(msg.sender == admin, "Only admin");
        paused = false;
    }
    
    // 所有写入函数都添加 whenNotPaused
    function createWish(string memory _content) public whenNotPaused {
        // ...
    }
    
    // 预留扩展接口
    mapping(uint256 => bytes) public extensionData;
    
    function setExtensionData(uint256 _wishId, bytes memory _data) public {
        // 未来可以用来存储额外数据
        extensionData[_wishId] = _data;
    }
}

实际项目中的建议

对于学习项目(如许愿墙):

  • 使用方案 1(部署新合约)
  • 简单直接,易于理解

对于小型项目:

  • 使用方案 2(数据迁移)
  • 保留用户数据,体验更好

对于大型 DeFi 项目:

  • 使用方案 3(代理模式)
  • 使用 OpenZeppelin 的成熟方案
  • 进行专业的安全审计

最佳实践:

  1. 充分测试:在测试网上反复测试,确保没有 bug
  2. 代码审计:重要项目必须进行安全审计
  3. 时间锁:升级操作设置延迟期,给用户反应时间
  4. 多签钱包:升级权限由多人共同控制
  5. 逐步去中心化:最终将升级权限交给社区治理

我们的许愿墙升级示例

假设我们要添加点赞功能,使用最简单的方案:

  1. 修改合约(已在"项目扩展建议"章节展示)
  2. 部署新合约到 Sepolia
  3. 更新前端
javascript
const CONTRACT_ADDRESS = "0x新合约地址"
const CONTRACT_ABI = [
  // ... 原有函数
  "function likeWish(uint256 _index) public",
  "function getWishLikes(uint256 _index) public view returns (uint256)"
]
  1. 添加点赞按钮到前端 UI
  2. 发布更新

这就是最简单的合约升级流程!

读操作 vs 写操作

读操作(免费):

javascript
await contract.getAllWishes()  // 查看愿望
await contract.getWishCount()  // 获取数量
  • 不改变区块链状态
  • 不需要 Gas 费
  • 瞬间完成

写操作(需要 Gas):

javascript
await contract.createWish("我的愿望")  // 发布愿望
  • 改变区块链状态
  • 需要支付 Gas 费
  • 需要等待确认

Web3 的成本与适用场景

这是理解 Web3 应用的关键问题:Web3 的成本结构与传统 Web2 完全不同,这直接决定了它的适用场景。

成本对比分析

传统 Web2 应用:

用户操作:完全免费
公司成本(每月):
- 服务器:$100-1,000
- 数据库:$50-500
- 带宽:$50-1,000
- 运维人员:$5,000-20,000

商业模式:公司承担成本,通过广告/会员盈利
用户体验:免费使用,无感知

Web3 应用(以太坊主网):

用户操作:每次写入需要 Gas 费
- 发布一条消息:$5-50
- 简单转账:$2-20
- 复杂合约交互:$20-200
- 网络拥堵时可能更贵

商业模式:用户直接承担成本
用户体验:每次操作都需要支付并等待确认

为什么 Web3 成本这么高?

全球冗余存储:

Web2:数据存储 1-3 个副本(主库 + 备份)
Web3:数据存储在数千个节点上

你的一条愿望 = 全球数千台电脑都要存储和验证

共识机制成本:

Web2:中心化验证,毫秒级完成
Web3:需要多个节点达成共识,消耗大量计算资源

永久存储成本:

Web2:可以删除旧数据,降低成本
Web3:数据永久存储,存储成本持续累积

Web3 的适用场景

✅ 适合 Web3 的场景

金融应用(DeFi)

典型应用:Uniswap(去中心化交易所)、Aave(借贷协议)

为什么适合:
- 涉及真实价值转移
- 需要绝对的透明和不可篡改
- Gas 费相对交易金额很小

实际案例:
- 交易 $10,000,Gas 费 $20 = 0.2%
- 用户可以接受这个成本
- 去中心化带来的价值 > 成本

数字资产(NFT)

典型应用:OpenSea(NFT 市场)、艺术品、游戏道具

为什么适合:
- 需要证明所有权和稀缺性
- 一次性铸造,长期持有
- 资产价值远大于 Gas 费

实际案例:
- 购买一个 $1,000 的 NFT
- Gas 费 $50 = 5%
- 但获得了永久的所有权证明

身份和凭证

典型应用:ENS 域名(vitalik.eth)、链上学历证书

为什么适合:
- 需要永久存在
- 防伪造
- 低频操作(一次性或很少修改)

实际案例:
- 注册一个 ENS 域名,支付 $50
- 终身有效,可以转让
- 一次性成本,长期价值

治理和投票

典型应用:DAO 治理、社区决策

为什么适合:
- 需要透明和防篡改
- 投票结果需要永久记录
- 低频高价值操作

实际案例:
- MakerDAO 治理投票
- 每次投票 $10-50
- 但决定数百万美元的资金使用

供应链溯源

典型应用:奢侈品防伪、食品安全追踪

为什么适合:
- 需要不可篡改的记录
- 商品价值远大于 Gas 费
- 低频写入(生产、流转节点)

实际案例:
- 一瓶 $1,000 的红酒
- 溯源记录成本 $5
- 提升了商品可信度和价值

❌ 不适合 Web3 的场景

社交媒体

场景:Twitter、微博、朋友圈

为什么不适合:
- 高频操作(每天几十次发布、点赞、评论)
- 每次 $5-50 的 Gas 费用户无法接受
- 内容价值低,不值得上链

如果 Twitter 上链:
- 发一条推文:$10
- 点赞:$5
- 转发:$10
- 每天成本:$100+
→ 完全不可行

即时通讯

场景:微信、WhatsApp、Telegram

为什么不适合:
- 超高频(每天数百条消息)
- 需要实时性(毫秒级响应)
- 区块链确认太慢(10-30 秒)

实际体验:
- 发一条消息 $5 + 等待 30 秒
- 用户体验灾难

视频/图片存储

场景:YouTube、Instagram、抖音

为什么不适合:
- 数据量巨大(GB 级别)
- 链上存储成本天文数字

实际成本:
- 存储 1GB 数据到以太坊:数百万美元
- 完全不现实
- 即使用 IPFS,成本也很高

游戏实时数据

场景:游戏中的移动、攻击、技能释放

为什么不适合:
- 超高频操作(每秒数十次)
- 需要毫秒级响应
- 区块链无法满足性能要求

实际体验:
- 每次移动 $5 + 等待 30 秒
- 游戏完全无法玩

搜索引擎

场景:Google、百度

为什么不适合:
- 需要中心化的索引和排序算法
- 实时性要求极高
- 数据量巨大
- 区块链无法提供高效的搜索功能

混合架构:Web2 + Web3

实际上,大多数成功的 Web3 应用都采用混合架构,结合两者的优势:

架构设计:

链上(Web3)存储:
✓ 资产所有权(NFT、代币)
✓ 关键交易记录
✓ 治理决策
✓ 智能合约逻辑

链下(Web2)存储:
✓ 用户界面和前端
✓ 社交互动(评论、点赞)
✓ 大文件存储(图片、视频 → IPFS)
✓ 实时数据和缓存
✓ 搜索和筛选功能

成功案例:OpenSea(NFT 市场)

链上:
- NFT 所有权记录
- 交易历史
- 智能合约(买卖逻辑)

链下:
- NFT 图片存储(IPFS)
- 搜索和筛选功能
- 用户评论和收藏
- 实时价格更新
- 推荐算法

成功案例:Axie Infinity(区块链游戏)

链上:
- 游戏资产(Axie NFT)
- 代币经济(SLP、AXS)
- 资产交易

链下:
- 游戏战斗逻辑
- 实时对战
- 游戏画面渲染
- 社交功能

Layer 2 解决方案

为了降低成本和提高性能,出现了多层网络架构:

以太坊主网(Layer 1):

Gas 费:$5-50
速度:15 TPS(每秒交易数)
安全性:最高
适用:高价值资产和交易

Layer 2(Arbitrum, Optimism, zkSync):

Gas 费:$0.1-1(降低 90%+)
速度:1,000+ TPS
安全性:继承主网安全性
适用:日常交易和应用

侧链(Polygon, BNB Chain):

Gas 费:$0.01-0.1(降低 99%+)
速度:10,000+ TPS
安全性:独立的安全机制
适用:高频低价值操作

成本对比:

发布一个愿望的成本:
- 以太坊主网:$10-50
- Arbitrum (L2):$0.5-2
- Polygon:$0.01-0.1

同样的功能,成本差异 100-1000 倍!

未来趋势

成本会持续降低:

  • Layer 2 技术日益成熟
  • 以太坊升级(Danksharding)
  • 更高效的共识机制(PoS)
  • 数据可用性层(DA Layer)

应用会分层部署:

高价值资产 → 以太坊主网(最安全,最贵)
中等价值 → Layer 2(平衡安全性和成本)
低价值高频 → 侧链/其他链(最便宜,最快)

混合架构成为主流:

  • 关键数据上链(所有权、交易)
  • 非关键数据链下(内容、社交)
  • 用户体验接近 Web2
  • 保留 Web3 的核心价值

选择 Web3 的决策标准

使用 Web3 当满足以下条件:

✓ 价值 > 成本(资产价值远大于 Gas 费)
✓ 需要去中心化(抗审查、无单点故障)
✓ 需要不可篡改(永久记录、可验证)
✓ 低频高价值操作(不是每秒都要交互)
✓ 需要透明公开(所有人可验证)
✓ 需要用户拥有数据所有权

使用 Web2 当满足以下条件:

✓ 高频低价值操作(社交、聊天)
✓ 需要实时性(毫秒级响应)
✓ 大数据量(视频、图片)
✓ 用户体验优先(免费、快速)
✓ 需要复杂的查询和搜索
✓ 需要中心化的内容审核

关键洞察

Web3 不是要替代 Web2,而是在特定场景下提供更好的解决方案。就像:

  • 飞机不会替代汽车(各有适用场景)
  • 区块链不会替代数据库(各有优势)
  • 去中心化不会替代中心化(各有价值)

我们的许愿墙项目是一个教学案例,帮助理解 Web3 的基本原理。在实际应用中,类似的社交功能更适合采用混合架构:

  • 愿望的所有权记录上链(证明是你发布的)
  • 愿望内容存储在 IPFS 或中心化服务器(降低成本)
  • 点赞、评论等社交互动在链下(提升体验)

Web3 的真正价值在于它为特定问题提供了更好的解决方案:

  • 金融:无需信任的价值转移
  • 资产:可验证的数字所有权
  • 治理:透明的决策机制
  • 身份:用户控制的数字身份

理解了成本和适用场景,你就能更好地判断何时使用 Web3,何时使用 Web2,以及如何结合两者的优势。

项目扩展建议

学完基础后,可以尝试添加以下功能:

1. 点赞功能

solidity
mapping(uint256 => uint256) public wishLikes;
mapping(uint256 => mapping(address => bool)) public hasLiked;

function likeWish(uint256 _index) public {
    require(!hasLiked[_index][msg.sender], "已经点过赞了");
    wishLikes[_index]++;
    hasLiked[_index][msg.sender] = true;
}

2. 愿望分类

solidity
enum Category { CAREER, LOVE, HEALTH, WEALTH, OTHER }

struct Wish {
    address author;
    string content;
    uint256 timestamp;
    Category category;  // 新增
}

3. 用户个人主页

显示某个地址发布的所有愿望。

4. 集成 IPFS

将愿望内容存储到 IPFS,链上只存储哈希,节省 Gas。

5. 添加 ENS 支持

显示用户的 ENS 域名(如 vitalik.eth)而不是地址。

学习资源推荐

官方文档:

开发工具:

区块浏览器:

学习平台:

总结

通过构建这个简单的许愿墙 DApp,了解了:

  1. Web3 的核心思想:去中心化、不可篡改、透明公开
  2. 智能合约开发:Solidity 基础语法和最佳实践
  3. 前端集成:使用 ethers.js 与区块链交互
  4. 钱包使用:MetaMask 的配置和使用
  5. 调试技巧:如何排查和解决常见问题

目前看起来,区块链的本质就像一个去中心化的 Git,通过共识机制和经济激励,实现了无需信任的价值传递。

并不是所有的应用都适合放在区块链上面,具体的应用场景,还需要了解一下。

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。