以太坊第一個 DApp 開發完整教學:從零到部署

帶領讀者從零開始,開發並部署第一個以太坊 DApp。詳細講解智慧合約開發、前端整合、錢包連接、測試網部署等完整流程,涵蓋 Hardhat、Solidity、React、Ethers.js 等主流開發工具。

以太坊第一個 DApp 開發完整教學:從零到部署

概述

去中心化應用程式(Decentralized Application,簡稱 DApp)是建立在區塊鏈之上的應用程式,透過智慧合約實現業務邏輯,使用戶能夠在無需信任第三方的情況下進行交易和互動。以太坊是目前最成熟的 DApp 開發平台,擁有完整的開發工具生態系統和豐富的學習資源。

本文將帶領讀者從零開始,開發並部署第一個以太坊 DApp。我們將建立一個簡單的「留言板」應用程式,讓用戶可以在區塊鏈上發布和查看留言。這個專案雖然簡單,但涵蓋了 DApp 開發的核心概念:智慧合約編寫、前端介面開發、錢包連接、區塊鏈交互等。

本教學適合具備基礎程式設計經驗(JavaScript、Solidity 基礎)的讀者。我們將使用主流的開發工具和框架,包括 Hardhat、Solidity、Ethers.js、React,讓讀者能夠掌握業界實際使用的開發流程。


第一章:開發環境建置

1.1 必需工具安裝

在開始開發之前,需要安裝以下工具:

Node.js 與 npm

# 檢查 Node.js 版本(需要 v14+)
node --version

# 檢查 npm 版本
npm --version

Git

# 檢查 Git 版本
git --version

程式碼編輯器

推薦使用 Visual Studio Code,並安裝以下擴充套件:

1.2 建立專案目錄

# 建立專案資料夾
mkdir my-first-dapp
cd my-first-dapp

# 初始化 npm 專案
npm init -y

1.3 安裝開發依賴

# 安裝 Hardhat - 以太坊開發環境
npm install --save-dev hardhat

# 安裝 Ethers.js - 以太坊區塊鏈交互庫
npm install ethers

# 安裝 Wagmi - React Hooks 庫
npm install wagmi viem

# 安裝 Vite - 前端建置工具
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm install ethers wagmi viem @tanstack/react-query

1.4 初始化 Hardhat

# 回到專案根目錄
cd ..

# 初始化 Hardhat 專案
npx hardhat init

選擇「Create a JavaScript project」,Hardhat 會自動建立基本的專案結構:

my-first-dapp/
├── contracts/           # 智慧合約目錄
├── scripts/             # 部署腳本目錄
├── test/                # 測試目錄
├── hardhat.config.js    # Hardhat 設定檔
└── package.json

第二章:智慧合約開發

2.1 撰寫留言板合約

contracts/ 目錄下建立 Guestbook.sol 檔案:

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

/**
 * @title Guestbook
 * @dev 簡單的區塊鏈留言板合約
 * @notice 允許用戶發布和查看留言
 */
