以太坊dApp前端UX开发规则Skill frontend-ux

这个技能提供以太坊去中心化应用前端用户体验开发的完整指南,专注于防止常见错误,包括按钮状态管理、地址显示、美元价值集成、RPC配置等,基于Scaffold-ETH 2框架。关键词:以太坊, dApp, 前端开发, UX设计, 智能合约, 区块链, 用户体验, Web3.

DApp开发 0 次安装 0 次浏览 更新于 3/24/2026

名称: 前端用户体验 描述: 用于防止最常见AI代理UI错误的以太坊去中心化应用前端用户体验规则。包括链上按钮、代币批准流程、地址显示、美元价值、RPC配置和发布前元数据的强制模式。围绕Scaffold-ETH 2构建,但模式适用于任何以太坊前端。在构建任何去中心化应用前端时使用。

前端用户体验规则

你可能出错的地方

“按钮工作。” 工作不是标准。它是否在交易期间禁用?是否显示加载指示器?是否在链上确认前保持禁用?如果用户拒绝是否显示错误?AI代理每次都会跳过所有这些。

“我用了wagmi钩子。” 错误的钩子。Scaffold-ETH 2通过useTransactor包装wagmi,它等待交易确认——不仅仅是钱包签名。原始wagmi的writeContractAsync在用户在MetaMask中点击确认后立即解析,在交易被挖出之前。你的按钮会在交易仍在待处理时重新启用。

“我显示了地址。” 作为原始十六进制?那不是显示它。<Address/>提供ENS解析、区块头像、复制到剪贴板和区块浏览器链接。原始0x1234...5678是不可接受的。


规则1:每个链上按钮——加载器 + 禁用

⚠️ 这是AI代理部署的#1错误。 用户点击批准,在他们的钱包中签名,回到应用,批准按钮再次可点击——所以他们再次点击,发送重复交易,现在两个批准待处理。按钮必须从点击时刻到交易在链上确认期间禁用并显示加载指示器。 不是直到钱包关闭。不是直到签名发送。直到区块确认。

任何触发区块链交易的按钮必须:

  1. 点击后立即禁用
  2. 显示加载指示器(“批准中…”、“质押中…”等)
  3. 保持禁用直到状态更新确认操作完成
  4. 完成后显示成功/错误反馈
// ✅ 正确:每个操作独立的加载状态
const [isApproving, setIsApproving] = useState(false);
const [isStaking, setIsStaking] = useState(false);

<button
  disabled={isApproving}
  onClick={async () => {
    setIsApproving(true);
    try {
      await writeContractAsync({ functionName: "approve", args: [...] });
    } catch (e) {
      console.error(e);
      notification.error("批准失败");
    } finally {
      setIsApproving(false);
    }
  }}
>
  {isApproving ? "批准中..." : "批准"}
</button>

❌ 永远不要为多个按钮使用单个共享的isLoading 每个按钮都有自己的加载状态。共享状态会导致UI条件切换按钮时显示错误的加载文本。

仅使用Scaffold钩子——永远不要原始Wagmi

// ❌ 错误:原始wagmi——在签名后解析,不是确认
const { writeContractAsync } = useWriteContract();
await writeContractAsync({...}); // 在MetaMask签名后立即返回!

// ✅ 正确:Scaffold钩子——等待交易被挖出
const { writeContractAsync } = useScaffoldWriteContract("MyContract");
await writeContractAsync({...}); // 等待实际链上确认

为什么: useScaffoldWriteContract在内部使用useTransactor,它等待区块确认。原始wagmi不会——你的UI将在交易仍在内存池时显示“成功”。


规则2:四状态流程——连接 → 网络 → 批准 → 操作

当用户需要与应用交互时,有四个状态。每次只显示一个明显的大按钮:

1. 未连接?       → 大“连接钱包”按钮(不是文本说“连接你的钱包来玩”)
2. 错误网络?       → 大“切换到Base”按钮
3. 批准不足? → “批准”按钮(带加载器,根据规则1)
4. 批准足够?     → “质押” / “存款” / 操作按钮

永远不要显示文本提示如“连接你的钱包来玩”或“请连接以继续。” 显示一个按钮。用户应该总是只有一个东西可以点击。

const { data: allowance } = useScaffoldReadContract({
  contractName: "Token",
  functionName: "allowance",
  args: [address, contractAddress],
});

const needsApproval = !allowance || allowance < amount;
const wrongNetwork = chain?.id !== targetChainId;
const notConnected = !address;

