智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端

智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端
📅 發布時間: 2025-01-29 🏷️ 標籤: Solidity, Web3.js, DApp, MetaMask, 前端開發, Sepolia

這篇文章繼承了上一篇智能合約 Solidity 教學 (中),我們開發一個 DApp 的前端,理解如何透過 Web3.js 連接區塊鏈上我們所部署的合約。


目錄

開發 Web3 應用的前端

、前言

在過去兩章節中,我們已經完成了智能合約,並且將其部署到測試鏈 Sepolia 上

Sepolia 是全球公開的以太坊測試網絡,已經算是個半公開環境了

在實際項目開發中,我們會使用 Ganache 之類的程序,來架設自己的私人區塊鏈,並且在上頭測試合約,以獲得更高的調測性與開發效率。

在本教學中則因為時間問題,省略了這一部分。

一、開發環境準備

  1. 安裝 Node.js 及 npm
    • 如果你的電腦還沒有安裝 Node.js,請至 Node.js 官方網站下載並安裝最新版本的 Node.js,即可同時安裝 npm(Node Package Manager)。
  2. 安裝 Vite此時 Vite 會自動幫你建立一個基本的前端專案結構。
    • 透過 npm 方式安裝 Vite(其實也可以直接使用 npm create vite@latest 來初始化專案)。
    • 這裡建議使用簡單的安裝方式快速開始:npm create vite@latest
    • 在互動式的 CLI 中選擇:
      1. 專案名稱 (如 my-web3-project)
      2. 框架選擇:Vanilla
      3. 選擇 JavaScript(非 TypeScript)
  3. 進入專案並安裝依賴
    • 切換到專案資料夾:cd my-web3-project
    • 安裝 web3.js:npm install web3

二、專案結構簡介

執行完 npm create vite@latest 之後,你的專案結構大致會長這樣:

my-web3-project ├─ index.html ├─ package.json ├─ vite.config.js └─ src   ├─ main.js   └─ style.css

​為了方便教學,所有的程式碼都會放在 index.html 中來展示結果。


三、拿到合約的 ABI 與合約地址


💻 上次部署的 HelloWorld 合約資訊

🎯 學習目標:理解如何取得合約 ABI 和地址,以便前端與區塊鏈互動

步驟 1: 合約部署資訊

在上次的教學範例中,HelloWorld 合約已經部署在 Sepolia 網路上。我們仍然需要兩項關鍵資訊才能在前端與之互動

合約部署圖

圖: Sepolia 測試網上部署的 HelloWorld 合約

步驟 2: 取得 ABI

首先是合約 ABI (Application Binary Interface)。可以在 Remix 部署成功後,點開「Solidity Compiler」下方的「ABI」標籤,取得 ABI 資訊(是一段 JSON 格式)。

Remix ABI 圖

圖: Remix 中取得 ABI 的位置

步驟 3: 取得合約地址

再來就是合約地址。你可以在我們剛剛 Deploy 好的合約中,點擊複製按鈕。

Remix 地址圖

圖: Remix 中複製合約地址


四、撰寫前端程式碼


💻 建構 DApp 前端程式碼

🎯 學習目標:使用 Web3.js 連接 MetaMask讀取與寫入智能合約狀態處理錢包授權與交易發送

步驟 1: 基本 HTML 結構

打開 index.html,替換成最基本的 HTML,我們一步一步來建構 Web3 應用。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

步驟 2: 新增 script 模組

為了方便教學,我會將 JavaScript 直接寫在 HTML 中。在正式專案開發時,這樣做程式碼會長到很難閱讀,要特別注意。

// ... (前面的 HTML 結構保持不變)

<head>
    <!-- ... -->
    <script type="module">
        // 註解:使用 type="module" 允許 import ES 模組語法,讓我們能匯入 Web3.js 庫。這是現代 JavaScript 的標準方式,提高程式碼模組化與維護性
    </script>
</head>

// ... (後面的 body 保持不變)

步驟 3: 匯入 Web3.js

還記得我們剛剛安裝的 web3.js 嗎?這是別人已經設計、封裝好的程式碼,我們可以簡單的使用 import 來把它加入我們的程式碼。

// ... (前面的 script 保持不變)

<script type="module">
    // 註解:匯入 Web3 庫,讓我們能透過 JavaScript 與 Ethereum 區塊鏈互動,而無需從頭實現 RPC 呼叫等低階細節
    import Web3 from "web3";
</script>

// ... (其餘保持不變)

步驟 4: 新增合約地址與 ABI

將剛剛獲得的合約地址與合約 ABI 貼上去。這個程式碼會瞬間變超級長。

// ... (前面的 import 保持不變)

const contractAddress = "0xFEA4f2C9B99Df0B2b7da67e60371BE4275C3d749"; // 註解:這是部署在 Sepolia 的合約地址,讓 Web3.js 知道要連接哪個合約