contract Guestbook {
    // 留言結構
    struct Message {
        address author;    // 留言者地址
        string content;     // 留言內容
        uint256 timestamp; // 留言時間
    }

    // 存儲所有留言的陣列
    Message[] public messages;

    // 記錄用戶的留言數量
    mapping(address => uint256[]) public userMessages;

    // 事件:當有新留言時觸發
    event NewMessage(address indexed author, string content, uint256 timestamp);

    /**
     * @dev 發布新留言
     * @param _content 留言內容
     */
    function postMessage(string memory _content) public {
        require(bytes(_content).length > 0, "留言內容不能為空");
        require(bytes(_content).length <= 500, "留言內容不能超過 500 字元");

        Message memory newMessage = Message({
            author: msg.sender,
            content: _content,
            timestamp: block.timestamp
        });

        messages.push(newMessage);
        userMessages[msg.sender].push(messages.length - 1);

        emit NewMessage(msg.sender, _content, block.timestamp);
    }

    /**
     * @dev 獲取所有留言數量
     * @return 留言總數
     */
    function getMessageCount() public view returns (uint256) {
        return messages.length;
    }

    /**
     * @dev 獲取指定範圍內的留言
     * @param _start 起始索引
     * @param _limit 最大數量
     * @return 留言陣列
     */
    function getMessages(uint256 _start, uint256 _limit) 
        public 
        view 
        returns (Message[] memory) 
    {
        uint256 length = _limit;
        if (_start + _limit > messages.length) {
            length = messages.length - _start;
        }

        Message[] memory result = new Message[](length);
        for (uint256 i = 0; i < length; i++) {
            result[i] = messages[_start + i];
        }
        return result;
    }

    /**
     * @dev 獲取用戶的所有留言
     * @param _user 用戶地址
     * @return 用戶的所有留言
     */
    function getUserMessages(address _user) 
        public 
        view 
        returns (Message[] memory) 
    {
        uint256[] storage indices = userMessages[_user];
        Message[] memory result = new Message[](indices.length);
        
        for (uint256 i = 0; i < indices.length; i++) {
            result[i] = messages[indices[i]];
        }
        return result;
    }
}

2.2 合約編譯

# 編譯智慧合約
npx hardhat compile

成功編譯後,會在 artifacts/ 目錄產生合約 ABI 和 Bytecode。

2.3 撰寫測試

test/ 目錄下建立 Guestbook.js 測試檔案:

const { expect } = require("chai");
const { ethers } = require("hardhat");

describe("Guestbook", function () {
  let guestbook;
  let owner;
  let user1;
  let user2;

  beforeEach(async function () {
    // 取得測試帳戶
    [owner, user1, user2] = await ethers.getSigners();

    // 部署合約
    const Guestbook = await ethers.getContractFactory("Guestbook");
    guestbook = await Guestbook.deploy();
    await guestbook.waitForDeployment();
  });

  describe("postMessage", function () {
    it("應該允許用戶發布留言", async function () {
      const tx = await guestbook.connect(user1).postMessage("Hello Web3!");
      await tx.wait();

      const messageCount = await guestbook.getMessageCount();
      expect(messageCount).to.equal(1);
    });

    it("應該在內容為空時 revert", async function () {
      await expect(
        guestbook.connect(user1).postMessage("")
      ).to.be.revertedWith("留言內容不能為空");
    });

    it("應該在內容過長時 revert", async function () {
      const longContent = "a".repeat(501);
      await expect(
        guestbook.connect(user1).postMessage(longContent)
      ).to.be.revertedWith("留言內容不能超過 500 字元");
    });

    it("應該正確記錄留言者地址", async function () {
      await guestbook.connect(user1).postMessage("Test message");
      
      const messages = await guestbook.getMessages(0, 1);
      expect(messages[0].author).to.equal(user1.address);
    });
  });

  describe("getMessages", function () {
    beforeEach(async function () {
      // 發布多條留言
      await guestbook.connect(user1).postMessage("Message 1");
      await guestbook.connect(user2).postMessage("Message 2");
      await guestbook.connect(user1).postMessage("Message 3");
    });

    it("應該返回正確數量的留言", async function () {
      const messages = await guestbook.getMessages(0, 10);
      expect(messages.length).to.equal(3);
    });

    it("應該正確返回指定範圍的留言", async function () {
      const messages = await guestbook.getMessages(1, 2);
      expect(messages.length).to.equal(2);
      expect(messages[0].content).to.equal("Message 2");
    });
  });

  describe("getUserMessages", function () {
    it("應該返回用戶的所有留言", async function () {
      await guestbook.connect(user1).postMessage("User1 msg 1");
      await guestbook.connect(user1).postMessage("User1 msg 2");
      await guestbook.connect(user2).postMessage("User2 msg 1");

      const user1Messages = await guestbook.getUserMessages(user1.address);
      expect(user1Messages.length).to.equal(2);
    });
  });
});

