从第一篇的钱包连接,到第五篇的多链与智能钱包,我们一步步搭建了一个功能完备的全栈 DApp。但“能跑”和“能服务成千上万用户”之间还隔着关键的一步:生产化。
今天我们将聚焦:
- 前端性能优化(缓存、请求合并、代码分割)
- 后端 RPC 连接池与降级策略
- 安全加固(环境变量、合约调用防护、CORS、速率限制)
- 使用 Docker 容器化前后端
- CI/CD 自动化部署(GitHub Actions)
- 监控与日志
- 总结整个系列
这篇文章不会引入新的合约交互,而是把你的代码打磨到随时可以上线。
1. 前端性能优化
1.1 TanStack Query 的缓存策略
wagmi 内部使用 TanStack Query(前身 React Query)来管理所有合约读取。我们可以通过 QueryClient 全局调整缓存行为,减少不必要的 RPC 调用。
修改 frontend/src/main.tsx 中 QueryClient 的初始化:
import { QueryClient } from '@tanstack/react-query'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30 秒内视为新鲜,不重取
gcTime: 5 * 60_000, // 缓存保留 5 分钟(原 cacheTime)
retry: 2, // 失败重试 2 次
refetchOnWindowFocus: false, // 生产环境常关闭
},
},
})
对于变化较慢的数据(如代币符号、精度),可以单独设置更长的 staleTime:
useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'symbol',
query: { staleTime: Infinity }, // 几乎不变化
})
1.2 合并多个合约读取
在“多链余额面板”中我们已经用 useReadContracts 批量读取。任何需要同时读取多个合约数据的场景,都应使用该 Hook,它会将请求合并到一个 multicall(如果链支持)或至少减少渲染次数。
例如,一次性获取代币符号、精度、余额:
const { data } = useReadContracts({
contracts: [
{ address: tokenAddress, abi: erc20Abi, functionName: 'symbol' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'decimals' },
{ address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [address!] },
],
query: { enabled: !!address },
})
const [symbol, decimals, balance] = data?.map(d => d.result) ?? []
1.3 代码分割与懒加载
Vite 默认对动态 import() 支持很好。将大的路由页面或低频组件改为懒加载:
import { lazy, Suspense } from 'react'
const Swap = lazy(() => import('./components/Swap'))
const AddLiquidity = lazy(() => import('./components/AddLiquidity'))
const TransferHistory = lazy(() => import('./components/TransferHistory'))
function App() {
return (
<Suspense fallback={<div>加载中...</div>}>
<Swap />
<AddLiquidity />
<TransferHistory />
</Suspense>
)
}
这能显著减少初始打包体积。
2. 后端性能与可靠性
2.1 RPC 连接池与轮换
后端监听事件、查询数据都依赖 RPC 节点。生产环境绝不能依赖单一公共端点。我们可以实现一个简单的轮询 + 降级管理器:
// backend/src/rpcManager.ts
import { createPublicClient, http, fallback } from 'viem'
import { sepolia } from 'viem/chains'
const RPC_URLS = [
process.env.SEPOLIA_RPC_1!,
process.env.SEPOLIA_RPC_2!,
process.env.SEPOLIA_RPC_3!,
]
export const sepoliaClient = createPublicClient({
chain: sepolia,
transport: fallback(
RPC_URLS.map(url => http(url, { timeout: 10_000 })),
{ rank: true } // 自动根据延迟和稳定性排序
),
})
fallback 传输会自动在 URL 之间切换,保证高可用。
2.2 事件监听重启策略
长时间运行的 watchContractEvent 可能因网络波动断开。我们需要在监听器中加入自动重启:
function watchWithRetry(client, params, retryDelay = 5000) {
const unwatch = client.watchContractEvent({
...params,
onError(error) {
console.error('事件监听错误,将在 5 秒后重启', error)
setTimeout(() => {
unwatch()
watchWithRetry(client, params, retryDelay)
}, retryDelay)
},
})
return unwatch
}
2.3 数据库连接优化
对于 SQLite,确保 WAL 模式开启以提高并发读取:
const db = new Database('events.db')
db.pragma('journal_mode = WAL')
如果未来数据量增大,考虑迁移到 PostgreSQL,并使用连接池(如 pg-pool)。
3. 安全加固
3.1 环境变量管理
所有敏感配置(RPC URL、私钥、项目 ID)必须通过环境变量注入,绝不硬编码。前端使用 VITE_ 前缀暴露给客户端时,务必注意不要包含后端密钥。
后端建议使用 dotenv 加载 .env 文件,并在 .gitignore 中排除。
3.2 合约调用防护
- 验证输入:在前端和后端都检查地址格式、数值范围。
- 滑点保护:Uniswap 的
amountOutMin绝不可为 0。计算最小输出量时应基于实时价格并留出滑点容忍度(如 0.5%)。 - 授权额度:提示用户只授权本次所需数量,而不是
MaxUint256。 - 重入检查:虽然标准 ERC-20 和 Uniswap V2 已安全,但如果与自定义合约交互,务必考虑重入风险。
3.3 后端 API 安全
- CORS:只允许你的前端域名:
app.use(cors({
origin: 'https://your-dapp.com',
methods: ['GET', 'POST'],
}))
- 速率限制:使用
express-rate-limit防止滥用:
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 分钟
max: 100, // 每个 IP 最多 100 次请求
})
app.use(limiter)
-
JWT 验证中间件:在需要认证的路由上复用第一篇实现的 JWT 验证。
-
HTTPS:生产环境必须使用 HTTPS,反向代理(Nginx、Caddy)或托管平台(Railway、Fly.io)自动处理。
4. Docker 容器化
将前后端打包为 Docker 镜像,便于在任何环境一致运行。
4.1 后端 Dockerfile
# backend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3001
CMD ["node", "dist/index.js"]
4.2 前端 Dockerfile(静态站点)
Vite 构建输出纯静态文件,可以直接用 Nginx 服务:
# frontend/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf 简单示例:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
4.3 Docker Compose 联合启动
# docker-compose.yml
version: '3.8'
services:
backend:
build: ./backend
ports:
- "3001:3001"
environment:
- NODE_ENV=production
- SEPOLIA_RPC_1=${SEPOLIA_RPC_1}
- SEPOLIA_RPC_2=${SEPOLIA_RPC_2}
- SEPOLIA_RPC_3=${SEPOLIA_RPC_3}
frontend:
build: ./frontend
ports:
- "80:80"
运行 docker compose up -d,整个应用就启动了。
5. CI/CD 自动化部署
使用 GitHub Actions,在推送代码到 main 分支时自动构建镜像并部署到云平台。
5.1 构建与推送镜像到 Docker Hub
创建 .github/workflows/deploy.yml:
name: Build and Deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push backend
uses: docker/build-push-action@v5
with:
context: ./backend
push: true
tags: yourname/dapp-backend:latest
- name: Build and push frontend
uses: docker/build-push-action@v5
with:
context: ./frontend
push: true
tags: yourname/dapp-frontend:latest
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS via SSH
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.VPS_HOST }}
username: ${{ secrets.VPS_USER }}
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/dapp
docker compose pull
docker compose up -d
如果你使用 Railway、Vercel 或 Netlify,也可以用它们的官方 Action 或 CLI 部署。
6. 监控与日志
6.1 后端日志
使用 winston 或 pino 记录结构化日志,并输出到文件或外部服务。
import pino from 'pino'
const logger = pino({ level: 'info' })
logger.info({ txHash: log.transactionHash }, '新交换事件')
6.2 前端错误跟踪
集成 Sentry 捕获前端异常:
import * as Sentry from '@sentry/react'
Sentry.init({
dsn: 'YOUR_SENTRY_DSN',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1.0,
})
包裹 App:
ReactDOM.createRoot(...).render(
<Sentry.ErrorBoundary fallback={<p>出错啦</p>}>
<App />
</Sentry.ErrorBoundary>
)
6.3 链上事件监控告警
在后端添加自定义告警:当大额转账或价格异常波动时,通过 Telegram/Discord 机器人发送通知。
7. 系列回顾与下一步
这六篇文章,我们从零到生产构建了一个全栈 DApp:
- 钱包集成 — 连接多钱包,签名验证,连接即登录
- 合约事件监听 — 实时捕捉链上日志,后端持久化
- ERC-20 代币 — 转账、授权、余额查询
- Uniswap V2 — 添加流动性、代币兑换,事件索引
- 多链与智能钱包 — 动态网络切换,Safe 多签集成
- 生产化 — 性能优化、安全加固、Docker、CI/CD、监控
你现在拥有一个可以扩展的 Web3 全栈脚手架。未来你可以继续叠加:
- ERC-721/NFT 的铸造与市场
- ERC-4337 账户抽象 实现免 Gas 或社交恢复
- Layer2 原生支持 (Arbitrum Nova, Base)
- 去中心化存储 (IPFS/Arweave) 保存元数据
- The Graph/Subsquid 替代自建索引器