{notConnected ? (
  <RainbowKitCustomConnectButton />  // 大连接按钮——不是文本
) : wrongNetwork ? (
  <button onClick={switchNetwork} disabled={isSwitching}>
    {isSwitching ? "切换中..." : "切换到Base"}
  </button>
) : needsApproval ? (
  <button onClick={handleApprove} disabled={isApproving}>
    {isApproving ? "批准中..." : "批准 $TOKEN"}
  </button>
) : (
  <button onClick={handleStake} disabled={isStaking}>
    {isStaking ? "质押中..." : "质押"}
  </button>
)}

关键细节:

  • 始终通过钩子读取批准额度,以便UI在批准交易确认时自动更新
  • 永远不要仅依赖本地状态跟踪批准额度
  • 错误网络检查首先进行——如果用户在错误网络点击批准,一切都会中断
  • 永远不要同时显示批准和操作按钮——一次一个按钮

规则3:地址显示——总是使用<Address/>

每次显示以太坊地址时,使用scaffold-eth的<Address/>组件:

import { Address } from "~~/components/scaffold-eth";

// ✅ 正确
<Address address={userAddress} />

// ❌ 错误——永远不要渲染原始十六进制
<span>{userAddress}</span>
<p>0x1234...5678</p>

<Address/>处理ENS解析、区块头像、复制到剪贴板、截断和区块浏览器链接。原始十六进制不可接受。

地址输入——总是使用<AddressInput/>

每次用户需要输入以太坊地址时,使用<AddressInput/>

import { AddressInput } from "~~/components/scaffold-eth";

// ✅ 正确
<AddressInput value={recipient} onChange={setRecipient} placeholder="接收地址" />

// ❌ 错误——永远不要使用原始文本输入处理地址
<input type="text" value={recipient} onChange={e => setRecipient(e.target.value)} />

<AddressInput/>提供ENS解析(输入“vitalik.eth”→解析为地址)、区块头像预览、验证和粘贴处理。

配对:<Address/>用于显示,<AddressInput/>用于输入。总是。

显示你的合约地址

每个去中心化应用应该在其主页面底部显示部署的合约地址使用<Address/>。用户希望验证区块浏览器上的合约。这建立信任并且是标准做法。

<div className="text-center mt-8 text-sm opacity-70">
  <p>合约:</p>
  <Address address={deployedContractAddress} />
</div>

规则4:处处显示美元价值

每个显示的代币或ETH金额应包括其美元价值。 每个代币或ETH输入应显示实时美元预览。

// ✅ 正确——显示带美元
<span>1,000 TOKEN (~$4.20)</span>
<span>0.5 ETH (~$1,250.00)</span>

// ✅ 正确——输入带实时美元预览
<input value={amount} onChange={...} />
<span className="text-sm text-gray-500">
  ≈ ${(parseFloat(amount || "0") * tokenPrice).toFixed(2)} USD
</span>

// ❌ 错误——金额无美元上下文
<span>1,000 TOKEN</span>  // 用户不知道这值多少

从哪里获取价格:

  • ETH价格: SE2内置钩子——useNativeCurrencyPrice()
  • 自定义代币: DexScreener API(https://api.dexscreener.com/latest/dex/tokens/TOKEN_ADDRESS)、链上Uniswap报价器或Chainlink预言机

这适用于显示和输入:

  • 显示余额?在它旁边显示美元。
  • 用户输入金额发送/质押/交换?在输入下方显示实时美元预览。
  • 交易确认?显示他们将要执行的操作的美元价值。

规则5:无重复标题

不要在页面主体顶部将应用名称作为<h1>放置。 SE2头部已经显示应用名称。重复它浪费空间并且看起来业余。

// ❌ 错误——AI代理总是这样做
<Header />  {/* 已经显示“🦞 我的去中心化应用” */}
<main>
  <h1>🦞 我的去中心化应用</h1>  {/* 重复!删除这个。 */}
  <p>应用描述</p>
  ...
</main>

// ✅ 正确——直接进入内容
<Header />  {/* 显示应用名称 */}
<main>
  <div className="grid grid-cols-2 gap-4">
    {/* 统计、余额、操作——无冗余标题 */}
  </div>
</main>

规则6:RPC配置

永远不要使用公共RPCmainnet.base.org等)——它们会限速并导致生产中的随机故障。

scaffold.config.ts中,始终设置:

rpcOverrides: {
  [chains.base.id]: process.env.NEXT_PUBLIC_BASE_RPC || "https://mainnet.base.org",
},
pollingInterval: 3000,  // 3秒,不是默认的30000

