Web3 全栈开发实战:生产级优化与部署

从第一篇的钱包连接,到第五篇的多链与智能钱包,我们一步步搭建了一个功能完备的全栈 DApp。但“能跑”和“能服务成千上万用户”之间还隔着关键的一步:生产化。

今天我们将聚焦:

  • 前端性能优化(缓存、请求合并、代码分割)
  • 后端 RPC 连接池与降级策略
  • 安全加固(环境变量、合约调用防护、CORS、速率限制)
  • 使用 Docker 容器化前后端
  • CI/CD 自动化部署(GitHub Actions)
  • 监控与日志
  • 总结整个系列

这篇文章不会引入新的合约交互,而是把你的代码打磨到随时可以上线。


1. 前端性能优化

1.1 TanStack Query 的缓存策略

wagmi 内部使用 TanStack Query(前身 React Query)来管理所有合约读取。我们可以通过 QueryClient 全局调整缓存行为,减少不必要的 RPC 调用。

修改 frontend/src/main.tsxQueryClient 的初始化:

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 后端日志

使用 winstonpino 记录结构化日志,并输出到文件或外部服务。

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:

  1. 钱包集成 — 连接多钱包,签名验证,连接即登录
  2. 合约事件监听 — 实时捕捉链上日志,后端持久化
  3. ERC-20 代币 — 转账、授权、余额查询
  4. Uniswap V2 — 添加流动性、代币兑换,事件索引
  5. 多链与智能钱包 — 动态网络切换,Safe 多签集成
  6. 生产化 — 性能优化、安全加固、Docker、CI/CD、监控

你现在拥有一个可以扩展的 Web3 全栈脚手架。未来你可以继续叠加:

  • ERC-721/NFT 的铸造与市场
  • ERC-4337 账户抽象 实现免 Gas 或社交恢复
  • Layer2 原生支持 (Arbitrum Nova, Base)
  • 去中心化存储 (IPFS/Arweave) 保存元数据
  • The Graph/Subsquid 替代自建索引器