目标:用 Reown AppKit(Web3Modal 正统升级)在 Svelte/SvelteKit 中实现“开箱即用”的钱包选择器 / 账户面板 / 切链按钮,并结合 wagmi + viem 完成读写合约、签名、事件监听等链交互。
文档按模块拆分,复制即用;所有关键代码都给到可运行实现 + 中文注释。
0. 项目准备
0.1 获取 Project ID
AppKit 需要一个项目 ID 才能调用官方聚合与二维码弹窗。
- 进入 Reown AppKit 官方控制台(Dashboard),创建项目并复制 Project ID。
常见入口:
dashboard.reown.com(或搜索 “Reown AppKit Dashboard”)。 - 放入前端环境变量(见 1.2)。
0.2 环境要求
- Node 18+(建议 20+)
- SvelteKit v2+ / v5(本文以 SvelteKit 为例)
- 基础 TS / EVM 知识(链 ID、签名、EIP-1559)
1. 初始化项目与安装依赖
# 新建 SvelteKit 项目(已有项目可跳过)
npm create svelte@latest svelte-appkit-demo
cd svelte-appkit-demo
npm i
# 安装 AppKit 与适配器 + wagmi/viem(核心链交互)
npm i @reown/appkit @reown/appkit-adapter-wagmi wagmi viem
1.2 环境变量
在项目根目录新增 .env / .env.production:
# 建议使用 SvelteKit 公共前缀(可在浏览器端读取)
PUBLIC_APPKIT_PROJECT_ID=你的_Reown_AppKit_Project_ID
2. 目录结构建议
src/
└─ lib/
├─ appkit.ts # AppKit + WagmiAdapter 初始化(只在浏览器执行)
├─ web3/
│ ├─ stores.ts # watchAccount / watchChainId → Svelte stores
│ └─ actions.ts # 读写合约 / 签名 / 切链 等无框架 API
└─ components/
├─ ConnectDock.svelte # 顶部工具条(AppKit 按钮 + 自定义状态)
└─ Examples.svelte # 余额读取 / 签名 / 写交易 示例
routes/
├─ +layout.svelte
└─ +page.svelte
3. AppKit 初始化(核心)
用
@reown/appkit-adapter-wagmi生成wagmiConfig,并交给 AppKit。随后读写合约依旧用 @wagmi/core,只是把wagmiConfig换成这份。
src/lib/appkit.ts
// 只在浏览器端初始化(SSR 保护)
import { browser } from '$app/environment'
// AppKit & 适配器
import { createAppKit } from '@reown/appkit'
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
// 内置多链预设(可按需扩充)
import { arbitrum, mainnet, base, optimism, polygon, sepolia } from '@reown/appkit/networks'
// 从环境变量读取 Project ID(SvelteKit 公共变量)
import { PUBLIC_APPKIT_PROJECT_ID } from '$env/static/public'
// 导出:给其他模块使用
export let appKit: ReturnType<typeof createAppKit> | undefined
export let wagmiConfig: any // 实际是 wagmi 的 Config 类型
if (browser) {
if (!PUBLIC_APPKIT_PROJECT_ID) {
console.warn('PUBLIC_APPKIT_PROJECT_ID 未设置,AppKit 将无法弹出钱包选择器')
}
// 选择支持的链(顺序会影响默认链)
const networks = [base, arbitrum, mainnet, optimism, polygon, sepolia]
// 用 AppKit 的 WagmiAdapter 生成 wagmiConfig(后续 read/write 都靠它)
const wagmiAdapter = new WagmiAdapter({
projectId: PUBLIC_APPKIT_PROJECT_ID,
networks,
ssr: true, // 为 SvelteKit 友好
// 可选:自定义 transports(RPC),放专用服务商以提升质量
// transports: { [base.id]: http('https://base-mainnet.g.alchemy.com/v2/xxx') }
})
wagmiConfig = wagmiAdapter.wagmiConfig
// 创建 AppKit(注册 Web Components + 默认模态)
appKit = createAppKit({
adapters: [wagmiAdapter],
projectId: PUBLIC_APPKIT_PROJECT_ID,
networks,
defaultNetwork: base, // 默认链(按钮 & Modal 初始展示)
features: {
email: false, // 如需启用邮箱/社交登录,置为 true(需在控制台开通)
onramp: false, // 法币买币(需要控制台配置)
swap: false // 代币兑换(需要控制台配置)
},
themeMode: 'dark', // 'light' | 'dark'
themeVariables: { // 自定义主题变量(与 Web Components 样式变量一致)
'--w3m-accent': '#00d0b6',
'--w3m-color-mix': '#14161a'
},
metadata: { // 钱包会展示的 dapp 信息
name: 'Svelte × Reown AppKit',
description: 'SvelteKit + AppKit + Wagmi/Viem 最小可用集成',
url: 'https://example.com',
icons: ['https://avatars.githubusercontent.com/u/179229932?s=200&v=4']
}
})
}
说明
- AppKit 会注册一组 Web Components(如
<appkit-button/>等),你在页面放上即可。wagmiConfig由WagmiAdapter提供;后续所有@wagmi/core的 actions 都使用这份wagmiConfig。
4. 全局挂载(一次性引入 AppKit)
src/routes/+layout.svelte
<script lang="ts">
// 仅导入即可完成 AppKit 初始化与 Web Components 注册
import '$lib/appkit'
</script>
<slot />
5. 顶部工具条(AppKit 组件 + 自定义状态)
src/lib/components/ConnectDock.svelte
<script lang="ts">
import { appKit } from '$lib/appkit'
import { address, shortAddr, chainId, status } from '$lib/web3/stores'
function openConnect() { appKit?.open({ view: 'Connect' }) } // 编程式打开
function openNetworks() { appKit?.open({ view: 'Networks' }) }
function openAccount() { appKit?.open({ view: 'Account' }) }
</script>
<nav class="dock">
<!-- 方式 A:使用 AppKit 自带 Web Components(开箱即用) -->
<appkit-connect-button />
<appkit-network-button />
<appkit-account-button />
<!-- 方式 B:自定义按钮(用编程式 API 打开) -->
<button on:click={openConnect}>连接钱包</button>
<button on:click={openNetworks}>切换网络</button>
<button on:click={openAccount}>账户</button>
<!-- 自定义状态展示(Svelte Store 来自 watchAccount/watchChainId) -->
<div class="state">
<span>状态:{$status}</span>
<span>链:{$chainId ?? '-'}</span>
<span>地址:{$address ? $shortAddr : '-'}</span>
</div>
</nav>
<style>
.dock{display:flex;gap:12px;align-items:center;flex-wrap:wrap;padding:10px;border-bottom:1px solid #2a2a2a}
.state{display:flex;gap:10px;color:#999}
button{padding:6px 10px}
</style>
常用 Web Components
<appkit-connect-button/>:连接/断开入口<appkit-network-button/>:切换网络<appkit-account-button/>:账户信息/地址/复制/断开- 你也可以完全不用这些按钮,改用
appKit.open({view:'...'})来编程式控制 Modal。
6. Svelte Stores(账户/链状态订阅)
src/lib/web3/stores.ts
import { writable, derived } from 'svelte/store'
import { getAccount, watchAccount, getChainId, watchChainId, reconnect } from '@wagmi/core'
import type { Address } from 'viem'
import { browser } from '$app/environment'
import { wagmiConfig } from '$lib/appkit'
export const address = writable<Address | undefined>()
export const isConnected = writable(false)
export const chainId = writable<number | undefined>()
export const status = writable<'disconnected' | 'connecting' | 'connected'>('disconnected')
export const shortAddr = derived(address, ($a) => $a ? `${$a.slice(0,6)}...${$a.slice(-4)}` : '')
if (browser) {
reconnect(wagmiConfig).catch(() => {})
const unwatchAccount = watchAccount(wagmiConfig, {
onChange(a){
address.set(a.address as Address | undefined)
isConnected.set(!!a.isConnected)
status.set(a.status ?? 'disconnected')
}
})
const unwatchChain = watchChainId(wagmiConfig, { onChange: id => chainId.set(id) })
const a = getAccount(wagmiConfig)
address.set(a.address as Address | undefined)
isConnected.set(!!a.isConnected)
status.set(a.status ?? 'disconnected')
chainId.set(getChainId(wagmiConfig))
// 可选:页面卸载时清理
// addEventListener('beforeunload', () => { unwatchAccount(); unwatchChain() })
}
7. 统一动作 API(读写/签名/切链)
src/lib/web3/actions.ts
// 无框架 API:你可以在任意组件中直接调用
import {
readContract, writeContract, waitForTransactionReceipt,
signMessage, signTypedData, switchChain, simulateContract,
getAccount, getPublicClient
} from '@wagmi/core'
import type { Abi, Address } from 'viem'
import { wagmiConfig } from '$lib/appkit'
// 读取合约
export async function contractRead<T = unknown>(
p: { address: Address; abi: Abi; functionName: string; args?: any[] }
){
return readContract(wagmiConfig, p) as Promise<T>
}
// 写入合约(推荐先 simulate)
export async function contractWrite(
p: { address: Address; abi: Abi; functionName: string; args?: any[] }
){
const sim = await simulateContract(wagmiConfig, p as any)
const hash = await writeContract(wagmiConfig, { ...p, ...sim.request })
return waitForTransactionReceipt(wagmiConfig, { hash })
}
// 基础签名(EIP-191)
export const personalSign = (message: string) =>
signMessage(wagmiConfig, { message })
// 结构化签名(EIP-712)
export async function signTyped(domain: any, types: any, value: Record<string, any>){
const acct = getAccount(wagmiConfig)
if (!acct.address) throw new Error('Not connected')
const primaryType = Object.keys(types)[0]
return signTypedData(wagmiConfig, { account: acct.address, domain, types, primaryType, message: value })
}
// 切链
export const switchTo = (id: number) => switchChain(wagmiConfig, { chainId: id })
// 事件监听(例:监听最新区块)
export function watchNewBlocks(cb:(n:number)=>void){
const pc = getPublicClient(wagmiConfig)
return pc.watchBlocks({ onBlock: (b)=> cb(Number(b.number)) })
}
8. 示例页面:余额读取 / 签名 / 写交易
src/lib/components/Examples.svelte
<script lang="ts">
import { chainId, address } from '$lib/web3/stores'
import { contractRead, contractWrite, personalSign } from '$lib/web3/actions'
import type { Address } from 'viem'
// 示例:Base 链 USDC(只为演示;请替换为你的业务合约)
const USDC: Address = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
const ERC20 = [
{"inputs":[{"name":"owner","type":"address"}],"name":"balanceOf","outputs":[{"type":"uint256"}],"stateMutability":"view","type":"function"},
{"inputs":[{"name":"spender","type":"address"},{"name":"amount","type":"uint256"}],"name":"approve","outputs":[{"type":"bool"}],"stateMutability":"nonpayable","type":"function"}
] as const
let balance = '-'
let signed = ''
let pending = false
async function loadBalance(){
if(!$address) return
const raw = await contractRead<bigint>({ address: USDC, abi: ERC20 as any, functionName:'balanceOf', args: [$address] })
balance = (Number(raw)/1e6).toLocaleString() + ' USDC'
}
async function doSign(){
signed = await personalSign('Hello from AppKit + Svelte!')
}
async function doApprove(){
if(!$address) return
pending = true
try {
const r = await contractWrite({
address: USDC, abi: ERC20 as any, functionName:'approve',
args: ['0x0000000000000000000000000000000000000001', 0n] // 示例:approve 0
})
alert(`Tx confirmed at block ${r.blockNumber}`)
} catch(e) {
alert((e as Error).message)
} finally {
pending = false
}
}
</script>
<div class="card">
<div>链:{$chainId ?? '-'}</div>
<div>地址:{$address ?? '-'}</div>
<button on:click={loadBalance} disabled={!$address || pending}>读 USDC 余额</button>
<div>余额:{balance}</div>
<button on:click={doSign} disabled={!$address || pending}>签名一段消息</button>
<div style="word-break:break-all">{signed}</div>
<button on:click={doApprove} disabled={!$address || pending}>
{pending ? '发送中…' : '发送一笔写交易(示例)'}
</button>
</div>
<style>
.card{padding:12px;border:1px dashed #666;border-radius:8px;display:grid;gap:8px}
</style>
9. 页面组装
src/routes/+page.svelte
<script>
import ConnectDock from '$lib/components/ConnectDock.svelte'
import Examples from '$lib/components/Examples.svelte'
</script>
<h1>Svelte × Reown AppKit Playground</h1>
<ConnectDock />
<Examples />
10. 主题 / 多语言 / 编程式控制
10.1 主题
在 createAppKit 里设置:
themeMode: 'dark', // 或 'light'
themeVariables: {
'--w3m-accent': '#00d0b6', // 主题主色
'--w3m-border-radius': '10px' // 圆角等
}
10.2 编程式控制视图
// 打开指定视图:Connect / Account / Networks / AllWallets ...
appKit?.open({ view: 'Connect' })
appKit?.close()
注:可结合你自己的按钮/路由控制弹窗逻辑。
11. 可选功能(按需开启)
下述功能常在 Reown 控制台 配置后方可使用,且可能需要额外 SDK/服务端配合。根据项目需求逐步打开,避免一次性拉太多依赖。
- Email / 社交登录(Embedded / MPC 钱包):在
features.email=true并完成控制台配置后启用。 - 法币买币 On-Ramp:
features.onramp=true,需在控制台绑定商户或支付渠道。 - Swap 兑换:
features.swap=true,适用于快速换币入口。 - 自定义钱包白名单 / 推荐位:控制台可配置优先展示的钱包、默认链等。
12. 事件监听 & L2 实战建议
- 链事件:用
getPublicClient().watchContractEvent/watchBlocks订阅;或在写交易后waitForTransactionReceipt再刷新。 - 多链映射:把合约地址做成
Record<number, Address>,按$chainId动态选择。 - Gas/费用:默认由 RPC 估算;若 L2(如 Base/OP/Arb)拥堵,可给
maxFeePerGas、maxPriorityFeePerGas合理上限。 - 错误文案:识别常见错误(User rejected / execution reverted / nonce too low / replacement underpriced),翻译成人类可读提示。
13. 常见问题(FAQ)
- 必须用 Project ID 吗?
用 AppKit 的聚合弹窗就需要;否则建议改走“裸 wagmi + viem”并自己集成 WalletConnect。 - SSR 报 window 未定义?
将 AppKit 初始化放在browser分支(本文已处理),或在组件的onMount中执行。 - Injected 钱包不出现/冲突?
桌面浏览器装多个扩展会覆盖 provider,先禁用其他扩展测试;移动端请在钱包自带 DApp 浏览器中打开。 - 切链失败?
个别钱包不支持自动wallet_addEthereumChain,需要用户在钱包内手动切换。 - AppKit 事件回调想打点?
建议复用 wagmi 的watchAccount/watchChainId与交易回执事件,在业务侧统一埋点。
14. 安全与 UX 清单
- 优先 EIP-712 结构化签名;签名前给出可读摘要(金额/地址/截止时间)。
- 所有 pending/confirmed/failed 状态明确可见;提供复制 Tx Hash。
- 断网/超时/被替换交易(speed up/cancel)要有兜底提示。
- RPC 降级与限速重试、日志脱敏与隐私合规。
15. License
本文与示例代码 MIT License,可自由复制与二次开发。
(Reown AppKit 的连接弹框本质是一个“多钱包聚合器” ,内置了 WalletConnect v2 能力,既支持扫码登录,也支持唤起(deep-link/通用链接)登录,还覆盖桌面扩展钱包与内置 DApp 浏览器注入的 provider。简明对照给你👇
支持的典型登录路径
- 桌面扩展钱包(Injected)
已安装(MetaMask、Rabby、OKX 等)→ 直接连接;未安装→ 给出安装或改用扫码。 - 移动钱包扫码(WalletConnect v2)
弹出官方 QR 模态框;用 MetaMask/Trust/OKX/TokenPocket/Bitget 等手机钱包 扫描二维码 即可建立会话。 - 移动端唤起(Deep Link / Universal Link)
在手机浏览器里点弹框里的“MetaMask/OKX/Trust…”→ 直接拉起已安装的钱包 App;没装则跳商店下载。 - DApp 浏览器内置注入
若用户在钱包自带 DApp 浏览器打开页面,AppKit 会识别 injected provider 并直接连接。 - 邮箱/社交登录、嵌入式/MPC 钱包(可选模块)
在 Dashboard 开启相应功能后,弹框可展示 Email / 社交登录 按钮,走托管或嵌入式钱包通道(是否显示取决于你在 AppKit 配置里启用与否)。)