# H5 小程序嵌入式应用开发模板 > 适用于嵌入微信小程序 web-view 的 H5 应用 > 技术栈:FastAPI + MongoDB (Beanie) + React 18 + React Router 7 + Vite 6 > 认证完全依赖小程序侧 JWT --- ## 一、项目架构 ### 1.1 技术栈 | 层 | 技术 | 备注 | |---|------|------| | 前端 | React 18 + React Router 7 + Vite 6 | SPA,独立 basename | | UI 库 | 自选(MUI / shadcn/ui / TailwindCSS) | | | 状态 | React Context + localStorage 持久化 | 无 Redux/Zustand | | 后端 | FastAPI + Uvicorn | | | 数据库 | MongoDB (Motor + Beanie ODM) | | | 认证 | JWT(外部传入,不验证签名) | python-jose | ### 1.2 目录结构 ``` project/ ├── frontend/ │ ├── src/app/ │ │ ├── pages/ # 页面组件 │ │ ├── components/ # 通用组件 │ │ ├── components/ui/ # 基础 UI 组件(shadcn/ui 风格) │ │ ├── context/ # React Context │ │ ├── services/ # API 服务 │ │ └── data/ # 静态数据 │ │ ├── routes.tsx # 路由配置 │ │ └── App.tsx # 根组件 │ ├── package.json │ └── dist/ # 构建产物 ├── backend/ │ ├── app/ │ │ ├── routers/ # API 路由模块 │ │ ├── models/ # Beanie 数据模型 │ │ ├── services/ # 业务逻辑层 │ │ ├── middleware/ # 认证中间件 │ │ └── config.py # 配置 │ ├── requirements.txt │ ├── serv.sh # 服务管理脚本 │ └── .env # 环境变量 └── docs/ └── development-standards.md ``` ### 1.3 多应用同域名部署架构 ``` 同一个 Nginx 下代理多个 H5 应用,每个应用独立后端进程: https://mp.yizhezhe.com/ ├── /app_03/ → 孕育无忧 H5 │ ├── → index.html(前端 SPA,basename=/app_03) │ └── /api/ → proxy_pass localhost:8030 ├── /baby_food/ → 婴儿辅食 H5 │ ├── → index.html(前端 SPA,basename=/baby_food) │ └── /api/ → proxy_pass localhost:8031 └── /album/ → 宝宝相册 H5 ├── → index.html(前端 SPA,basename=/album) └── /api/ → proxy_pass localhost:8032 ``` 每个应用是一个独立项目(独立 repo 或独立目录),拥有自己的: - 前端构建产物(`dist/`) - 后端服务进程(不同端口) - 数据库(不同 MongoDB database) - `.env` 和环境变量 ### 1.4 Nginx 配置模板 ```nginx server { listen 443 ssl; server_name mp.yizhezhe.com; # ─── 应用: app_03 ─── location = /app_03 { return 301 /app_03/; } location /app_03/ { alias /www/app_03/frontend/dist/; try_files $uri $uri/ /app_03/index.html; } location /app_03/api/ { rewrite ^/app_03(/api/.*)$ $1 break; proxy_pass http://127.0.0.1:8030; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # ─── 应用: baby_food ─── location = /baby_food { return 301 /baby_food/; } location /baby_food/ { alias /www/baby_food/frontend/dist/; try_files $uri $uri/ /baby_food/index.html; } location /baby_food/api/ { rewrite ^/baby_food(/api/.*)$ $1 break; proxy_pass http://127.0.0.1:8031; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } ``` > location = /xxx 必须带,否则 /app_03 无尾随斜杠时返回 404。 ### 1.5 新建应用步骤 1. 复制模板目录,重命名项目 2. 修改 `backend/app/config.py` → `app_name`, `mongodb_db_name`, `port` 3. 修改前端 basename(Vite `base` + React Router `basename`) 4. 定义数据模型(`backend/app/models/`) 5. 编写业务 API(`backend/app/routers/`) 6. 编写前端页面 + 路由 7. 添加 Nginx location 块 8. 启动验证 --- ## 二、认证规范 ### 2.1 认证流程 ``` 小程序 → {token(JWT)} → URL params → H5 LoginPage ↓ decodeToken(JWT) 提取 uid/openid/phone ↓ localStorage 存储 token + user_basic ↓ 后端 API 请求带 Authorization: Bearer ↓ get_current_user 解码 JWT → 按 openid 查找/创建用户 ``` ### 2.2 JWT 格式 **前端 decodeToken() 解析出的字段:** ```typescript interface TokenPayload { uid: number; // 小程序侧的用户 ID openid: string; // 微信 openid phone: string; // 手机号(可能为空) exp: number; // 过期时间戳 } ``` **后端自签 token(dev-login 兼容,同格式):** ```json { "uid": "...", "openid": "...", "phone": "...", "exp": ..., "iss": "localhost" } ``` ### 2.3 认证中间件规则 ```python # middleware/auth.py async def get_current_user(credentials: HTTPAuthorizationCredentials) -> User: payload = jwt.decode(token, None, options={"verify_signature": False, "verify_exp": False}) openid = payload.get("openid") # 必须存在 # 手动验证 exp 过期 user = await User.find_one(User.openid == openid) if not user: user = User(openid=openid, phone=phone or None) await user.insert() else: # 有 phone 且变化时更新 return user ``` - 不验证 JWT 签名(`verify_signature: False`),信任小程序侧 - 手动验证 `exp` 过期 - 提取 `openid` → 按 openid 查找/创建用户 - `create_access_token()` 保持原样,兼容旧逻辑 ### 2.4 DEV_MODE - 环境变量 `DEV_MODE=1` 启用开发模式 - 仅 `/api/auth/dev-login` 受此控制 - 测试账号固定 openid,在浏览器开发时自动登录 - 启动方式:`DEV_MODE=1 uvicorn app.main:app --port 8030 --reload` ### 2.5 前端登录流程(LoginPage.tsx 模板) ```typescript const params = new URLSearchParams(window.location.search); const tokenFromUrl = params.get('token'); if (tokenFromUrl) { const payload = decodeToken(tokenFromUrl); setTokenAndUser(tokenFromUrl, { id: String(payload.uid), openid: payload.openid, phone: payload.phone, }); // 拉取用户数据 → 跳转首页或引导页 fetchProfile().then((dto) => { navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); }); } else if (isWechatMiniProgram()) { // 通知小程序传递 token window.wx?.miniProgram?.postMessage({ type: "NEED_TOKEN" }); } else { // 浏览器开发环境 → dev-login devLogin().then(res => setTokenAndUser(res.token, { ... })); } ``` --- ## 三、前端规范 ### 3.1 API 调用 ```typescript // services/api.ts const TOKEN_KEY = "auth_token"; export function getToken(): string | null { return localStorage.getItem(TOKEN_KEY); } export function setToken(token: string) { localStorage.setItem(TOKEN_KEY, token); } export function clearToken() { localStorage.removeItem(TOKEN_KEY); } /** 通用 API 请求封装 */ async function fetchApi(path: string, options: RequestInit = {}): Promise { const token = getToken(); const headers: Record = { "Content-Type": "application/json" }; if (token) headers["Authorization"] = `Bearer ${token}`; const res = await fetch(apiPath(path), { ...options, headers }); if (res.status === 401) { clearToken(); window.location.href = APP_BASE_URL; } if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `请求失败 (${res.status})`); } return res.json(); } ``` **新增 API 的步骤:** 1. 在 `api.ts` 定义接口(`export interface XxxResponse`) 2. 定义函数(`export async function xxx(): Promise`) 3. 在页面中 import 调用 ### 3.2 路由与守卫 ```typescript // routes.tsx function ProtectedRoute() { const token = getToken(); const urlToken = new URLSearchParams(window.location.search).get('token'); if (urlToken) { return ; } if (!token) { return ; } return ; } ``` 路由结构: ``` / → LoginPage(无保护) /onboarding → 引导页(登录后首次使用) /home → 首页(保护) /xxx → 其他业务页面(保护) ``` ### 3.3 状态管理(Context 模板) ```typescript // context/UserContext.tsx export interface BasicUserInfo { id: string; openid: string; phone: string; } interface UserContextValue { user: UserProfile; // 由具体业务定义 token: string | null; setTokenAndUser: (token: string, info: BasicUserInfo) => void; fetchProfile: () => Promise; logout: () => void; // + 业务自定义方法 } ``` - `BasicUserInfo` (`id`/`openid`/`phone`) 持久化到 `localStorage['user_basic']` - 用户业务数据持久化到各自 key - 切换用户时自动清理旧数据 ### 3.4 组件规范 - 页面组件统一放在 `pages/`,命名 `XxxPage.tsx` - 通用业务组件在 `components/` - 基础 UI 组件在 `components/ui/`(shadcn/ui 风格,按需引入) - 函数式组件 + Hooks,class 组件禁用 - `react-hooks/exhaustive-deps` 可 eslint-disable(mount-once 模式) - 动画推荐 `framer-motion`(包名 `motion`),图标推荐 `lucide-react` ### 3.5 编码风格 - 中文注释 - `// ─── Section ───` 大段视觉分隔 - 不需要过度的错误处理,完整 traceback 就是调试信息 - 复杂逻辑封装到工具类/服务层,页面保持精简 ### 3.6 前端路径规范(多应用部署) 每个 H5 应用的 basename 由两部分确定: | 配置项 | 位置 | 值 | |--------|------|----| | Vite `base` | `frontend/vite.config.ts` | `'/app_03/'` | | React Router `basename` | `routes.tsx` → `createBrowserRouter(routes, {basename: '/app_03'})` | `'/app_03'` | | fetch API 前缀 | `api.ts` → `APP_BASE_URL` | `'/app_03'` | **basename 命名规范:** `/{项目标识}`,全小写 + 下划线(`snake_case`)。 **配置示例(vite.config.ts):** ```typescript export default defineConfig({ base: '/app_03/', // 构建产物路径前缀 plugins: [react()], server: { port: 5173 }, }) ``` **配置示例(routes.tsx):** ```typescript const router = createBrowserRouter(routes, { basename: '/app_03', }); ``` **api.ts 中的 base URL 自动拼接:** ```typescript const API_PREFIX = import.meta.env.VITE_API_PREFIX || "/api"; // APP_BASE_URL = basename = 从 API_PREFIX 提取("/app_03/api" → "/app_03") const APP_BASE_URL = API_PREFIX.replace(/\/api\/?$/, '') || '/'; function joinPrefix(prefix: string, path: string) { const p = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix; const s = path.startsWith('/') ? path : `/${path}`; return `${p}${s}`; } // 最终请求: /app_03/api/xxx ``` **.env 配置:** ```ini # API_PREFIX 必须带 basename,开发和 Nginx 代理一致 VITE_API_PREFIX=/app_03/api ``` **Vite proxy 配置(开发环境):** ```typescript // vite.config.ts server: { proxy: { [apiPrefix]: { // "/app_03/api" target: 'http://localhost:8030', changeOrigin: true, rewrite: (path) => path.replace(/^\/app_03\/api/, '/api'), }, }, } ``` --- ## 四、后端规范 ### 4.1 路由与 API 前缀规范 每个应用的 API 通过 Nginx 统一代理,路径结构为 `/{basename}/api/*`。 | 应用 | 前端路径 | API 路径 | 后端端口 | |------|---------|---------|---------| | 孕育无忧 | `/app_03/` | `/app_03/api/*` | 8030 | | 婴儿辅食 | `/baby_food/` | `/baby_food/api/*` | 8031 | | 宝宝相册 | `/album/` | `/album/api/*` | 8032 | **后端路由注册:** ```python # config.py api_prefix: str = "/api" # 后端内部的 API 前缀,与前端 APP_BASE_URL 一致 # routers/__init__.py — 统一注册 router = APIRouter(prefix="/api") router.include_router(auth.router) # auth.router 前缀: /api/auth/xxx router.include_router(user.router) # user.router 前缀: /api/user/xxx # 单个路由模块定义(routers/user.py) router = APIRouter(prefix="/api/user", tags=["user"]) ``` > Nginx 的 `rewrite ^/app_03(/api/.*)$ $1 break` 会将 `/app_03/api/xxx` 剥离为 `/api/xxx` 转发到后端,所以后端不感知外部路径前缀。 **路由文件结构:** ```python routers/ ├── __init__.py # 统一注册所有路由 ├── auth.py # 认证:exchange-code, dev-login ├── user.py # 用户 CRUD └── *.py # 按业务模块拆分 ``` ### 4.2 数据库命名规范 ```python # config.py mongodb_db_name: str = "app_03" # 数据库名 = 应用标识,与 basename 一致 ``` | 应用 | 数据库名 | 说明 | |------|---------|------| | 孕育无忧 | `app_03` | 应用目录名 / 项目名 | | 婴儿辅食 | `baby_food` | 全小写 snake_case | | 宝宝相册 | `album` | 简短标识 | **规范:** - 全小写 + 下划线(`snake_case`) - 与前端 basename 保持一致(去掉首斜杠) - 与项目目录名保持一致 - 避免用复数(`users` → ❌, `app_03` → ✅) - 同一 MongoDB 实例下的所有应用数据库平级,无交叉数据 **models 目录结构(每个应用独立):** ```python models/ ├── __init__.py # 导出所有模型 └── user.py # User Document # 业务模型按需新增文件,不跨应用复用 ``` ### 4.3 数据模型(Beanie ODM 模板) ```python class User(Document): openid: str = Field(..., unique=True) # 微信 openid,用户唯一标识 session_key: Optional[str] = None union_id: Optional[str] = None name: str = "" phone: Optional[str] = None avatar_url: Optional[str] = None is_onboarded: bool = False # 是否完成引导 created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) ``` > **注意:** 业务模型按需新增,User 仅作为认证底座,业务数据可新建 Document 或嵌入 User。 ### 4.4 API 响应格式 ```python # 成功 — 直接返回 Pydantic Model 或 dict # 错误 — HTTPException(status_code, detail="中文描述") ``` ### 4.5 服务管理 (serv.sh) ```bash # .env 中配置: APP_NAME=app_name APP_MODULE=app.main:app HOST=0.0.0.0 PORT=8000 DEV_MODE=1 # 命令: ./serv.sh start [--reload] # 启动 ./serv.sh stop # 停止 ./serv.sh restart [--reload] # 重启 ./serv.sh status # 状态 ./serv.sh logs [YYYY-MM-DD] # 日志 ``` --- ## 五、代码模板 ### 5.1 新增业务页面 ```typescript import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; import { useUser } from "../context/UserContext"; import { fetchApi } from "../services/api"; export function XxxPage() { const navigate = useNavigate(); const { user, token } = useUser(); const [data, setData] = useState(null); const [error, setError] = useState(""); useEffect(() => { fetchApi("/xxx").then(setData).catch(e => setError(e.message)); }, []); if (error) return
{error}
; if (!data) return
加载中...
; return
{/* 业务内容 */}
; } ``` ### 5.2 新增 API 接口 ```typescript // api.ts export interface XxxDTO { id: string; // ... 业务字段 } export async function getXxxList(): Promise { return fetchApi("/xxx"); } export async function createXxx(data: Record): Promise { return fetchApi("/xxx", { method: "POST", body: JSON.stringify(data) }); } ``` ### 5.3 后端路由模板 ```python from fastapi import APIRouter, Depends from app.middleware.auth import get_current_user from app.models.user import User router = APIRouter(prefix="/api/xxx", tags=["xxx"]) @router.get("/") async def list_items(user: User = Depends(get_current_user)): # user.openid / user.id 可用 return {"items": [...]} @router.post("/") async def create_item(data: ..., user: User = Depends(get_current_user)): return {"id": "...", ...} ``` --- ## 六、安全注意事项 1. JWT 不验证签名 — 信任链依赖于小程序的环境隔离,H5 本身不做额外验证 2. DEV_MODE 仅控制 dev-login — 生产环境不设此变量 3. CORS 配置 `cors_origins: *` — 开发环境宽松,生产应限制 4. 微信 session_key 存入数据库但不对外暴露 --- ## 七、环境变量 (.env 模板) ```ini # MongoDB MONGODB_URL=mongodb://localhost:27017 MONGODB_DB_NAME=app_name # JWT JWT_SECRET_KEY=change-this-secret-key JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=10080 # WeChat WECHAT_APP_ID= WECHAT_APP_SECRET= # Server HOST=0.0.0.0 PORT=8000 DEBUG=true DEV_MODE=1 # CORS CORS_ORIGINS=* ``` --- ## 八、初始化新应用 Checklist - [ ] 定义项目标识(`app_03`/`baby_food`/`album` 等),保持一致 - [ ] 复制模板 → 修改 `backend/app/config.py` → `app_name`, `mongodb_db_name`, `port` - [ ] 修改前端 basename:Vite `base` + React Router `basename` + `api.ts` 的 `APP_BASE_URL` - [ ] 设置 WeChat AppID/Secret(生产环境) - [ ] 更换 `JWT_SECRET_KEY` - [ ] 定义业务数据模型(`backend/app/models/`) - [ ] 编写业务 API 路由(`backend/app/routers/`) - [ ] 创建前端页面 + 路由配置 - [ ] 添加 Nginx location 块(前端 SPA + API 代理) - [ ] 启动验证:`DEV_MODE=1` → 浏览器访问 → dev-login → 首页