⚙️ 第 4 章:使用 ethers.js 与智能合约交互
🧩 4.1 前端和合约是如何"对话"的?
想象你在写一个普通网站时:
- 你用 axios 请求 REST API;
- API 返回 JSON 数据。
而在 Web3 世界:
- "服务器" = 智能合约;
- "axios" = ethers.js;
- "接口调用" = 发送交易到区块链。
🧠 类比图
css
graph LR
A[React 前端] -->|调用| B[ethers.js]
B -->|发送交易| C[区块链节点 Provider]
C -->|执行| D[智能合约 Contract]
D -->|返回结果| A
🧰 4.2 什么是 ABI?
ABI(Application Binary Interface) 就是合约的"接口说明书"。
例如,你部署了一个合约:
csharp
contract Counter {
uint256 public count;
function increment() public {
count += 1;
}
function getCount() public view returns(uint256) {
return count;
}
}
编译后会生成一份 ABI 文件,告诉前端:
css
[ { "inputs": [], "name": "increment", "outputs": [], "stateMutability": "nonpayable", "type": "function" },
{ "inputs": [], "name": "getCount", "outputs": [{ "type": "uint256" }], "stateMutability": "view", "type": "function" }
]
这相当于告诉前端:
「我有两个函数,一个能+1,一个能读取 count。」
🧱 4.3 构建 ethers.js 合约对象
ini
import { ethers } from "ethers";
const contractAddress = "0x1234..."; // 你的合约地址
const abi = [ ... ]; // 上面的 ABI
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(contractAddress, abi, provider);
这一步等价于:
"我拿到了一张通往智能合约的地图。"
📖 4.4 调用只读函数(read)
vbscript
const count = await contract.getCount();
console.log("当前计数:", count.toString());
- 不需要钱包签名;
- 不需要支付 Gas;
- 只查询链上数据。
✍️ 4.5 调用写入函数(write)
ini
const signer = await provider.getSigner();
const contractWithSigner = contract.connect(signer);
const tx = await contractWithSigner.increment();
console.log("交易已发出:", tx.hash);
await tx.wait(); // 等待上链
console.log("✅ 交易确认成功");
📘 写入函数与传统 API 的不同:
- 必须用钱包签名;
- 每笔交易都要支付 Gas;
- 交易需要等待确认(几秒到几十秒)。
⚙️ 交易流程图
rust
sequenceDiagram
用户 ->> 前端: 点击"+1 按钮"
前端 ->> MetaMask: 请求签名
MetaMask ->> 用户: 弹出确认窗口
用户 ->> MetaMask: 同意
MetaMask ->> 区块链: 发送交易
区块链 ->> 前端: 返回交易哈希
前端 ->> 用户: 显示"等待确认中"
区块链 ->> 前端: 确认交易成功
🧠 4.6 监听事件(Event)
智能合约可以发出事件,前端可以实时监听。
csharp
event Incremented(uint256 newCount);
function increment() public {
count += 1;
emit Incremented(count);
}
前端监听:
vbscript
contract.on("Incremented", (newCount) => {
console.log("计数已增加到:", newCount.toString());
});
这就像监听数据库"更新事件"。
✅ 4.7 小结
操作类型 | 是否签名 | 是否消耗Gas | 示例 |
---|---|---|---|
read-only | ❌ 否 | ❌ 否 | getCount() |
write | ✅ 是 | ✅ 是 | increment() |
💰 第 5 章:钱包签名与交易安全
🔐 5.1 钱包签名是什么?
签名(Signature)是用私钥生成的"加密证明"。
📘 类比理解:
你给别人发一封电子信(交易),钱包会帮你签上"数字签名",
任何人都能验证签名真伪,但没人能伪造。
🧩 签名原理图
css
graph LR
A[私钥] -->|签名| B[消息]
B -->|生成| C[数字签名]
C -->|验证| D[公钥地址]
✉️ 5.2 签名消息(前端登录)
登录示例(前端签名验证身份):
ini
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const message = "登录验证:" + new Date().toISOString();
const signature = await signer.signMessage(message);
console.log("签名结果:", signature);
这类签名常用于 DApp 登录(例如 OpenSea、Lens)。
🧱 5.3 验证签名(后端或链上)
ini
const recoveredAddress = ethers.verifyMessage(message, signature);
console.log("签名者:", recoveredAddress);
如果 recoveredAddress === 用户钱包地址
,说明签名有效。
🔄 5.4 交易生命周期(完整可视化)
rust
sequenceDiagram
用户 ->> 前端: 点击按钮(例如转账)
前端 ->> 钱包: 构建交易请求
钱包 ->> 用户: 弹出确认窗口
用户 ->> 钱包: 确认
钱包 ->> 区块链: 广播交易
区块链 ->> 矿工: 验证 & 打包
矿工 ->> 区块链: 写入区块
区块链 ->> 前端: 返回交易哈希
前端 ->> 用户: 显示"成功"
⚠️ 5.5 常见错误与解决方案
错误信息 | 原因 | 解决 |
---|---|---|
"insufficient funds" | 钱包内无 ETH | 去测试网水龙头领取 |
"user rejected transaction" | 用户取消签名 | 引导重新确认 |
"gas estimation failed" | 调用失败 | 检查函数参数或权限 |
🧠 第 6 章:构建一个完整的前端 DApp(计数器)
🧩 6.1 我们要实现的功能
- 连接钱包
- 显示当前计数
- 点击按钮自动 +1
- 实时更新界面
🖼️ 效果示意图
css
graph TD
A[按钮:+1] --> B[调用 increment()]
B --> C[MetaMask 确认]
C --> D[交易上链]
D --> E[链上数据更新]
E --> F[前端实时显示 count]
💻 6.2 React 前端完整代码
javascript
import { useState, useEffect } from "react";
import { ethers } from "ethers";
const ABI = [
"function getCount() view returns(uint256)",
"function increment()"
];
const CONTRACT_ADDRESS = "0xYourContractAddress";
export default function CounterApp() {
const [count, setCount] = useState(null);
const [account, setAccount] = useState(null);
const [loading, setLoading] = useState(false);
async function connectWallet() {
const provider = new ethers.BrowserProvider(window.ethereum);
const [addr] = await provider.send("eth_requestAccounts", []);
setAccount(addr);
await fetchCount();
}
async function fetchCount() {
const provider = new ethers.BrowserProvider(window.ethereum);
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, provider);
const value = await contract.getCount();
setCount(Number(value));
}
async function increment() {
setLoading(true);
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new ethers.Contract(CONTRACT_ADDRESS, ABI, signer);
const tx = await contract.increment();
await tx.wait();
await fetchCount();
setLoading(false);
}
return (
<div style={{ padding: 40 }}>
<h2>🧮 Web3 计数器 DApp</h2>
{!account && <button onClick={connectWallet}>连接钱包</button>}
{account && (
<>
<p>当前账户:{account}</p>
<p>当前计数:{count}</p>
<button onClick={increment} disabled={loading}>
{loading ? "交易进行中..." : "+1"}
</button>
</>
)}
</div>
);
}
🧠 6.3 合约代码(Solidity)
csharp
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Counter {
uint256 public count;
function increment() public {
count += 1;
}
function getCount() public view returns (uint256) {
return count;
}
}
部署后替换 CONTRACT_ADDRESS
即可。
🧩 6.4 实现后的完整流程
rust
sequenceDiagram
用户 ->> DApp: 打开页面
DApp ->> 钱包: 连接账户
钱包 ->> DApp: 返回地址
用户 ->> DApp: 点击 +1
DApp ->> 钱包: 请求签名交易
钱包 ->> 用户: 弹窗确认
用户 ->> 钱包: 同意
钱包 ->> 区块链: 发送交易
区块链 ->> DApp: 返回确认
DApp ->> 用户: 更新计数
🧾 6.5 已经学会了什么
✅ 与钱包交互
✅ 调用合约的读写方法
✅ 等待交易确认
✅ 实现一个真实的 Web3 前端 DApp
太棒了 🙌
接下来我们进入 Web3 前端全图解实战教程(进阶篇)第 7~10 章 。
这是从"能跑 DApp"到"能做完整项目"的关键阶段。
你将学到:
- ERC20 代币读写与转账
- NFT 铸造(Mint)实战
- 钱包登录验证(签名身份)
- 网络与链切换(Mainnet/Testnet/自定义链)
🪙 第 7 章:与 ERC20 代币交互
🎯 7.1 ERC20 是什么?
ERC20 是以太坊上最常见的代币标准。
定义了一组通用接口,让所有钱包、DApp 都能识别并交互。
常见函数:
方法 | 作用 |
---|---|
totalSupply() |
代币总量 |
balanceOf(address) |
查询余额 |
transfer(to, amount) |
转账 |
approve(spender, amount) |
授权第三方支出 |
transferFrom(from, to, amount) |
从授权账户转账 |
📘 ERC20 概念图
css
graph LR
A[用户钱包] -->|transfer| B[接收方]
A -->|approve| C[智能合约]
C -->|transferFrom| B
🧱 7.2 ERC20 示例合约(简化版)
ini
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyToken {
string public name = "DemoToken";
string public symbol = "DMT";
uint8 public decimals = 18;
uint256 public totalSupply = 1000000 * 10 ** uint256(decimals);
mapping(address => uint256) public balanceOf;
constructor() {
balanceOf[msg.sender] = totalSupply;
}
function transfer(address to, uint256 amount) public returns (bool) {
require(balanceOf[msg.sender] >= amount, "余额不足");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}
💻 7.3 前端查询余额与转账
ini
import { ethers } from "ethers";
const ERC20_ABI = [
"function name() view returns (string)",
"function symbol() view returns (string)",
"function balanceOf(address) view returns (uint)",
"function transfer(address to, uint amount)"
];
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const token = new ethers.Contract("0xTokenAddress", ERC20_ABI, signer);
const name = await token.name();
const balance = await token.balanceOf(await signer.getAddress());
console.log(`${name} 余额:`, ethers.formatUnits(balance, 18));
const tx = await token.transfer("0xReceiver", ethers.parseUnits("1", 18));
await tx.wait();
console.log("✅ 转账完成");
💡 小贴士
- 所有 ERC20 都有相同接口;
- 只需换地址,就能与任何代币交互;
ethers.formatUnits()
用于格式化代币数(带小数)。
🔄 7.4 前端展示代币信息
javascript
function TokenBalance() {
const [balance, setBalance] = useState("0");
useEffect(() => {
async function fetchBalance() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const token = new ethers.Contract(TOKEN_ADDR, ERC20_ABI, provider);
const bal = await token.balanceOf(await signer.getAddress());
setBalance(ethers.formatUnits(bal, 18));
}
fetchBalance();
}, []);
return <p>你的代币余额:{balance} DMT</p>;
}
🖼️ 第 8 章:NFT(ERC721)铸造实战
🧠 8.1 NFT 是什么?
NFT(Non-Fungible Token)= "独一无二的数字物品"。
📘 类比:
ERC20 是"货币",NFT 是"收藏品"。
🧩 ERC721 交互结构图
css
graph TD
A[前端DApp] -->|mint| B[NFT合约]
B -->|生成唯一ID| C[TokenURI JSON]
C -->|包含| D[图片/元数据]
💎 8.2 合约示例(简化 NFT Mint)
typescript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
contract MyNFT is ERC721 {
uint256 public nextId = 1;
constructor() ERC721("MyNFT", "MNFT") {}
function mint() public {
_mint(msg.sender, nextId);
nextId++;
}
}
💻 8.3 前端 Mint 逻辑
ini
import { ethers } from "ethers";
const NFT_ABI = [
"function mint()",
"function nextId() view returns (uint256)"
];
const NFT_ADDRESS = "0xNFTAddress";
async function mintNFT() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const nft = new ethers.Contract(NFT_ADDRESS, NFT_ABI, signer);
const tx = await nft.mint();
await tx.wait();
console.log("✅ Mint 成功!");
}
🖼️ 前端页面效果图
css
graph LR
A[Mint 按钮] --> B[钱包签名确认]
B --> C[区块链生成 Token ID]
C --> D[用户获得新 NFT]
🧠 8.4 显示用户持有的 NFT
如果 NFT 绑定了 metadata URI:
json
{
"name": "MyNFT #1",
"image": "https://gateway.pinata.cloud/ipfs/xxx",
"description": "First NFT"
}
前端就可以读取并展示:
ini
const uri = await nft.tokenURI(1);
const metadata = await fetch(uri).then(res => res.json());
console.log(metadata.image);
然后用 <img src={metadata.image} />
展示。
🔑 第 9 章:钱包登录与签名验证
💬 9.1 为什么 DApp 不需要密码登录?
Web3 登录不靠用户名密码,而靠钱包地址 + 签名。
📘 原理:
前端生成一条随机消息 → 钱包签名 → 后端验证 → 证明你是钱包主人。
⚙️ 登录流程图
rust
sequenceDiagram
用户 ->> 前端: 点击"登录"
前端 ->> 钱包: 请求签名消息
钱包 ->> 用户: 弹窗确认
用户 ->> 钱包: 同意签名
钱包 ->> 前端: 返回签名结果
前端 ->> 后端: 发送签名 & 地址
后端 ->> 前端: 验证成功,返回 Token
🧱 9.2 前端签名消息
javascript
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const message = `登录验证,时间戳: ${Date.now()}`;
const signature = await signer.signMessage(message);
// 发送到后端验证
await fetch("/api/verify", {
method: "POST",
body: JSON.stringify({ address, message, signature })
});
🔍 9.3 后端验证签名(Node.js)
javascript
import { verifyMessage } from "ethers";
app.post("/verify", (req, res) => {
const { address, message, signature } = req.body;
const signer = verifyMessage(message, signature);
if (signer.toLowerCase() === address.toLowerCase()) {
res.json({ success: true });
} else {
res.status(401).json({ success: false });
}
});
✅ 9.4 登录结果
- 验证成功 → 颁发 JWT / session
- 验证失败 → 提示"签名无效"
这就是 Web3 登录的无密码体验!
🌐 第 10 章:网络切换与多链支持
🔄 10.1 为什么要支持多链?
Web3 应用可能运行在:
- Ethereum 主网(mainnet)
- 测试网(Sepolia、Holesky)
- 其他链(Polygon、BSC、Arbitrum)
🌍 多链架构图
css
graph TD
A[DApp前端] --> B[MetaMask Provider]
B -->|switchNetwork| C[不同链RPC]
C --> D[目标链智能合约]
🧱 10.2 切换网络示例
csharp
async function switchToSepolia() {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0xaa36a7" }] // Sepolia
});
}
🧩 10.3 添加自定义网络
php
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [{
chainId: "0x89", // Polygon
chainName: "Polygon Mainnet",
rpcUrls: ["https://polygon-rpc.com/"],
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
blockExplorerUrls: ["https://polygonscan.com/"]
}]
});
✅ 10.4 网络检测与监听
javascript
window.ethereum.on("chainChanged", (chainId) => {
console.log("网络切换为:", chainId);
window.location.reload();
});
🎓 结语:从前端到 Web3 工程师的跃迁
你现在已经掌握:
能力 | 说明 |
---|---|
⚙️ ethers.js 基础 | 与合约交互(读/写) |
🔐 钱包签名 | 交易签名、登录验证 |
💰 Token | ERC20 读写、转账 |
🖼️ NFT | ERC721 Mint、展示 |
🌐 多链支持 | 钱包网络切换 |
接下来建议的 进阶路线:
-
深入合约开发
学习 Solidity + Hardhat 测试与部署
官方教程:docs.ethers.org
Solidity 文档:docs.soliditylang.org
-
使用前端框架集成
- Next.js + Wagmi + RainbowKit
- 支持多钱包连接与自动链检测
-
真实项目实战
- Token Dashboard
- NFT Mint 平台
- Web3 登录门户
太好了 💪
我们现在进入整套《前端开发者的 Web3 全图解实战教程》第三部分:综合项目篇(第 11~15 章) 。
这是让你从"懂概念" → "能做项目" → "能上线"的阶段。
📘 目标:带你手把手完成三个可上线的 Web3 应用。
每章都有清晰架构图 + 合约 + 前端代码 + 讲解。
🚀 第 11 章:Token Dashboard(代币看板)
🎯 功能目标
构建一个代币仪表盘,支持:
- 连接钱包
- 显示代币余额(多个 ERC20)
- 一键转账
- 实时监听余额变化
🧩 系统架构图
css
graph TD
A[React DApp 前端] --> B[ethers.js]
B --> C[智能合约(ERC20)]
B --> D[钱包 Provider]
D --> E[区块链节点]
🧱 11.1 前端布局设计
xml
<div className="p-8 max-w-md mx-auto space-y-6">
<h2 className="text-2xl font-bold">💰 Token Dashboard</h2>
<button onClick={connectWallet}>连接钱包</button>
<div>
<h3>账户:{account}</h3>
<ul>
{tokens.map(t => (
<li key={t.symbol}>
{t.symbol}: {t.balance}
</li>
))}
</ul>
</div>
<input placeholder="目标地址" value={to} onChange={e => setTo(e.target.value)} />
<input placeholder="数量" value={amount} onChange={e => setAmount(e.target.value)} />
<button onClick={transfer}>转账</button>
</div>
🧩 11.2 查询多个代币余额
ini
const TOKENS = [
{ symbol: "USDT", address: "0xYourToken1" },
{ symbol: "DMT", address: "0xYourToken2" }
];
const ABI = ["function balanceOf(address) view returns(uint)"];
async function loadBalances() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const addr = await signer.getAddress();
const results = await Promise.all(
TOKENS.map(async (t) => {
const token = new ethers.Contract(t.address, ABI, provider);
const balance = await token.balanceOf(addr);
return { ...t, balance: ethers.formatUnits(balance, 18) };
})
);
setTokens(results);
}
🧩 11.3 实现代币转账
csharp
async function transfer() {
const token = new ethers.Contract(selectedToken.address, ABI_TRANSFER, signer);
const tx = await token.transfer(to, ethers.parseUnits(amount, 18));
await tx.wait();
alert("✅ 转账成功");
loadBalances();
}
🔄 实时监听余额变化
dart
window.ethereum.on("accountsChanged", loadBalances);
window.ethereum.on("chainChanged", () => window.location.reload());
🎨 效果预览图
css
graph TD
A[Token Dashboard]
A --> B[钱包地址]
A --> C[Token 列表]
A --> D[输入转账信息]
A --> E[MetaMask 确认]
🖼️ 第 12 章:NFT Mint 平台(图像上传 + 元数据存储)
🎯 功能目标
用户可以:
- 上传图片
- 填写 NFT 名称与描述
- 上传元数据到 IPFS(Pinata)
- 调用合约 mint()
🧩 系统架构图
css
graph TD
A[React前端] -->|upload| B[Pinata IPFS]
A -->|mint| C[NFT合约]
C --> D[区块链]
D --> E[钱包]
⚙️ 12.1 准备 Pinata API
- 注册 www.pinata.cloud/
- 获取
JWT Token
- 前端使用 axios 上传文件
💻 12.2 上传文件到 IPFS
javascript
import axios from "axios";
const PINATA_JWT = "Bearer <Your Pinata JWT>";
async function uploadToIPFS(file) {
const formData = new FormData();
formData.append("file", file);
const res = await axios.post("https://api.pinata.cloud/pinning/pinFileToIPFS", formData, {
headers: {
Authorization: PINATA_JWT,
"Content-Type": "multipart/form-data",
},
});
return `https://gateway.pinata.cloud/ipfs/${res.data.IpfsHash}`;
}
💾 12.3 上传 Metadata 并 Mint
ini
async function mintNFT(file, name, desc) {
const imageUrl = await uploadToIPFS(file);
const metadata = { name, description: desc, image: imageUrl };
const res = await axios.post("https://api.pinata.cloud/pinning/pinJSONToIPFS", metadata, {
headers: { Authorization: PINATA_JWT },
});
const uri = `https://gateway.pinata.cloud/ipfs/${res.data.IpfsHash}`;
const contract = new ethers.Contract(NFT_ADDRESS, ABI, signer);
const tx = await contract.mint(uri);
await tx.wait();
console.log("✅ NFT Mint 成功");
}
🖼️ 用户交互图
rust
sequenceDiagram
用户 ->> DApp: 上传图片
DApp ->> Pinata: 上传到 IPFS
Pinata ->> DApp: 返回 CID
DApp ->> 合约: 调用 mint(uri)
合约 ->> 区块链: 生成新 NFT
区块链 ->> 用户: NFT 铸造完成
🔐 第 13 章:Web3 登录门户(签名验证 + JWT)
🎯 功能目标
- 用户点击"登录钱包"
- 前端生成签名消息
- 后端验证签名
- 返回 JWT Token
🧩 13.1 前端签名登录
ini
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const nonce = Date.now();
const message = `Login verification for ${address}, nonce: ${nonce}`;
const signature = await signer.signMessage(message);
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ address, message, signature }),
});
const { token } = await res.json();
localStorage.setItem("jwt", token);
🧱 13.2 后端验证并生成 JWT
javascript
import jwt from "jsonwebtoken";
import { verifyMessage } from "ethers";
app.post("/api/login", (req, res) => {
const { address, message, signature } = req.body;
const signer = verifyMessage(message, signature);
if (signer.toLowerCase() === address.toLowerCase()) {
const token = jwt.sign({ address }, process.env.JWT_SECRET, { expiresIn: "1h" });
res.json({ token });
} else {
res.status(401).json({ error: "Invalid signature" });
}
});
🔒 登录流程图
css
graph TD
A[前端] --> B[钱包签名]
B --> C[后端验证]
C --> D[JWT 生成]
D --> E[返回 Token]
E --> A[用户已登录]
🌐 第 14 章:多链钱包支持与持久化
🎯 目标
- 自动检测网络
- 一键切换链
- 记住用户登录状态
⚙️ 14.1 使用 wagmi + RainbowKit
bash
npm install wagmi viem @rainbow-me/rainbowkit
javascript
import { WagmiConfig, createConfig, configureChains } from "wagmi";
import { sepolia, polygon } from "wagmi/chains";
import { RainbowKitProvider, getDefaultWallets } from "@rainbow-me/rainbowkit";
const { chains, publicClient } = configureChains([sepolia, polygon], []);
const { connectors } = getDefaultWallets({ appName: "Web3App", chains });
const config = createConfig({ connectors, publicClient });
export default function App() {
return (
<WagmiConfig config={config}>
<RainbowKitProvider chains={chains}>
<YourDapp />
</RainbowKitProvider>
</WagmiConfig>
);
}
🌍 14.2 自动记忆钱包连接状态
RainbowKit 默认会持久化用户钱包登录状态到 localStorage。
刷新后仍能保持连接。
☁️ 第 15 章:部署上线(Vercel + 公网链)
🎯 步骤总览
- 部署合约到 Sepolia 测试网
- 配置前端
.env
(合约地址、API) - 将 React/Next.js 应用上传至 Vercel
- 绑定域名,公开访问
⚙️ 15.1 部署合约
使用 Hardhat:
arduino
npx hardhat run scripts/deploy.js --network sepolia
返回地址:
yaml
Contract deployed at: 0x1234abcd...
🧩 15.2 配置前端环境变量
ini
VITE_CONTRACT_ADDRESS=0x1234abcd...
VITE_PINATA_JWT=Bearer xxxxxx
☁️ 15.3 部署到 Vercel
arduino
npm run build
vercel --prod
Vercel 会自动打包 React 项目生成一个在线访问链接。
📊 系统架构图(全流程)
css
graph TD
A[用户浏览器] --> B[前端 React DApp (Vercel)]
B --> C[钱包 MetaMask]
B --> D[区块链网络 (Sepolia / Polygon)]
B --> E[后端验证服务 (JWT 登录)]
E --> F[IPFS Pinata 存储]
🎓 完成总结
你现在已经能独立构建并上线一个完整的 Web3 前端项目:
✅ Token 仪表盘
✅ NFT Mint 平台
✅ 钱包登录验证
✅ 多链切换
✅ Vercel 部署
接下来推荐: