Web3 全栈开发入门与实战

·

“Web3 全栈开发”听起来神秘,实际上可分为三大件:智能合约、前端页面、链上数据索引服务。本文将手把手带你完成一次完整的 DApp 落地:从合约编写、前端签名,到 The Graph 加速查询,并穿插常见坑位与优化技巧,助你迅速跑通开发、测试、上线整个流程。


区块链、钱包与 DApp 的三角关系

在进入代码之前,先理清 链—钱包—合约—界面 四条龙脉:

  1. 区块链节点:全网同步的“不可篡改数据库”,提供原生 RPC;
  2. 加密钱包:存放私钥,每一笔写操作都需用户签名;
  3. 智能合约:链上脚本,用于业务逻辑与资产转移;
  4. 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

  1. 引入 CDN:Bootstrap 5、Ethers.js 5.7
  2. window.ethereum 检测并链接 MetaMask
  3. 通过 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 deploy

Graph 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 + 自定义域名

  1. index.html 及静态资源推送到 GitHub 仓库;
  2. 在仓库设置开启 GitHub Pages;
  3. 等待 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 秒;或切换到自建索引节点以进一步提速。


小结:轻装上阵的三件套

Web3 世界里,玩法只有想不到,没有做不到。从 OpenSea、Uniswap 到 Lens,核心仍是这三大件。带着本文代码,“搭积木”般的开发体验就在指尖。

祝你顺利完成第一张 NFT 的「链上签证」,也欢迎在 GitHub Issue 交流实战心得!