const contractABI = [ // 註解:ABI 是合約的介面描述,告訴 Web3.js 合約有哪些函數、參數與事件,讓我們能安全呼叫而不會出錯
    {
        inputs: [
            ......
        ],
        stateMutability: "nonpayable",
        type: "constructor",
    },
    ...
];

步驟 5: 獨立 ABI 檔案

contractABI 真的太長了!!讓我們把它獨立成一個檔案吧。在 src 資料夾中創建新的檔案 abi.js 並貼上剛剛的 合約 ABI。記得在最前面加上 export,這表示允許其他程式檔案使用這個變數。

// abi.js
// 註解:獨立 ABI 檔案避免 HTML 過長,提高程式碼可讀性與模組化,方便未來重用或維護
export const contractABI = [
    {
        inputs: [
            ....
        ],
        stateMutability: "nonpayable",
        type: "constructor",
    },
    ...
];
專案結構圖

圖: src 資料夾新增 abi.js 檔案


步驟 6: 匯入 ABI

回到 index.html,透過 import 將剛剛宣告的變數導進來。

// ... (前面的 import Web3 保持不變)

// 新增:匯入 ABI
import { contractABI } from "./src/abi.js"; // 註解:從獨立檔案匯入 ABI,讓主程式碼更簡潔

const contractAddress = "0xFEA4f2C9B99Df0B2b7da67e60371BE4275C3d749";

步驟 7: 新增錢包連線按鈕

設計一個按鈕來連結用戶錢包。

// ... (script 保持不變)

<body>
    <div>
        // 新增:連線按鈕,讓用戶主動觸發 MetaMask 授權
        <button id="connectButton">連線錢包</button>
    </div>
</body>

步驟 8: 綁定連線事件

在 JS 中設定該按鈕點擊後要做什麼。

// ... (變數宣告保持不變)

const connectButton = document.getElementById("connectButton"); // 註解:取得 DOM 元素,讓我們能監聽用戶互動

connectButton.addEventListener("click", async () => { // 註解:使用 addEventListener 監聽點擊事件,async 允許內部使用 await 處理非同步操作如錢包請求
    // ...點擊後要做的事情
});

步驟 9: 檢查 MetaMask 安裝

點擊按鈕後,要先檢查瀏覽器是否安裝了 MetaMask(或其他注入 web3 的錢包)。(如果沒裝任何錢包 window.ethereum 不會有東西)。

// ... (事件監聽器內)

if (typeof window.ethereum !== 'undefined') { // 註解:檢查 window.ethereum 是否存在,這是 MetaMask 等錢包注入的全局物件,用來確認錢包環境
    // ... 設定錢包
} else {
    alert('請先安裝 MetaMask 或其他以太坊錢包外掛!');
}

步驟 10: 請求錢包授權

請求使用者透過 MetaMask 授權並連接錢包,讓 DApp 可以存取使用者的 Ethereum 帳戶地址。

// ... (try 區塊新增)

try {
    // 註解:eth_requestAccounts 是 EIP-1102 標準,用戶需手動確認授權,避免未經同意存取帳戶,提高安全性
    await window.ethereum.request({ method: 'eth_requestAccounts' });
} catch (error) { // 註解:try-catch 處理用戶拒絕授權或網路錯誤,提供使用者友善回饋
    console.error(error);
    alert("連線錢包失敗,請查看控制台訊息或再次嘗試。");
}

步驟 11: 初始化 Web3 實例

將 MetaMask 作為 Web3 的提供者 (Provider),讓 DApp 透過 MetaMask 與區塊鏈互動。

// ... (授權後新增)

let web3; // 註解:全域變數存放 Web3 實例,供後續合約操作使用,避免重複初始化

// 註解:new Web3(provider) 使用 MetaMask 作為 RPC 端點,讓我們透過它發送交易與查詢狀態
web3 = new Web3(window.ethereum);

步驟 12: 取得連線帳戶

可以印出來看看有沒有成功連結帳號。

// ... (Web3 初始化後新增)

// 註解:getAccounts() 取得授權帳戶列表,用來確認連線並後續指定交易發送者
const accounts = await web3.eth.getAccounts();
console.log('已連線帳戶:', accounts[0]);

步驟 13: 建立合約實例

因為你可能沒學過物件導向,所以讓我用更簡單的方式來解釋這段程式碼。你可以想像 Web3.js 會幫我們創建一個「合約操作員」,這個操作員負責幫我們與區塊鏈上的智能合約互動。這個合約操作員具備三個關鍵能力:擁有你的錢包地址的操控權、知道你要操作哪個智能合約、知道該怎麼操作這份合約。

let contractInstance; // 新增:存放合約實例變數

// ... (連線成功後新增)

// 註解:new web3.eth.Contract(ABI, address) 建立合約代理物件,讓我們能直接呼叫合約方法如 message() 或 setMessage()
contractInstance = new web3.eth.Contract(contractABI, contractAddress);