将API密钥保存在.env.local——永远不要硬编码在提交到Git的配置文件中。

⚠️ SE2的wagmiConfig.tsx添加了一个裸http()(无URL)作为后备传输。 Viem将裸http()解析为链的默认公共RPC(例如Base的mainnet.base.org)。即使在scaffold配置中设置了rpcOverrides,公共RPC仍将被命中,因为viem的fallback()并行触发传输。你必须从services/web3/wagmiConfig.tsx的后备数组中移除裸http(),以便只使用你配置的RPC。如果你不这样做,你的应用将在每个轮询周期中向公共RPC发送请求,并在生产中被429限速。

监控RPC使用: 合理 = 每3秒一次请求。如果你看到15+请求/秒,你有错误:

  • 钩子在循环中重新渲染
  • 重复钩子调用
  • 缺少依赖数组
  • 不需要的钩子上设置watch: true

规则7:发布前检查清单

在将前端部署到生产之前,每个项目必须通过:

开放图 / Twitter卡片(必需):

// 在app/layout.tsx或getMetadata.ts中
export const metadata: Metadata = {
  title: "你的应用名称",
  description: "应用描述",
  openGraph: {
    title: "你的应用名称",
    description: "应用描述",
    images: [{ url: "https://你的生产域名.com/缩略图.png" }],
  },
  twitter: {
    card: "summary_large_image",
    title: "你的应用名称",
    description: "应用描述",
    images: ["https://你的生产域名.com/缩略图.png"],
  },
};

⚠️ 开放图图片URL必须:

  • https://开头的绝对URL
  • 实际生产域名(不是localhost,不是相对路径)
  • 不是可能未设置的环境变量
  • 实际可达(通过浏览器访问URL测试)

移除所有Scaffold-ETH 2默认身份:

  • [ ] README重写——不是SE2模板README
  • [ ] 页脚清理——移除BuidlGuidl链接、“Fork me”链接、支持链接、任何SE2品牌。替换为你的项目仓库链接
  • [ ] 网站图标更新——不是SE2默认
  • [ ] 标签标题是你的应用名称——不是“Scaffold-ETH 2”

完整检查清单:

  • [ ] 开放图图片URL是绝对的、实际生产域名
  • [ ] 开放图标题和描述设置(不是默认SE2文本)
  • [ ] Twitter卡片类型设置(summary_large_image
  • [ ] 所有SE2默认品牌移除(README、页脚、网站图标、标签标题)
  • [ ] 浏览器标签标题正确
  • [ ] RPC覆盖设置(不是公共RPC)
  • [ ] 从wagmiConfig.tsx后备数组中移除裸http()(无静默公共RPC后备)
  • [ ] pollingInterval为3000
  • [ ] 所有合约地址与部署的匹配
  • [ ] 生产代码中无硬编码测试网/localhost值
  • [ ] 每个地址显示使用<Address/>
  • [ ] 每个地址输入使用<AddressInput/>
  • [ ] 每个链上按钮有自己的加载器 + 禁用状态
  • [ ] 批准流程有网络检查 → 批准 → 操作模式
  • [ ] 无重复h1标题匹配头部

externalContracts.ts — 在构建之前

所有外部合约(代币、协议、任何你未部署的)必须在构建前端之前添加到packages/nextjs/contracts/externalContracts.ts,带地址和ABI。

// packages/nextjs/contracts/externalContracts.ts
export default {
  8453: {  // Base链ID
    USDC: {
      address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      abi: [...],  // ERC-20 ABI
    },
  },
} as const;

为什么在之前: Scaffold钩子(useScaffoldReadContractuseScaffoldWriteContract)只与在deployedContracts.ts(自动生成)或externalContracts.ts(手动)中注册的合约一起工作。如果你编写引用未注册合约的前端代码,它会静默失败。

永远不要编辑deployedContracts.ts——它由yarn deploy自动生成。将你的外部合约放在externalContracts.ts中。


人类可读金额

始终在合约单位和显示单位之间转换:

// 合约 → 显示
import { formatEther, formatUnits } from "viem";
formatEther(weiAmount);           // 18小数(ETH、DAI、大多数代币)
formatUnits(usdcAmount, 6);       // 6小数(USDC、USDT)

// 显示 → 合约
import { parseEther, parseUnits } from "viem";
parseEther("1.5");                // → 1500000000000000000n
parseUnits("100", 6);             // → 100000000n(USDC)

永远不要向用户显示原始wei/单位。 1500000000000000000毫无意义。1.5 ETH (~$3,750)意味着一切。


资源