智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端
📅 發布時間: 2025-01-29 🏷️ 標籤: Solidity, Web3.js, DApp, MetaMask, 前端開發, Sepolia
這篇文章繼承了上一篇智能合約 Solidity 教學 (中),我們開發一個 DApp 的前端,理解如何透過 Web3.js 連接區塊鏈上我們所部署的合約。
目錄
- 智能合約 Solidity 教學 (上) - Solidity 基礎語法教學
- 智能合約 Solidity 教學 (中) - MetaMask 與以太坊測試網
- 智能合約 Solidity 教學 (下) - Web3.js 實際開發一個 DApp 去中心化應用前端
開發 Web3 應用的前端
零、前言
在過去兩章節中,我們已經完成了智能合約,並且將其部署到測試鏈 Sepolia 上
Sepolia 是全球公開的以太坊測試網絡,已經算是個半公開環境了
在實際項目開發中,我們會使用 Ganache 之類的程序,來架設自己的私人區塊鏈,並且在上頭測試合約,以獲得更高的調測性與開發效率。
在本教學中則因為時間問題,省略了這一部分。
一、開發環境準備
- 安裝 Node.js 及 npm
- 如果你的電腦還沒有安裝 Node.js,請至 Node.js 官方網站下載並安裝最新版本的 Node.js,即可同時安裝 npm(Node Package Manager)。
- 安裝 Vite此時 Vite 會自動幫你建立一個基本的前端專案結構。
- 透過 npm 方式安裝 Vite(其實也可以直接使用
npm create vite@latest來初始化專案)。 - 這裡建議使用簡單的安裝方式快速開始:npm create vite@latest
- 在互動式的 CLI 中選擇:
- 專案名稱 (如
my-web3-project) - 框架選擇:Vanilla
- 選擇 JavaScript(非 TypeScript)
- 專案名稱 (如
- 透過 npm 方式安裝 Vite(其實也可以直接使用
- 進入專案並安裝依賴
- 切換到專案資料夾:
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 的位置
步驟 3: 取得合約地址
再來就是合約地址。你可以在我們剛剛 Deploy 好的合約中,點擊複製按鈕。

圖: 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 介面
步驟 24: 驗證合約變化
到 Remix 看看,你會發現數值也變了。

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