alert("錢包連線成功!");

步驟 14: 新增讀取介面

在 HTML 中顯示合約中的 message 變數。

// ... (連線按鈕 div 後新增)

<div>
    // 新增:顯示當前 message 與讀取按鈕,讓用戶查看合約狀態
    <p>目前合約訊息:<span id="currentMessage">---</span></p>
    <button id="readMessageButton">讀取合約訊息</button>
</div>

步驟 15: 綁定讀取事件

在 JS 中設定該按鈕點擊後要做什麼。讀取區塊鏈的資料前,要先連結錢包再進行操作。

// ... (連線事件後新增)

const readMessageButton = document.getElementById("readMessageButton");

readMessageButton.addEventListener("click", async () => {
    if (!contractInstance) { // 註解:檢查合約實例是否存在,避免未連線就操作,防止錯誤
        alert("請先按 [連線錢包] 按鈕,再進行操作。");
        return;
    }
});

步驟 16: 呼叫讀取方法

從智能合約中讀取 message 變數的值,而且不會發送交易,只會查詢區塊鏈上的數據。

// ... (讀取事件內 try 新增)

try {
    // 註解:.methods.message().call() 是純查詢 (view),不消耗 gas、不需簽名,直接從節點取得狀態,提高效率與成本低
    const message = await contractInstance.methods.message().call();
    console.log("合約訊息為:", message);
} catch (error) { // 註解:錯誤處理涵蓋網路問題或合約不存在等情況
    console.error(error);
    alert("讀取合約訊息失敗!");
}

步驟 17: 更新 UI 顯示

將從區塊鏈上取得的資料寫入網頁。

const currentMessageSpan = document.getElementById("currentMessage"); // 新增:取得 span 元素

// ... (讀取成功後新增)
currentMessageSpan.textContent = message; // 註解:動態更新 DOM,讓用戶即時看到合約狀態變化

步驟 18: 新增寫入介面

再來就是要來實踐如何觸發 setMessage 的 function 啦。先從 HTML 介面開始。

// ... (讀取 div 後新增)

<div>
    // 新增:輸入框與寫入按鈕,讓用戶修改合約狀態
    <input type="text" id="newMessageInput" placeholder="輸入新的訊息" />
    <button id="setMessageButton">寫入合約訊息</button>
</div>

步驟 19: 綁定寫入事件

處理 JS 的事件綁定與錢包檢查。

// ... (讀取事件後新增)

// 新增:取得寫入相關元素
const setMessageButton = document.getElementById("setMessageButton");
const newMessageInput = document.getElementById("newMessageInput");

setMessageButton.addEventListener("click", async () => {
    if (!contractInstance) {
        alert("請先按 [連線錢包] 按鈕,再進行操作。");
        return;
    }
    // ... 點擊按鈕並確認連線錢包後要做的事情
});

步驟 20: 驗證輸入

從輸入框取得當前輸入的數值,檢查一下有沒有輸入。

// ... (錢包檢查後新增)

const newMessage = newMessageInput.value; // 註解:取得用戶輸入,作為 setMessage 參數
if (!newMessage) { // 註解:前端驗證避免無效交易,節省 gas 與用戶體驗
    alert("請輸入新的訊息再嘗試。");
    return;
}

步驟 21: 取得發送帳戶

取得剛剛綁定好的錢包,由於用戶可能有多個錢包,使用首個錢包。

try {
    // 新增:指定交易發送帳戶
    // 註解:交易需指定 from 帳戶,用戶需簽名支付 gas fee,這是區塊鏈變更狀態的必要步驟
    const accounts = await web3.eth.getAccounts();
    const usedAccount = accounts[0];
    
} catch (error) {
    // ...
}

步驟 22: 發送寫入交易

正式發起 setMessage 的合約請求,並且以我剛剛選定好的錢包來當作付款對象。

// ... (帳戶取得後新增)

const receipt = await contractInstance.methods
    .setMessage(newMessage) // 註解:呼叫合約寫入方法,需簽名交易上鏈,會消耗 gas
    .send({ from: usedAccount }); // 註解:send() 發送交易,receipt 包含交易 hash、區塊號等確認資訊

console.log("交易回執:", receipt); // 註解:記錄交易結果,用於除錯與確認
alert("寫入訊息成功,請再度點擊 [讀取合約訊息] 查看更新。");

步驟 23: 測試運行

最後打開測試網站 http://localhost:5173/ 跑跑看吧。

DApp 運行圖

圖: 本地運行 DApp 介面

步驟 24: 驗證合約變化

到 Remix 看看,你會發現數值也變了。

Remix 更新圖

圖: Remix 中合約狀態已更新


💡 提示: 恭喜!你已完成一個完整的 DApp,能讀寫區塊鏈狀態。記得切換 MetaMask 到 Sepolia 測試網,並使用測試 ETH。