name: 前端部署手册 description: 用于以太坊 dApps 的完整构建到生产流程。包括分支模式设置、IPFS 部署、Vercel 配置、ENS 子域名设置和完整生产检查清单。基于 Scaffold-ETH 2 但适用于任何以太坊前端项目。在部署任何 dApp 到生产时使用。
前端部署手册
你可能做错的地方
“我会使用 yarn chain。” 错误。yarn chain 给你一个空的本地链,没有协议、没有代币、没有状态。yarn fork --network base 给你一个真实的 Base 副本,包含 Uniswap、Aave、USDC、真实的鲸鱼余额——一切。总是使用分支模式。
“我部署到 IPFS 并且它工作了。” CID 是否改变了?如果没有,你部署了过时的输出。路由是否工作?没有 trailingSlash: true,除了 / 外的每个路由都返回 404。你检查了 OG 图像吗?没有 NEXT_PUBLIC_PRODUCTION_URL,它指向 localhost:3000。
“我会手动设置项目。” 不要。npx create-eth@latest 处理一切——Foundry、Next.js、RainbowKit、脚手架钩子。永远不要运行 forge init 或从头创建 Next.js 项目。
分支模式设置
为什么分支,不是链
yarn chain (错误) yarn fork --network base (正确)
└─ 空的本地链 └─ 真实的 Base 主网分支
└─ 没有协议 └─ Uniswap、Aave 等可用
└─ 没有代币 └─ 真实的 USDC、WETH 存在
└─ 在隔离中测试 └─ 针对真实状态测试
设置
npx create-eth@latest # 选择:foundry、目标链、名称
cd <项目名称>
yarn install
yarn fork --network base # 终端 1:真实 Base 的分支
yarn deploy # 终端 2:部署合约到分支
yarn start # 终端 3:Next.js 前端
关键:链 ID 陷阱
使用分支模式时,前端目标网络必须是 chains.foundry(链 ID 31337),不是你要分支的链。
分支在本地 Anvil 上运行,链 ID 31337。即使你在分支 Base:
// scaffold.config.ts 在开发期间
targetNetworks: [chains.foundry], // ✅ 不是 chains.base!
只有当部署合约到真实网络时,才切换到 chains.base。
启用区块挖掘
# 在新终端中——对于时间依赖的逻辑是必需的
cast rpc anvil_setIntervalMining 1
没有这个,block.timestamp 保持冻结。任何使用时间戳的合约逻辑(截止时间、过期、归属)将无声地中断。
使其永久化 编辑 packages/foundry/package.json 在分支脚本中添加 --block-time 1。
部署到 IPFS(推荐)
IPFS 是 SE2 推荐的部署路径。完全避免 Vercel 的内存限制。产生一个完全去中心化的静态站点。
完整构建命令
cd packages/nextjs
rm -rf .next out # 总是先清理
NEXT_PUBLIC_PRODUCTION_URL="https://yourapp.yourname.eth.link" \
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" \
NEXT_PUBLIC_IPFS_BUILD=true \
NEXT_PUBLIC_IGNORE_BUILD_ERROR=true \
yarn build
# 上传到 BuidlGuidl IPFS
yarn bgipfs upload out
# 保存 CID!
Node 25+ localStorage 填充(必需)
Node.js 25+ 自带一个内置的 localStorage 对象,缺少标准 WebStorage API 方法(getItem、setItem)。这会破坏 next-themes、RainbowKit 和任何在静态页面生成期间调用 localStorage.getItem() 的库。
你会看到的错误:
TypeError: localStorage.getItem is not a function
Error occurred prerendering page "/_not-found"
修复: 在 packages/nextjs/ 创建 polyfill-localstorage.cjs:
if (typeof globalThis.localStorage !== "undefined" &&
typeof globalThis.localStorage.getItem !== "function") {
const store = new Map();
globalThis.localStorage = {
getItem: (key) => store.get(key) ?? null,
setItem: (key, value) => store.set(key, String(value)),
removeItem: (key) => store.delete(key),
clear: () => store.clear(),
key: (index) => [...store.keys()][index] ?? null,
get length() { return store.size; },
};
}
为什么使用 --require 而不是 instrumentation.ts? Next.js 为预渲染生成一个单独的构建工作进程。--require 注入到每个 Node 进程(包括工作进程)。next.config.ts 填充只运行在主进程。instrumentation.ts 不在构建工作进程中运行。只有 --require 有效。
IPFS 路由——为什么路由中断
IPFS 网关提供静态文件。没有服务器处理路由。三件事必须为真:
1. output: "export" 在 next.config.ts ——生成静态 HTML 文件。
2. trailingSlash: true(关键) ——这是路由中断的第一原因:
trailingSlash: false(默认)→ 生成debug.htmltrailingSlash: true→ 生成debug/index.html- IPFS 网关自动解析目录到
index.html,但不是裸文件名 - 没有尾部斜杠:
/debug→ 404 ❌ - 有尾部斜杠:
/debug→debug/→debug/index.html✅
3. 页面必须在静态预渲染中幸存 ——任何在 yarn build 期间崩溃的页面(导入时的浏览器 API、localStorage)被静默跳过→ IPFS 上的 404。
完整的 IPFS 安全 next.config.ts 模式:
const isIpfs = process.env.NEXT_PUBLIC_IPFS_BUILD === "true";
if (isIpfs) {
nextConfig.output = "export";
nextConfig.trailingSlash = true;
nextConfig.images = { unoptimized: true };
}
SE2 的区块浏览器页面 在导入时使用 localStorage 并在静态导出期间崩溃。如果不需,重命名 app/blockexplorer 为 app/_blockexplorer-disabled。
过时构建检测
第一号 IPFS 陷阱: 你编辑代码,然后部署旧构建。
# 任何代码更改后强制:
rm -rf .next out # 1. 删除旧工件
# ... 运行完整构建命令 ... # 2. 从头重新构建
grep -l "YOUR_STRING" out/_next/static/chunks/app/*.js # 3. 验证更改存在
# 时间戳检查:
stat -f '%Sm' app/page.tsx # 源修改时间
stat -f '%Sm' out/ # 构建输出时间
# 源比 out/ 新 = 过时构建。先重建!
CID 是证据: 如果部署后 IPFS CID 没有改变,你部署了相同的内容。真正的代码更改总是产生新的 CID。
部署后验证路由
ls out/*/index.html # 每个路由有一个目录 + index.html
curl -s -o /dev/null -w "%{http_code}" -L "https://GATEWAY/ipfs/CID/debug/"
# 应该返回 200,不是 404
部署到 Vercel(替代方案)
SE2 是一个单体仓库——Vercel 需要特殊配置。
配置
- 根目录:
packages/nextjs - 安装命令:
cd ../.. && yarn install - 构建命令: 留默认(
next build) - 输出目录: 留默认(
.next)
# 通过 API:
curl -X PATCH "https://api.vercel.com/v9/projects/PROJECT_ID" \
-H "Authorization: Bearer $VERCEL_TOKEN" \
-H "Content-Type: application/json" \
-d '{"rootDirectory": "packages/nextjs", "installCommand": "cd ../.. && yarn install"}'
常见失败
| 错误 | 原因 | 修复 |
|---|---|---|
| “未检测到 Next.js 版本” | 根目录未设置 | 设置为 packages/nextjs |
| “cd packages/nextjs: 没有这样的文件” | 构建命令有 cd |
清除它——根目录处理这个 |
| OOM / 退出码 129 | SE2 单体仓库超过 8GB | 使用 IPFS 替代,或 vercel --prebuilt |
决策树
想部署 SE2?
├─ IPFS(推荐)→ yarn ipfs / 手动构建 + 上传
│ └─ 完全去中心化,无内存限制,与 ENS 配合
├─ Vercel → 设置根目录 + 安装命令
│ └─ 快速 CDN,但中心化。大型项目可能 OOM
└─ vercel --prebuilt → 本地构建,推送工件到 Vercel
└─ 最佳两者:本地构建能力 + Vercel CDN
ENS 子域名设置
两个主网交易,将 ENS 子域名指向你的 IPFS 部署。
交易 1:创建子域名(仅新应用)
- 打开
https://app.ens.domains/yourname.eth - 转到“子名”标签→“新子名”
- 输入标签(如
myapp)→ 下一步→跳过个人资料→打开钱包→确认 - 如果 gas 卡住:切换 MetaMask 到以太坊→活动标签→“加速”
交易 2:设置 IPFS 内容哈希
- 导航到
https://app.ens.domains/myapp.yourname.eth - “记录”标签→“编辑记录”→“其他”标签
- 粘贴到内容哈希字段:
ipfs://<CID> - 保存→打开钱包→在 MetaMask 中确认
对于现有应用的更新:跳过交易 1,只做交易 2。
验证
# 1. 链上内容哈希匹配
RESOLVER=$(cast call 0x00000000000C2e074eC69A0dFb2997BA6C7d2e1e \
"resolver(bytes32)(address)" $(cast namehash myapp.yourname.eth) \
--rpc-url https://eth.llamarpc.com)
cast call $RESOLVER "contenthash(bytes32)(bytes)" \
$(cast namehash myapp.yourname.eth) --rpc-url https://eth.llamarpc.com
# 2. 网关响应(缓存可能需 5-15 分钟)
curl -s -o /dev/null -w "%{http_code}" -L "https://myapp.yourname.eth.link"
# 3. OG 元数据正确(不是 localhost)
curl -s -L "https://myapp.yourname.eth.link" | grep 'og:image'
使用 .eth.link 不是 .eth.limo —— .eth.link 在移动端工作更好。
进入生产——完整检查清单
当用户说“发布它”,按照这个精确序列。
步骤 1:最终代码审查 🤖
- 所有反馈已纳入
- 没有重复 h1,没有原始地址,没有共享 isLoading
scaffold.config.ts有rpcOverrides和pollingInterval: 3000
步骤 2:选择域名 👤
问:“你想要什么子域名?例如 myapp.yourname.eth → `myapp.yourname.eth.link”"
步骤 3:生成 OG 图像 + 修复元数据 🤖
- 创建 1200×630 PNG(
public/thumbnail.png)——不是库存 SE2 缩略图 - 设置
NEXT_PUBLIC_PRODUCTION_URL为实时域名 - 验证
og:image将解析为绝对生产 URL
步骤 4:清洁构建 + IPFS 部署 🤖
cd packages/nextjs && rm -rf .next out
NEXT_PUBLIC_PRODUCTION_URL="https://myapp.yourname.eth.link" \
NODE_OPTIONS="--require ./polyfill-localstorage.cjs" \
NEXT_PUBLIC_IPFS_BUILD=true NEXT_PUBLIC_IGNORE_BUILD_ERROR=true \
yarn build
# 上传前验证:
ls out/*/index.html # 路由存在
grep 'og:image' out/index.html # 不是 localhost
stat -f '%Sm' app/page.tsx # 源比 out/ 旧
stat -f '%Sm' out/
yarn bgipfs upload out # 保存 CID
步骤 5:分享供批准 👤
发送:“构建准备审查:https://community.bgipfs.com/ipfs/<CID>”
在接触 ENS 前等待批准。
步骤 6:设置 ENS 🤖
创建子域名(如果新)+ 设置 IPFS 内容哈希。两个主网交易。
步骤 7:验证 🤖
- 内容哈希在链上匹配
.eth.link网关响应 200- OG 图像正确加载
- 路由工作(
/debug/等)
步骤 8:报告 👤
“实时在 https://myapp.yourname.eth.link —— ENS 内容哈希在链上确认,元数据设置完成。”
构建验证过程
构建不是在代码编译时完成。它在像真实用户一样测试后才完成。
阶段 1:代码质量保证(自动化)
- 扫描
.tsx文件中的原始地址字符串(应使用<Address/>) - 扫描多个按钮共享的
isLoading状态 - 扫描交易按钮上缺少的
disabled属性 - 验证 RPC 配置和轮询间隔
- 验证 OG 元数据有绝对 URL
- 验证任何文件中没有公共 RPC
阶段 2:智能合约测试
forge test # 所有测试通过
forge test --fuzz-runs 10000 # 模糊测试
测试边缘情况:零金额、最大金额、未经授权的调用者、重入尝试。
阶段 3:浏览器测试(真正的测试)
打开应用并进行完整演练:
- 加载应用 ——是否正确渲染?
- 检查页面标题 ——是否正确,不是“Scaffold-ETH 2”?
- 连接钱包 ——连接流程是否工作?
- 错误网络 ——在错误链上连接,验证“切换到 Base”出现
- 切换网络 ——点击切换按钮,验证工作
- 批准流程 ——验证批准按钮显示,点击,等待交易,验证操作按钮出现
- 主要操作 ——点击主要操作,验证加载器,等待交易,验证状态更新
- 错误处理 ——拒绝钱包中的交易,验证 UI 恢复
- 地址显示 ——所有地址显示 ENS/blockies,不是原始十六进制?
- 分享 URL ——检查 OG 展开(图像、标题、描述)
阶段 4:质量保证子代理(复杂构建)
对于大型项目,生成一个带有新上下文的子代理。给它仓库路径和部署 URL。它根据 UX 规则读取所有代码,打开浏览器,独立点击,并报告问题。
不要做这些
- ❌
yarn chain——使用yarn fork --network <链> - ❌
forge init——使用npx create-eth@latest - ❌ 手动 Next.js 设置 —— SE2 处理它
- ❌ 手动钱包连接 —— SE2 预配置了 RainbowKit
- ❌ 编辑
deployedContracts.ts——它是自动生成的 - ❌ 在
scaffold.config.ts中硬编码 API 密钥 ——使用.env.local - ❌ 在生产中使用
mainnet.base.org——使用 Alchemy 或类似
资源
- SE2 文档: https://docs.scaffoldeth.io/
- UI 组件: https://ui.scaffoldeth.io/
- SpeedRun Ethereum: https://speedrunethereum.com/
- ETH 技术树: https://www.ethtechtree.com
- BuidlGuidl IPFS: https://upload.bgipfs.com