前端阅读 05月28日 02:25
Web3 前端如何实现 NFT 的展示与交易?
随着 NFT 市场从投机走向实用,前端开发者面临的核心挑战已经从"能不能连上链"变成了"怎么做出安全、流畅、可维护的 NFT 应用"。这道题考察的不仅是 ethers.js 的 API 调用,更是对钱包集成、合约交互、数据层设计和安全防护的整体理解。钱包连接:从 MetaMask 到现代连接方案连接钱包是所有 Web3 应用的入口。2026 年的主流做法已经不再直接操作 window.ethereum,而是使用 wagmi + viem 组合:// wagmi v2 配置import { createConfig, http } from 'wagmi'import { mainnet, polygon } from 'wagmi/chains'import { injected, walletConnect } from 'wagmi/connectors'export const config = createConfig({ chains: [mainnet, polygon], connectors: [ injected(), walletConnect({ projectId: 'YOUR_WC_PROJECT_ID' }), ], transports: { [mainnet.id]: http(), [polygon.id]: http(), },})直接使用 window.ethereum 的方式存在三个问题:无法处理多钱包切换、缺少自动重连机制、类型安全缺失。wagmi 通过 React Hook 封装解决了这些问题:import { useAccount, useConnect, useDisconnect } from 'wagmi'function WalletConnect() { const { address, isConnected } = useAccount() const { connect, connectors } = useConnect() const { disconnect } = useDisconnect() if (isConnected) { return ( <div> <p>{address}</p> <button onClick={() => disconnect()}>断开连接</button> </div> ) } return ( <div> {connectors.map((connector) => ( <button key={connector.uid} onClick={() => connect({ connector })}> 连接 {connector.name} </button> ))} </div> )}追问:用户切换了钱包账户或网络,应用状态如何同步? wagmi 的 useAccount 和 useChainId 会自动监听 accountsChanged 和 chainChanged 事件触发重渲染,无需手动绑定监听器。NFT 数据获取:直接调用 vs 索引服务获取用户持有的 NFT 列表,最直觉的方式是直接调用合约方法,但这里有个常见误区——balanceOf 返回的是持有数量,不是 tokenId 列表。正确做法是调用 tokenOfOwnerByIndex(ERC-721 Enumerable 扩展)或遍历 Transfer 事件:// 方式一:ERC-721 Enumerableasync function getUserNFTs(contractAddress: string, owner: string) { const contract = getContract(contractAddress) const balance = await contract.balanceOf(owner) const tokenIds = await Promise.all( Array.from({ length: Number(balance) }, (_, i) => contract.tokenOfOwnerByIndex(owner, i) ) ) return tokenIds}// 方式二:通过 Transfer 事件过滤(不依赖 Enumerable 扩展)async function getNFTsByEvents(contractAddress: string, owner: string) { const contract = getContract(contractAddress) const sentFilter = contract.filters.Transfer(owner, null) const receivedFilter = contract.filters.Transfer(null, owner) const [sentEvents, receivedEvents] = await Promise.all([ contract.queryFilter(sentFilter), contract.queryFilter(receivedFilter), ]) // 计算当前持有的 tokenId(收到减去发出) const owned = new Set<number>() receivedEvents.forEach((e) => owned.add(Number(e.args?.tokenId))) sentEvents.forEach((e) => owned.delete(Number(e.args?.tokenId))) return [...owned]}但当用户持有大量 NFT 或需要跨集合查询时,直接调用合约的 RPC 请求会非常多,页面加载极慢。这就是 The Graph 等索引服务存在的意义:# The Graph 子图查询query GetOwnerNFTs($owner: String!) { nfts(where: { owner: $owner }) { id tokenId tokenURI collection { name } }}追问:如果 NFT 元数据存储在 IPFS 上,前端如何高效加载? 使用 IPFS 网关(如 ipfs.io 或自建网关)将 ipfs://Qm... 转换为 HTTPS URL,配合 Pinata 等 CDN 服务加速。对于图片,可用 <img loading="lazy"> 和 Intersection Observer 实现懒加载,避免一次性请求几十张图片。NFT 展示渲染:组件化与性能展示层的关键是组件化设计和加载状态管理:interface NFTMetadata { name: string description: string image: string attributes: { trait_type: string; value: string }[]}function NFTCard({ nft }: { nft: NFTMetadata }) { return ( <div className="nft-card"> <img src={nft.image.replace('ipfs://', 'https://ipfs.io/ipfs/')} alt={nft.name} loading="lazy" /> <h3>{nft.name}</h3> <p>{nft.description}</p> </div> )}// 列表页使用虚拟滚动处理大量 NFTimport { useVirtualizer } from '@tanstack/react-virtual'function NFTList({ nfts }: { nfts: NFTMetadata[] }) { const parentRef = useRef<HTMLDivElement>(null) const virtualizer = useVirtualizer({ count: nfts.length, getScrollElement: () => parentRef.current, estimateSize: () => 320, }) return ( <div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}> <div style={{ height: `${virtualizer.getTotalSize()}px` }}> {virtualizer.getVirtualItems().map((item) => ( <NFTCard key={item.key} nft={nfts[item.index]} /> ))} </div> </div> )}安全细节:IPFS 元数据可能被篡改或包含 XSS 载荷,渲染时必须使用 React 的 {nft.name} 而非 dangerouslySetInnerHTML,React 默认会转义 HTML 实体。交易实现:从挂单到成交的完整流程NFT 交易的核心场景有两种:直接购买(固定价格)和竞价拍卖。以固定价格购买为例:import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi'import { parseEther } from 'viem'const MARKETPLACE_ABI = [ 'function buyItem(address nftAddress, uint256 tokenId) payable',]function BuyNFTButton({ nftAddress, tokenId, price,}: { nftAddress: `0x${string}` tokenId: bigint price: string}) { const { writeContract, data: hash } = useWriteContract() const { isLoading, isSuccess } = useWaitForTransactionReceipt({ hash }) const handleBuy = () => { writeContract({ address: MARKETPLACE_ADDRESS, abi: MARKETPLACE_ABI, functionName: 'buyItem', args: [nftAddress, tokenId], value: parseEther(price), }) } return ( <button onClick={handleBuy} disabled={isLoading}> {isLoading ? '交易确认中...' : isSuccess ? '购买成功' : `购买 ${price} ETH`} </button> )}挂单(Listing)流程:const { writeContract } = useWriteContract()function listNFT(nftAddress: `0x${string}`, tokenId: bigint, price: string) { // 第一步:授权 marketplace 合约操作 NFT // 第二步:创建挂单 writeContract({ address: nftAddress, abi: ERC721_ABI, functionName: 'approve', args: [MARKETPLACE_ADDRESS, tokenId], }) // approve 交易确认后再调用 createListing}追问:如何处理交易失败和回滚? wagmi 的 useWriteContract 返回的 error 字段包含交易失败原因。常见失败场景包括:Gas 不足、合约 require 条件不满足、前端价格与链上价格不同步(其他人先买了)。最后一种需要通过 useContractRead 实时获取最新价格或在交易前做链上校验。安全防护:前端必须做的事前端安全是 NFT 应用的最后一道防线,核心措施包括:交易模拟:在用户签名前,通过 Tenderly 或自建模拟器预执行交易,检测是否会触发恶意合约调用。viem 支持 call 方法做本地模拟:import { publicClient } from './config'// 模拟交易,不会实际发送const result = await publicClient.simulateContract({ address: nftAddress, abi: ERC721_ABI, functionName: 'transferFrom', args: [fromAddress, toAddress, tokenId], account: userAddress,})钓鱼防护:验证交易参数与用户预期一致。攻击者可能构造恶意合约,在 transferFrom 中转移比预期更多的资产。前端应在签名弹窗中明确显示操作内容和涉及资产。合约地址白名单:只允许与已验证的合约地址交互,拒绝未知合约调用,防止用户被诱导与钓鱼合约交互。签名内容可读化:使用 EIP-712 类型化签名,让用户在钱包中看到结构化的签名内容而非不透明的十六进制数据:const domain = { name: 'NFT Marketplace', version: '1', chainId: 1, verifyingContract: MARKETPLACE_ADDRESS,}const types = { Listing: [ { name: 'nftAddress', type: 'address' }, { name: 'tokenId', type: 'uint256' }, { name: 'price', type: 'uint256' }, ],}const signature = await signTypedData({ domain, types, primaryType: 'Listing', message: { nftAddress, tokenId, price },})多链与账户抽象2026 年的 NFT 应用需要考虑多链部署和账户抽象。用户可能持有 Ethereum 主网和 Polygon 上的不同 NFT,前端需要支持链切换和跨链资产聚合展示。ERC-4337 账户抽象让用户无需管理助记词即可使用社交登录创建钱包,这对降低 NFT 应用的使用门槛至关重要。前端集成账户抽象的典型方案是使用 Privy 或 Biconomy 的 SDK,它们封装了智能账户的创建和 Gas 代付逻辑,用户可用邮箱或 Google 账号登录,无需安装 MetaMask。