“Web3 全栈开发”听起来神秘,实际上可分为三大件:智能合约、前端页面、链上数据索引服务。本文将手把手带你完成一次完整的 DApp 落地:从合约编写、前端签名,到 The Graph 加速查询,并穿插常见坑位与优化技巧,助你迅速跑通开发、测试、上线整个流程。
区块链、钱包与 DApp 的三角关系
在进入代码之前,先理清 链—钱包—合约—界面 四条龙脉:
- 区块链节点:全网同步的“不可篡改数据库”,提供原生 RPC;
- 加密钱包:存放私钥,每一笔写操作都需用户签名;
- 智能合约:链上脚本,用于业务逻辑与资产转移;
- DApp 前端:面向普通用户的可视化入口,通过钱包插件与合约交互。
从零到合约:Solidity 开写
我们以 Polygon 测试网 Mumbai 为例,实现一个 “NFT 卡包” 合约:任何地址都能一次性 mint 一张独一无二的 SVG 图像卡牌。
初始化 Hardhat 项目
在终端依次执行:
mkdir web3stack && cd web3stack
npm init -y
npm install --save-dev hardhat
npx hardhat # 选择 JavaScript project
npm install --save @openzeppelin/contracts编写 ERC-721 合约
contracts/Card.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract Card is ERC721 {
uint256 private _nextTokenId = 1;
constructor() ERC721("Web3Card", "W3C") {}
function mint() external returns (uint256 tokenId) {
tokenId = _nextTokenId++;
_safeMint(msg.sender, tokenId);
}
function getImage(uint256 tokenId) external view returns (string memory) {
require(_exists(tokenId), "Token not exist");
// 返回示例 SVG
return string(
abi.encodePacked(
"data:image/svg+xml;base64,",
"PHN2ZyB3aWR0aD0iMjAwIiBoZWlnaHQ9IjIwMCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiIGZpbGw9ImxpZ2h0c2VhZ3JlZW4iLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1zaXplPSIyMCIKZm9udC1mYW1pbHk9IkFyaWFsIj5XZWIzIENhcmQgIzxiPk1pbnQgTm93PC9iPjwvdGV4dD48L3N2Zz4K"
)
);
}
}本地编译
npx hardhat compile部署脚本
scripts/deploy-card.js
const hre = require("hardhat");
async function main() {
const Card = await hre.ethers.getContractFactory("Card");
const card = await Card.deploy();
await card.deployed();
console.log("Contract deployed address:", card.address);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});连接到 Mumbai 测试网
在 hardhat.config.js 增加字段:
module.exports = {
solidity: "0.8.17",
networks: {
testnet: {
url: "https://matic-mumbai.chainstacklabs.com",
accounts: [process.env.PRIVATE_KEY], // 写入私钥前务必加密
},
},
};申请测试 MATIC:进入 「Polygon官方水龙头」即可领取。
部署:
PRIVATE_KEY=0x**** npx hardhat run scripts/deploy-card.js --network testnet记住终端会打印出的合约地址(如 0x8131…1ab0)。
前端:极简 HTML + Ethers.js
新建 index.html:
- 引入 CDN:Bootstrap 5、Ethers.js 5.7
- 用
window.ethereum检测并链接 MetaMask - 通过 ABI 构造合约实例
核心交互代码:
<script type="module">
import { ethers } from "https://cdn-esm.skypack.dev/[email protected]";
const CONTRACT_ADDRESS = "0x8131aa1B766966f9F8ec3E1132D9d29D92311AB0";
const NFT_ABI = [ /* 复制上一步 hardhat 生成的 ABI */ ];
// 连接钱包
async function connectWallet() {
await window.ethereum.request({ method: "eth_requestAccounts" });
document.getElementById("wallet").innerText = "已连接";
}
// 造卡
async function mintCard() {
const provider = new ethers.providers.Web3Provider(window.ethereum);
const signer = provider.getSigner();
const contract = new ethers.Contract(CONTRACT_ADDRESS, NFT_ABI, signer);
const tx = await contract.mint();
await tx.wait(1);
alert("NFT 铸造成功!");
}
window.connectWallet = connectWallet;
window.mintCard = mintCard;
</script>👉 查看更多 JavaScript 调用合约最佳实践,怕你漏掉细节
数据索引:链上 → 查询利器 The Graph
节点返回的是“事件日志”,平铺,无聚合;想实现“分页 & 筛选”级别的查询,链上要配合 索引器。
创建 Subgraph
npm install -g @graphprotocol/graph-cli
graph init --product hosted-service yourGithub/web3stack
# 交互填写:
# 合约地址:0x8131…1ab0,起始 block:在 PolygonScan 找到的块高定制 schema.graphql
删掉默认模板,改为:
type Card @entity(immutable: false) {
id: ID!
owner: Bytes!
image: String!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}Event 处理器 src/card.ts
import { Card } from "../../generated/Card/Card";
import { Transfer as TransferEvent } from "../../generated/Card/Card";
import { Card as CardEntity } from "../../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let tokenId = event.params.tokenId.toString();
let contract = Card.bind(event.address);
let nft = CardEntity.load(tokenId);
if (!nft) {
nft = new CardEntity(tokenId);
nft.transactionHash = event.transaction.hash;
}
nft.owner = event.params.to;
nft.image = contract.getImage(event.params.tokenId);
nft.blockNumber = event.block.number;
nft.blockTimestamp = event.block.timestamp;
nft.save();
}部署 & 查询
cd subgraph/web3stack
npm run codegen && npm run deployGraph Playground 立即可用:
{
cards(first: 20, orderBy:blockTimestamp, orderDirection:desc) {
id
owner
image
}
}前端调用只需:
const res = await fetch('https://api.thegraph.com/subgraphs/name/yourGithub/web3stack', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});一键上线:GitHub Pages + 自定义域名
- 把
index.html及静态资源推送到 GitHub 仓库; - 在仓库设置开启 GitHub Pages;
- 等待 DNS 生效,或者绑定自定义域名。
FAQ:开发途中的高频疑问
Q1:Gas 费太高,有没有更好方案?
A:选择 Layer2 或侧链(Polygon、Arbitrum、Base)。测试网完全免费,主网也能省 90% 以上 Gas。
Q2:如何支持多钱包?
A:引入 web3modal 库,一行配置即可接入 Coinbase Wallet、Trust Wallet 等,体验无缝切换。
Q3:合约需要升级怎么办?
A:使用 OpenZeppelin Upgrades 插件,合约逻辑与存储分离,保留数据的同时实现升级。
Q4:前端多链部署如何管理配置?
A:建立 configs/chains.json 文件,根据 chainId 动态切换合约地址、RPC 与浏览器链接。
Q5:Modal 弹窗提示 401Unauthorized?
A:检查 PRIVATE_KEY 环境变量写入方式,建议使用 dotenv 加 .env 护钥,切勿明文推送。
Q6:The Graph 查询速率慢?
A:项目冷启动需若干区块同步,完成后延迟<1 秒;或切换到自建索引节点以进一步提速。
小结:轻装上阵的三件套
- 智能合约:Solidity + Hardhat,一次写完、万物可跑
- 数据索引:The Graph,不写数据库却拥有 SQL 级查询
- 前端交互:一行 CDN 就连接 MetaMask,拖拽式开发模式同样适用
Web3 世界里,玩法只有想不到,没有做不到。从 OpenSea、Uniswap 到 Lens,核心仍是这三大件。带着本文代码,“搭积木”般的开发体验就在指尖。
祝你顺利完成第一张 NFT 的「链上签证」,也欢迎在 GitHub Issue 交流实战心得!