Next.js 最佳实践
Next.js 最佳实践
本页内容正在整理中,欢迎贡献。
概述
Next.js 是构建以太坊 DApp 前端的主流框架,提供 SSR/SSG、文件路由、API Routes 等功能。本文介绍如何在 Next.js App Router 中正确集成 wagmi/viem,处理客户端/服务端渲染差异,以及 DApp 特有的性能优化技巧。
主要内容
项目初始化
npx create-next-app@latest my-dapp \ --typescript \ --tailwind \ --app \ --src-dir
cd my-dappnpm install wagmi viem @tanstack/react-query @rainbow-me/rainbowkit目录结构推荐
src/├── app/│ ├── layout.tsx # 根 Layout,包含 Providers│ ├── page.tsx # 首页│ └── api/│ └── nonce/route.ts # SIWE nonce API├── components/│ ├── wallet/ # 钱包相关组件│ ├── contract/ # 合约交互组件│ └── ui/ # 通用 UI 组件├── hooks/│ ├── useTokenBalance.ts # 自定义 wagmi hooks│ └── useNFTs.ts├── lib/│ ├── wagmi.ts # wagmi config│ ├── contracts.ts # 合约 ABI 和地址│ └── utils.ts└── types/ └── contracts.ts # 合约类型定义处理 SSR/Hydration
wagmi 的 hooks 只能在客户端运行,需要正确处理:
import { createConfig, http } from "wagmi";import { mainnet, sepolia } from "wagmi/chains";import { injected, walletConnect } from "wagmi/connectors";
export const wagmiConfig = createConfig({ chains: [mainnet, sepolia], connectors: [ injected(), walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }), ], transports: { [mainnet.id]: http(), [sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL), }, // 重要:禁用 SSR(避免 hydration 错误) ssr: true,});"use client"; // 必须标记为客户端组件import { WagmiProvider, cookieToInitialState } from "wagmi";import { wagmiConfig } from "@/lib/wagmi";
export function Providers({ children, cookie,}: { children: React.ReactNode; cookie?: string | null;}) { const initialState = cookieToInitialState(wagmiConfig, cookie); return ( <WagmiProvider config={wagmiConfig} initialState={initialState}> {/* ... */} </WagmiProvider> );}合约地址与 ABI 管理
import { Address } from "viem";import { mainnet, sepolia } from "wagmi/chains";
export const CONTRACT_ADDRESSES: Record<number, { myToken: Address }> = { [mainnet.id]: { myToken: "0x主网地址" as Address, }, [sepolia.id]: { myToken: "0xSepolia地址" as Address, },};
// ABI 使用 as const 保留完整类型信息export const MY_TOKEN_ABI = [ { name: "balanceOf", type: "function", stateMutability: "view", inputs: [{ name: "account", type: "address" }], outputs: [{ name: "", type: "uint256" }], },] as const;自定义 Hooks 封装
import { useAccount, useChainId, useReadContract } from "wagmi";import { CONTRACT_ADDRESSES, MY_TOKEN_ABI } from "@/lib/contracts";import { formatEther } from "viem";
export function useTokenBalance() { const { address } = useAccount(); const chainId = useChainId();
const contractAddress = CONTRACT_ADDRESSES[chainId]?.myToken;
const { data, isLoading, refetch } = useReadContract({ address: contractAddress, abi: MY_TOKEN_ABI, functionName: "balanceOf", args: address ? [address] : undefined, query: { enabled: !!address && !!contractAddress }, });
return { balance: data ? formatEther(data) : "0", isLoading, refetch, };}环境变量配置
NEXT_PUBLIC_WC_PROJECT_ID=your_walletconnect_project_idNEXT_PUBLIC_SEPOLIA_RPC_URL=https://eth-sepolia.g.alchemy.com/v2/KEYNEXT_PUBLIC_MAINNET_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/KEY
# 服务端私密变量(不加 NEXT_PUBLIC_ 前缀)ALCHEMY_API_KEY=your_keyAPI Routes:后端 Web3 操作
import { NextRequest, NextResponse } from "next/server";import { createWalletClient, http } from "viem";import { privateKeyToAccount } from "viem/accounts";import { sepolia } from "viem/chains";
// 后台铸造(例如验证后免 Gas 为用户铸造)export async function POST(req: NextRequest) { const { recipientAddress } = await req.json();
const account = privateKeyToAccount(process.env.MINTER_PRIVATE_KEY as `0x${string}`); const client = createWalletClient({ account, chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL), });
const hash = await client.writeContract({ address: NFT_CONTRACT, abi: NFT_ABI, functionName: "mint", args: [recipientAddress], });
return NextResponse.json({ hash });}性能优化
- 合约 ABI Tree Shaking:只导入用到的 ABI 片段
- useReadContracts:批量读取多个合约数据,减少 RPC 调用
- 查询缓存:通过
staleTime控制数据刷新频率 - 动态导入:对重型 Web3 组件使用
next/dynamic
// 动态导入避免 SSR 问题const WalletModal = dynamic(() => import("@/components/WalletModal"), { ssr: false,});