執行測試:

npx hardhat test

第三章:本機部署與測試

3.1 啟動本機測試網路

# 啟動本機 Hardhat 節點
npx hardhat node

這會啟動一個本機以太坊測試網路,提供 20 個測試帳戶,每個帳戶有 10,000 ETH(測試用)。

3.2 部署腳本

建立 scripts/deploy.js 部署腳本:

const hre = require("hardhat");

async function main() {
  console.log("開始部署 Guestbook 合約...");

  // 取得合約工廠
  const Guestbook = await hre.ethers.getContractFactory("Guestbook");

  // 部署合約
  const guestbook = await Guestbook.deploy();
  
  // 等待部署完成
  await guestbook.waitForDeployment();
  
  // 取得合約地址
  const contractAddress = await guestbook.getAddress();

  console.log(`Guestbook 合約已部署至: ${contractAddress}`);
  
  // 驗證合約
  console.log("\n驗證合約功能...");
  
  // 發布測試留言
  const tx = await guestbook.postMessage("Hello from Hardhat!");
  await tx.wait();
  
  const messageCount = await guestbook.getMessageCount();
  console.log(`留言數量: ${messageCount}`);
  
  const messages = await guestbook.getMessages(0, 1);
  console.log(`第一條留言: ${messages[0].content}`);
  console.log(`留言者: ${messages[0].author}`);
  
  console.log("\n部署成功!");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

3.3 部署到本機網路

# 在另一個終端執行
npx hardhat run scripts/deploy.js --network localhost

第四章:前端開發

4.1 專案結構

frontend/src/ 目錄下建立以下檔案結構:

frontend/src/
├── components/
│   ├── ConnectWallet.jsx    # 錢包連接元件
│   ├── MessageBoard.jsx     # 留言板元件
│   └── MessageForm.jsx      # 發布留言表單
├── hooks/
│   └── useGuestbook.js      # 自定義鉤子
├── abi/
│   └── guestbook.json       # 合約 ABI
├── App.jsx                  # 主應用程式
├── main.jsx                 # 進入點
└── index.css                # 樣式

4.2 配置 Web3 提供者

修改 frontend/src/main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { http, createConfig } from 'wagmi'
import { mainnet, sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'

import App from './App'
import './index.css'

// 配置 Wagmi
const config = createConfig({
  chains: [mainnet, sepolia],
  connectors: [
    injected() // 瀏覽器錢包(如 MetaMask)
  ],
  transports: {
    [mainnet.id]: http(),
    [sepolia.id]: http(),
  },
})

const queryClient = new QueryClient()

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </WagmiProvider>
  </React.StrictMode>,
)

4.3 建立 ABI 檔案

將編譯後的 ABI 複製到 frontend/src/abi/guestbook.json

[
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "author",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "string",
        "name": "content",
        "type": "string"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "timestamp",
        "type": "uint256"
      }
    ],
    "name": "NewMessage",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "string",
        "name": "_content",
        "type": "string"
      }
    ],
    "name": "postMessage",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "getMessageCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "_start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "_limit",
        "type": "uint256"
      }
    ],
    "name": "getMessages",
    "outputs": [
      {
        "components": [
          {
            "internalType": "address",
            "name": "author",
            "type": "address"
          },
          {
            "internalType": "string",
            "name": "content",
            "type": "string"
          },
          {
            "internalType": "uint256",
            "name": "timestamp",
            "type": "uint256"
          }
        ],
        "internalType": "struct Guestbook.Message[]",
        "name": "",
        "type": "tuple[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

4.4 建立自定義 Hook

建立 frontend/src/hooks/useGuestbook.js

import { useState, useEffect } from 'react'
import { useReadContract, useWriteContract, useAccount } from 'wagmi'
import { ethers } from 'ethers'
import guestbookABI from '../abi/guestbook.json'

const CONTRACT_ADDRESS = '0x5FbDB2315678afecb367f032d93F642f64180aa3' // 本機部署地址

export function useGuestbook() {
  const { address, isConnected } = useAccount()
  const [messages, setMessages] = useState([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState(null)

  // 讀取合約:取得留言數量
  const { data: messageCount } = useReadContract({
    address: CONTRACT_ADDRESS,
    abi: guestbookABI,
    functionName: 'getMessageCount',
  })

  // 讀取合約:取得留言列表
  const { refetch: fetchMessages } = useReadContract({
    address: CONTRACT_ADDRESS,
    abi: guestbookABI,
    functionName: 'getMessages',
    args: [0n, 50n],
    query: {
      enabled: !!messageCount,
      onSuccess: (data) => {
        setMessages(data || [])
      }
    }
  })

  // 寫入合約:發布留言
  const { writeContract: postMessage, isPending: isPosting } = useWriteContract()

  const publishMessage = async (content) => {
    if (!isConnected) {
      setError('請先連接錢包')
      return
    }

    setIsLoading(true)
    setError(null)

    try {
      await postContract({
        address: CONTRACT_ADDRESS,
        abi: guestbookABI,
        functionName: 'postMessage',
        args: [content],
      })
      
      // 重新整理留言列表
      await fetchMessages()
    } catch (err) {
      setError(err.message || '發布失敗')
    } finally {
      setIsLoading(false)
    }
  }

  return {
    messages,
    messageCount: messageCount ? Number(messageCount) : 0,
    isLoading,
    isPosting,
    error,
    publishMessage,
    refreshMessages: fetchMessages,
  }
}

// 輔助函數:格式化地址
export function formatAddress(address) {
  if (!address) return ''
  return `${address.slice(0, 6)}...${address.slice(-4)}`
}

// 輔助函數:格式化時間
export function formatTimestamp(timestamp) {
  if (!timestamp) return ''
  const date = new Date(Number(timestamp) * 1000)
  return date.toLocaleString('zh-TW')
}

4.5 建立錢包連接元件

建立 frontend/src/components/ConnectWallet.jsx

import { useAccount, useConnect, useDisconnect } from 'wagmi'
import { injected } from 'wagmi/connectors'

export function ConnectWallet() {
  const { address, isConnected } = useAccount()
  const { connect } = useConnect({
    connector: injected(),
  })
  const { disconnect } = useDisconnect()

  if (isConnected) {
    return (
      <div className="wallet-info">
        <span className="wallet-address">
          {address.slice(0, 6)}...{address.slice(-4)}
        </span>
        <button onClick={() => disconnect()} className="disconnect-btn">
          斷開連接
        </button>
      </div>
    )
  }

  return (
    <button onClick={() => connect()} className="connect-btn">
      連接錢包
    </button>
  )
}

4.6 建立留言表單元件

建立 frontend/src/components/MessageForm.jsx

import { useState } from 'react'
import { useAccount } from 'wagmi'

export function MessageForm({ onSubmit, isPosting }) {
  const [content, setContent] = useState('')
  const { isConnected } = useAccount()

  const handleSubmit = (e) => {
    e.preventDefault()
    if (!content.trim()) return
    onSubmit(content)
    setContent('')
  }

  return (
    <form onSubmit={handleSubmit} className="message-form">
      <textarea
        value={content}
        onChange={(e) => setContent(e.target.value)}
        placeholder={isConnected ? "寫下你的留言..." : "請先連接錢包"}
        disabled={!isConnected || isPosting}
        maxLength={500}
      />
      <div className="form-footer">
        <span className="char-count">{content.length}/500</span>
        <button 
          type="submit" 
          disabled={!isConnected || !content.trim() || isPosting}
          className="submit-btn"
        >
          {isPosting ? '發布中...' : '發布留言'}
        </button>
      </div>
    </form>
  )
}

4.7 建立留言板元件

建立 frontend/src/components/MessageBoard.jsx

import { useGuestbook, formatAddress, formatTimestamp } from '../hooks/useGuestbook'

export function MessageBoard() {
  const { messages, messageCount, isLoading, refreshMessages } = useGuestbook()

  if (isLoading) {
    return <div className="loading">載入中...</div>
  }

  return (
    <div className="message-board">
      <div className="board-header">
        <h2>留言板</h2>
        <span className="message-count">共 {messageCount} 條留言</span>
      </div>

      {messages.length === 0 ? (
        <div className="empty-state">
          尚無留言,成為第一個發布留言的人吧!
        </div>
      ) : (
        <div className="messages-list">
          {messages.slice().reverse().map((message, index) => (
            <div key={index} className="message-item">
              <div className="message-header">
                <span className="message-author">{formatAddress(message.author)}</span>
                <span className="message-time">{formatTimestamp(message.timestamp)}</span>
              </div>
              <div className="message-content">{message.content}</div>
            </div>
          ))}
        </div>
      )}

      <button onClick={() => refreshMessages()} className="refresh-btn">
        刷新
      </button>
    </div>
  )
}

4.8 主應用程式

修改 frontend/src/App.jsx

import { useAccount } from 'wagmi'
import { ConnectWallet } from './components/ConnectWallet'
import { MessageForm } from './components/MessageForm'
import { MessageBoard } from './components/MessageBoard'
import { useGuestbook } from './hooks/useGuestbook'

function App() {
  const { isConnected } = useAccount()
  const { publishMessage, isPosting, error } = useGuestbook()

  return (
    <div className="app">
      <header className="header">
        <h1>🌐 以太坊留言板</h1>
        <ConnectWallet />
      </header>

      <main className="main">
        <section className="form-section">
          <MessageForm onSubmit={publishMessage} isPosting={isPosting} />
          {error && <div className="error-message">{error}</div>}
        </section>

        <section className="board-section">
          <MessageBoard />
        </section>
      </main>

      <footer className="footer">
        <p>這是一個部署在以太坊區塊鏈上的去中心化應用</p>
        <p>所有留言都會被永久記錄在區塊鏈上</p>
      </footer>
    </div>
  )
}

export default App

4.9 添加樣式

修改 frontend/src/index.css

:root {
  --primary: #627eea;
  --background: #f5f5f5;
  --card-bg: #ffffff;
  --text: #333333;
  --text-secondary: #666666;
  --border: #e0e0e0;
}

* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--background);
  color: var(--text);
  line-height: 1.6;
}

.app {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 0;
  border-bottom: 1px solid var(--border);
}

.header h1 {
  font-size: 1.5rem;
  color: var(--primary);
}

.connect-btn, .disconnect-btn {
  padding: 10px 20px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.2s;
}

.connect-btn {
  background: var(--primary);
  color: white;
}

.disconnect-btn {
  background: #f0f0f0;
  color: #666;
}

.main {
  padding: 30px 0;
}

.form-section {
  margin-bottom: 30px;
}

.message-form {
  background: var(--card-bg);
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.message-form textarea {
  width: 100%;
  min-height: 100px;
  padding: 15px;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-size: 1rem;
  resize: vertical;
}

.form-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 15px;
}

.char-count {
  color: var(--text-secondary);
  font-size: 0.875rem;
}

.submit-btn {
  padding: 10px 24px;
  background: var(--primary);
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}

.submit-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.error-message {
  margin-top: 10px;
  padding: 10px;
  background: #fee;
  color: #c00;
  border-radius: 8px;
}

.message-board {
  background: var(--card-bg);
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.board-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
  padding-bottom: 15px;
  border-bottom: 1px solid var(--border);
}

.message-count {
  color: var(--text-secondary);
}

.empty-state {
  text-align: center;
  padding: 40px;
  color: var(--text-secondary);
}

.messages-list {
  display: flex;
  flex-direction: column;
  gap: 15px;
}

.message-item {
  padding: 15px;
  background: #f9f9f9;
  border-radius: 8px;
  border-left: 4px solid var(--primary);
}

.message-header {
  display: flex;
  justify-content: space-between;
  margin-bottom: 8px;
  font-size: 0.875rem;
}

.message-author {
  color: var(--primary);
  font-weight: 600;
}

.message-time {
  color: var(--text-secondary);
}

.message-content {
  white-space: pre-wrap;
  word-break: break-word;
}

.refresh-btn {
  margin-top: 20px;
  width: 100%;
  padding: 10px;
  background: #f0f0f0;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

.footer {
  text-align: center;
  padding: 30px 0;
  color: var(--text-secondary);
  font-size: 0.875rem;
}

第五章:部署上線

5.1 部署到 Sepolia 測試網路

首先,需要設定 Hardhat 設定檔以支援 Sepolia 網路:

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.19",
  networks: {
    sepolia: {
      url: process.env.SEPOLIA_RPC_URL,
      accounts: [process.env.PRIVATE_KEY],
    },
  },
};

建立 .env 檔案(請勿提交到 Git):

SEPOLIA_RPC_URL=your_sepolia_rpc_url_here
PRIVATE_KEY=your_wallet_private_key_here

取得 Sepolia 測試 ETH

  1. 前往 https://faucet.sepolia.dev/ 或其他水龍頭
  2. 輸入錢包地址領取測試 ETH

部署到 Sepolia

npx hardhat run scripts/deploy.js --network sepolia

部署成功後,會得到合約地址。將其更新到前端的 CONTRACT_ADDRESS 常數中。

5.2 建置前端

cd frontend
npm run build

5.3 部署到 Vercel

# 安裝 Vercel CLI
npm install -g vercel

# 部署
vercel

按照指示完成部署,幾分鐘後就可以透過 Vercel 提供的網址訪問你的 DApp。


第六章:開發安全須知

6.1 智慧合約安全檢查清單

在部署到主網之前,請確保

  1. [ ] 合約通過完整測試覆蓋
  2. [ ] 使用知名審計服務審計(如有)
  3. [ ] 設定合理的 Gas 限制
  4. [ ] 考慮升級機制(可選)
  5. [ ] 設定 Emergency Withdrawal 功能(可選)

6.2 常見智慧合約漏洞

重入攻擊(Reentrancy)

// 不安全的寫法
function withdraw() external {
    (bool success, ) = msg.sender.call{value: balance}("");
    require(success);
    balance[msg.sender] = 0;
}

// 安全的寫法(Checks-Effects-Interactions)
function withdraw() external {
    uint256 amount = balance[msg.sender];
    require(amount > 0, "No balance");
    balance[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: amount}("");
    require(success);
}

整數溢位

Solidity 0.8+ 內建整數溢位檢查,但仍需注意。

6.3 錢包安全


結論

恭喜你!已經完成了第一個以太坊 DApp 的開發。這個留言板專案雖然簡單,但涵蓋了 DApp 開發的核心概念和流程:

  1. 智慧合約開發:使用 Solidity 編寫區塊鏈邏輯
  2. 本機測試:使用 Hardhat 進行開發和測試
  3. 前端整合:使用 Ethers.js/Wagmi 與區塊鏈交互
  4. 部署上線:將應用部署到測試網路和正式環境

這只是開始!以太坊生態系統還有更多值得探索的領域:

建議下一步學習方向:


參考資源

  1. Ethereum Foundation - 官方文檔
  2. Hardhat - 以太坊開發環境
  3. Solidity - 智慧合約語言
  4. Wagmi - React Hooks 庫
  5. OpenZeppelin - 智能合約庫

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!