前端常见技术方案
面向 React、Vue、H5、微信小程序、支付宝小程序和 uni-app 项目的工程实践手册。
本文关注“机制、边界、生产风险和选型规则”,不绑定单一框架。示例默认采用 TypeScript 严格模式。
目录
- 1. 技术方案的判断框架
- 2. 状态与数据源设计
- 3. 草稿系统
- 4. 网络请求层
- 5. 服务端状态与缓存
- 6. 登录、认证与权限
- 7. 表单与交互
- 8. 列表、路由与页面恢复
- 9. 文件上传
- 10. 离线能力与 PWA
- 11. 配置、灰度与多端适配
- 12. 性能方案
- 13. 监控、埋点与稳定性
- 14. 跨页面与跨标签页通信
- 15. 安全方案
- 16. 测试与质量门禁
- 17. 常见反模式
- 18. 项目落地顺序
- 19. 生产检查清单
- 20. 参考资料
1. 技术方案的判断框架
前端技术方案不是 API 清单,而是对以下问题的系统回答:
- 谁是权威数据源:组件内存、URL、本地存储还是服务端?
- 数据什么时候失效:按时间、按事件、按版本还是手动失效?
- 异常后怎么恢复:重试、回滚、降级、离线队列还是提示用户?
- 并发时谁覆盖谁:最后写入覆盖、乐观锁、字段合并还是协同算法?
- 页面销毁后任务怎么办:取消、忽略结果、继续缓存还是转入后台?
- 多端是否同构:H5、小程序和 App 的生命周期、存储和网络能力是否一致?
- 安全边界在哪里:哪些校验只是交互优化,哪些必须由服务端保证?
1.1 方案分级
| 等级 | 典型实现 | 适用场景 |
|---|---|---|
| 页面级 | 组件状态、简单 loading、防抖 | 单页面、低风险、短生命周期 |
| 应用级 | 请求层、Store、统一错误处理、缓存 | 中型业务应用 |
| 业务级 | 草稿、权限、上传、幂等、状态机 | 商品、订单、合同、内容发布 |
| 平台级 | 监控、灰度、离线、配置中心、适配层 | 多业务、多端、长期演进项目 |
1.2 选型原则
- 先确认失败成本,再决定方案复杂度。
- 前端体验优化不能替代服务端一致性和安全控制。
- 优先选择可观测、可取消、可重试、可回滚的实现。
- 不为几十条数据引入虚拟列表,不为普通图片引入分片上传。
- 不把所有状态放进全局 Store,也不让每个页面自行封装请求。
2. 状态与数据源设计
2.1 状态分类
| 状态类型 | 推荐位置 | 示例 |
|---|---|---|
| 组件局部状态 | 组件内部 | 弹窗开关、输入框临时值 |
| 跨组件 UI 状态 | 最近公共父组件或轻量 Store | 当前 Tab、筛选面板开关 |
| 可分享、可刷新恢复状态 | URL | 搜索词、分页、排序、筛选条件 |
| 服务端业务数据 | 服务端状态缓存 | 用户信息、商品详情、订单列表 |
| 长期用户偏好 | 本地存储 | 主题、列表密度、最近选择 |
| 未完成业务数据 | 草稿系统 | 商品编辑、文章编辑、复杂表单 |
| 安全与权限状态 | 服务端为权威,前端只做展示 | 角色、权限、数据范围 |
2.2 单一权威数据源
同一个字段不要同时由以下位置共同决定:
- 组件内部状态
- 全局 Store
- URL Query
- 本地存储
- 接口响应
推荐明确单向流:
权威数据源
↓
派生状态
↓
页面展示
↓
用户操作
↓
更新权威数据源
2.3 服务端状态与客户端状态的边界
服务端状态具有以下特征:
- 归服务端所有,前端持有的是快照。
- 可能被其他用户、设备或后台任务修改。
- 存在过期、重新获取、并发和同步问题。
客户端状态通常只描述当前设备的交互过程,例如弹窗、输入焦点、临时筛选面板。不要用通用 Store 手写一套不完整的服务端缓存系统。
3. 草稿系统
草稿系统的核心是:
将尚未正式提交的数据按稳定业务标识暂存,再次进入时恢复,正式提交后清理。
3.1 三种方案
| 方案 | 优点 | 局限 | 适用场景 |
|---|---|---|---|
| 本地草稿 | 快、简单、弱网可用 | 不能跨设备,清缓存后可能丢失 | 简单表单、匿名用户 |
| 服务端草稿 | 跨设备、可长期保留 | 依赖网络,需要处理冲突 | 文章、商品、简历、合同 |
| 混合草稿 | 本地兜底、服务端同步 | 状态和冲突处理更复杂 | 正式生产业务优先选择 |
3.2 标准流程
3.3 数据结构
type DraftStatus = 'local' | 'syncing' | 'synced' | 'failed' | 'submitted';
interface Draft<TData> {
draftKey: string;
draftId?: string;
bizType: string;
bizId?: string;
data: TData;
version: number;
schemaVersion: number;
updatedAt: number;
status: DraftStatus;
}
草稿键至少应隔离用户和业务:
interface DraftKeyParams {
userId: string;
businessType: string;
businessId?: string;
}
/**
* 生成用户和业务维度唯一的草稿标识。
*/
function createDraftKey(params: DraftKeyParams): string {
const targetId = params.businessId ?? 'create';
return `draft:${params.userId}:${params.businessType}:${targetId}`;
}
多租户项目还要加入 tenantId。匿名用户可使用设备级临时 ID,登录后再执行草稿归属迁移。
3.4 保存时机
- 普通表单:停止输入
500~1500ms后保存。 - 富文本:停止输入
1000~3000ms后保存,或每10~30s周期保存。 - 页面隐藏、路由离开、应用进入后台时补保存一次。
- 不能只依赖退出生命周期,因为进程被杀死时异步任务不一定完成。
3.5 恢复策略
进入页面时:
读取正式数据
↓
读取本地和服务端草稿
↓
比较 bizId、schemaVersion、version 和更新时间
↓
自动恢复或询问用户
低风险内容可自动恢复并提示;编辑已有业务数据时,优先询问:
检测到较新的草稿,是否恢复?
可提供:恢复、放弃、查看差异。
3.6 并发冲突
只按客户端时间进行最后写入覆盖存在风险,因为设备时间可能不一致。推荐服务端乐观锁:
PUT /api/drafts/123
If-Match: 10
服务端规则:
请求 version = 数据库 version → 更新成功,version + 1
请求 version ≠ 数据库 version → 返回 409 Conflict
冲突处理按复杂度选择:
- 简单表单:提示选择本地版或服务端版。
- 字段独立表单:按字段合并。
- 富文本:展示版本差异。
- 实时多人协同:使用 OT 或 CRDT,不再属于普通草稿方案。
3.7 提交竞态
危险流程:
自动保存请求发出
↓
正式提交成功并删除草稿
↓
旧自动保存请求返回
↓
草稿被重新创建
提交前应:
- 取消防抖任务。
- 中止或忽略在途保存请求。
- 将状态置为
submitted。 - 服务端拒绝继续更新已提交草稿。
- 正式提交成功后再清除本地数据。
3.8 附件草稿
不要在 Storage 中保存大体积 Base64。推荐先上传临时文件,草稿只保存 fileId:
type UploadStatus = 'uploading' | 'uploaded' | 'failed';
interface DraftAttachment {
fileId: string;
name: string;
size: number;
uploadStatus: UploadStatus;
}
服务端定期清理:
- 已过期草稿。
- 草稿关联的临时文件。
- 上传成功但没有绑定业务或草稿的孤立文件。
3.9 草稿仓储抽象
interface DraftRepository<TData> {
get(draftKey: string): Promise<Draft<TData> | null>;
save(draft: Draft<TData>): Promise<void>;
remove(draftKey: string): Promise<void>;
}
可分别实现:
WebDraftRepositoryWechatDraftRepositoryAlipayDraftRepositoryUniAppDraftRepositoryRemoteDraftRepositoryHybridDraftRepository
3.10 草稿反模式
- 打开空页面就创建空草稿。
- 不区分账号,导致账号之间串数据。
- 直接保存密码、验证码、银行卡等敏感字段。
- 没有过期机制和结构版本迁移。
- 正式提交后不清理草稿。
- 本地和服务端只按
updatedAt粗暴覆盖。
4. 网络请求层
4.1 分层结构
页面 / 组件
↓
业务 Service
↓
Request Client
↓
fetch / axios / wx.request / my.request / uni.request
页面不应散落原生请求调用。
interface ApiResponse<TData> {
code: number;
message: string;
data: TData;
requestId?: string;
}
interface RequestOptions<TBody = unknown> {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string;
body?: TBody;
signal?: AbortSignal;
timeoutMs?: number;
}
统一请求层通常负责:
- Base URL 和环境配置。
- Token 注入和刷新。
- 参数序列化。
- 超时与取消。
- 响应结构归一化。
- 业务错误映射。
- 请求 ID 和日志。
- 重试与幂等策略。
4.2 请求竞态
搜索、筛选和路由快速切换时,旧请求可能晚于新请求返回。常见处理方式:
- 使用
AbortController取消旧请求。 - 使用递增请求序号,只接收最新响应。
- 让服务端状态库按 Query Key 管理请求。
let currentController: AbortController | null = null;
/**
* 发起搜索并取消上一轮未完成请求。
*/
async function search(keyword: string): Promise<SearchResult> {
currentController?.abort();
currentController = new AbortController();
return searchService.search(keyword, {
signal: currentController.signal,
});
}
AbortSignal 被中止后不能复用。取消请求时要区分 AbortError、超时和真实网络错误,避免把用户主动取消显示成故障。
4.3 超时
超时不是简单的 Promise.race。如果底层请求仍在执行,会继续占用连接并可能产生副作用。优先使用真正传递到请求层的 AbortSignal。
/**
* 创建带超时的请求信号。
*/
function createTimeoutSignal(timeoutMs: number): AbortSignal {
return AbortSignal.timeout(timeoutMs);
}
生产环境需要评估 AbortSignal.timeout() 和 AbortSignal.any() 的目标平台兼容性;旧浏览器或部分 WebView 可使用 AbortController + setTimeout 回退。
4.4 防重复提交与幂等
前端措施:
- 点击后立即进入 loading。
- 禁用重复交互。
- 同业务 Key 的请求使用互斥锁。
- 页面卸载时取消非关键请求。
服务端措施:
idempotencyKey。- 唯一业务流水号。
- 数据库唯一约束。
- 状态机校验。
前端防抖不能替代服务端幂等,尤其是支付、下单、发券和状态流转。
4.5 重试
只重试满足以下条件的请求:
- 网络瞬时失败。
- 网关超时。
- 服务端明确允许重试。
- 操作本身具备幂等性。
不要默认重试:
- 参数错误。
- 权限错误。
- 业务校验错误。
- 非幂等创建操作。
推荐指数退避并加入随机抖动:
1s → 2s → 4s → 8s
4.6 错误分类
type ErrorCategory =
| 'network'
| 'timeout'
| 'cancelled'
| 'authentication'
| 'authorization'
| 'validation'
| 'business'
| 'server'
| 'unknown';
错误展示应区分:
- 用户可修复:展示字段提示或操作建议。
- 可自动恢复:后台重试或降级。
- 不可恢复:显示错误页和请求 ID。
- 用户主动取消:通常不展示错误 Toast。
5. 服务端状态与缓存
5.1 缓存不是“存过就不请求”
服务端状态缓存至少需要描述:
- Query Key。
- 新鲜时间。
- 未使用缓存保留时间。
- 失效事件。
- 重新获取触发条件。
- 并发请求去重。
- 请求取消。
- 错误和重试策略。
5.2 staleTime 与 gcTime
以 TanStack Query v5 为例:
staleTime:数据在多长时间内被认为是新鲜的。gcTime:Query 无观察者后,未使用缓存保留多久再回收。- 二者不是同一个概念;延长
gcTime不代表数据始终新鲜。
当前 v5 文档和实现中,staleTime 默认是 0;客户端 gcTime 默认是 5 分钟,服务端环境默认为 Infinity。项目应根据数据变化频率显式配置,而不是依赖默认值。
| 数据 | 建议策略 |
|---|---|
| 国家、省市、静态字典 | 长 staleTime,版本变化时主动失效 |
| 用户个人信息 | 中等 staleTime,修改后失效 |
| 商品库存、价格 | 短 staleTime 或关键节点强制刷新 |
| 支付结果 | 不依赖普通缓存作为最终依据 |
| 实时消息 | WebSocket / SSE 推送结合缓存更新 |
5.3 Query Key 设计
const productQueryKeys = {
all: ['products'] as const,
lists: () => [...productQueryKeys.all, 'list'] as const,
list: (filters: ProductFilters) =>
[...productQueryKeys.lists(), filters] as const,
detail: (productId: string) =>
[...productQueryKeys.all, 'detail', productId] as const,
};
Query Key 必须包含影响响应结果的全部输入。不要将对象中不稳定、无关或不可序列化的值混入 Key。
5.4 主动失效
Mutation 成功后通常有三种策略:
- 使用响应结果精确更新缓存。
- 使相关 Query 失效并重新获取。
- 同时更新列表和详情缓存。
失效不是清空所有缓存。应按业务关联范围精确失效,避免一次修改导致全站请求风暴。
5.5 请求去重与取消
同一个 Query Key 的并发读取应共享请求结果。组件卸载时是否取消请求,需要结合缓存价值判断:
- 请求结果对后续页面仍有价值,可让请求完成并写入缓存。
- 搜索、路由切换等旧结果无价值,应消费并传递
AbortSignal,让底层真正取消。
TanStack Query v5 中,如果 Query Function 消费了 AbortSignal,最后一个观察者移除后可以取消请求并回退状态;如果未消费 Signal,在途请求可能继续完成并写入缓存。工程上不能只“接收 signal 参数”,必须真正传给 fetch 或底层 Client。
5.6 乐观更新
取消相关查询
↓
保存旧缓存快照
↓
立即修改缓存
↓
请求成功:提交结果或重新校验
请求失败:回滚快照
↓
最终使相关查询失效
适合:点赞、收藏、简单开关、拖拽排序。
不适合直接作为最终依据:支付、库存扣减、金额结算、权限变更。
5.7 持久化缓存
持久化缓存可以加快冷启动和弱网恢复,但要解决:
- 用户隔离。
- 缓存版本。
- 过期时间。
- 退出登录清理。
- 敏感字段脱敏。
- 数据结构迁移。
- 服务端新旧版本兼容。
不要将“恢复本地缓存”理解为“数据仍然有效”。恢复后仍应根据新鲜度和业务要求进行后台校验。
6. 登录、认证与权限
6.1 Token 自动续期
业务请求返回 401
↓
检查是否已有 refreshPromise
├─ 有:加入等待队列
└─ 无:发起刷新 Token
↓
刷新成功:重放原请求
刷新失败:清理登录态并跳转登录
let refreshPromise: Promise<string> | null = null;
关键风险:
- 多请求同时刷新 Token。
- 刷新接口自身进入 401 循环。
- 重放非幂等请求导致重复提交。
- 退出登录后旧请求又写回用户数据。
- 多标签页同时刷新和覆盖 Token。
6.2 权限分层
| 层级 | 前端职责 | 安全边界 |
|---|---|---|
| 页面级 | 控制路由进入和菜单展示 | 服务端仍需校验 |
| 功能级 | 控制按钮和操作入口 | 服务端仍需校验 |
| 数据级 | 只做展示提示 | 必须由服务端控制 |
前端权限控制主要服务于用户体验,不是安全边界。
6.3 登录后返回原页面
保存站内路由、Query 和过期时间:
interface LoginRedirect {
path: string;
query: Record<string, string>;
expiresAt: number;
}
只允许跳转到站内白名单路由,避免开放重定向漏洞。支付参数、Token 等敏感信息不要直接放进返回 URL。
6.4 退出登录清理
至少清理:
- Access Token 和 Refresh Token。
- 用户级服务端状态缓存。
- 本地用户资料。
- 用户草稿的当前可见引用。
- WebSocket 连接。
- 权限和菜单状态。
- 等待重放的请求队列。
7. 表单与交互
7.1 表单状态模型
interface FormState<TValues extends object> {
initialValues: TValues;
values: TValues;
errors: Partial<Record<keyof TValues, string>>;
touched: Partial<Record<keyof TValues, boolean>>;
dirty: boolean;
submitting: boolean;
}
复杂表单需要明确:初始值、当前值、校验结果、脏状态、提交状态和草稿状态。
7.2 校验分层
- 输入级:格式、长度、必填。
- 表单级:字段之间的关系。
- 异步级:用户名重复、优惠券有效性。
- 服务端级:最终业务和安全校验。
异步校验要处理防抖、取消和竞态。
7.3 防抖与节流
| 方案 | 行为 | 适用场景 |
|---|---|---|
| 防抖 | 停止一段时间后执行一次 | 搜索、草稿、异步校验 |
| 节流 | 固定时间内最多执行一次 | 滚动、拖拽、手势、曝光 |
提交类操作还需要 loading 锁和服务端幂等,不能只使用防抖。
7.4 未保存离开提醒
判断标准应是“当前值是否不同于初始值”,而不是“表单是否非空”。
Web 的 beforeunload 存在限制:
- 浏览器通常只显示系统统一文案。
- 不保证所有移动端和进程终止场景触发。
- 不适合作为唯一保存机制。
- 仅在确实存在未保存内容时注册,避免影响返回缓存和用户体验。
框架路由离开守卫负责站内导航;自动草稿负责数据安全;beforeunload 只做补充。
7.5 分步表单
长流程推荐第一步创建服务端草稿并获得 draftId:
创建 draftId
↓
基本信息
↓
详细配置
↓
附件上传
↓
确认提交
每一步保存独立校验结果,最终提交时由服务端重新做完整校验。
7.6 弹窗统一管理
弹窗队列需要处理:
- 优先级。
- 去重。
- 同一时间只显示一个。
- 是否允许中断。
- 页面切换后是否继续。
- 展示频率限制。
不要让登录、协议、权限、活动弹窗同时叠加。
8. 列表、路由与页面恢复
8.1 页码与游标分页
| 方案 | 优点 | 局限 |
|---|---|---|
| 页码分页 | 支持跳页,实现直观 | 数据变化时可能重复或遗漏 |
| 游标分页 | 适合持续变化的数据 | 不适合任意跳页 |
信息流、聊天记录、动态列表优先使用稳定游标。
interface ListState<TItem> {
items: TItem[];
loading: boolean;
refreshing: boolean;
finished: boolean;
error: string | null;
nextCursor: string | null;
}
8.2 列表状态机
至少区分:
- 首次加载。
- 下拉刷新。
- 加载更多。
- 空状态。
- 首次加载失败。
- 加载更多失败。
- 已加载全部。
一个 loading 布尔值无法准确描述这些状态。
8.3 页面状态恢复
列表进入详情再返回时,可恢复:
- 滚动位置。
- 筛选条件。
- 搜索词。
- 当前分页和数据。
- Tab 位置。
- 被查看或修改的目标项。
存储规则:
可分享状态 → URL
短期页面快照 → 路由缓存 / Store
长期用户偏好 → 本地存储
权威业务状态 → 服务端
8.4 虚拟列表
虚拟列表只渲染视口附近元素,适合数千条以上或 DOM 成本很高的列表。
生产难点:
- 不定高元素。
- 图片加载后高度变化。
- 吸顶区域。
- 滚动锚点。
- 无限分页。
- 动态插入和删除。
几十条普通列表不要过早引入虚拟化。
9. 文件上传
9.1 上传状态机
type FileUploadStatus =
| 'pending'
| 'uploading'
| 'success'
| 'failed'
| 'cancelled';
interface UploadFile {
id: string;
name: string;
size: number;
localUrl: string;
remoteUrl?: string;
fileId?: string;
progress: number;
status: FileUploadStatus;
}
流程:
选择文件
↓
类型、大小、数量、尺寸校验
↓
本地预览
↓
上传
├─ 成功
├─ 失败重试
└─ 用户取消
9.2 本地路径与远程文件
必须区分:
- 本地临时路径。
- 远程临时文件 ID。
- 正式文件 ID。
- CDN URL。
- 上传成功但业务未提交的孤立文件。
不要把本地临时路径提交给服务端当永久地址。
9.3 并发控制
多文件上传建议限制并发数,避免:
- 占满浏览器连接。
- 小程序端内存和网络压力过大。
- 进度频繁更新导致页面抖动。
- 单个失败拖垮整个批次。
9.4 分片上传
适合视频和大文件:
计算文件标识
↓
切分分片
↓
查询已上传分片
↓
受控并发上传
↓
失败分片重试
↓
服务端校验并合并
小图片和普通附件不需要分片。
9.5 前端文件校验边界
前端可以检查扩展名、MIME、大小和尺寸,但服务端必须重新验证真实文件内容。前端校验只是提前反馈,不是安全控制。
10. 离线能力与 PWA
10.1 离线能力分层
| 层级 | 能力 |
|---|---|
| 静态壳离线 | 页面基础资源可打开 |
| 只读缓存 | 展示最近一次成功数据 |
| 离线草稿 | 无网时继续编辑 |
| 离线队列 | 网络恢复后补发操作 |
| 冲突同步 | 处理本地与服务端并发修改 |
不要一开始就承诺“完全离线可用”,应明确具体支持到哪一层。
10.2 Precache 与 Runtime Cache
- Precache:构建期已知的应用壳资源,例如 HTML、JS、CSS 和图标。
- Runtime Cache:运行时遇到的图片、接口或第三方资源。
Workbox 的 Precache 在 Service Worker 安装阶段拉取清单资源;关键资源获取或写缓存失败可能导致本次 Service Worker 安装失败。清单应保持可控,不要将大量非关键资源全部预缓存。
10.3 缓存策略
| 策略 | 机制 | 适用场景 | 风险 |
|---|---|---|---|
| Cache First | 先缓存,未命中再网络 | 带 Hash 静态资源、图片 | 需要过期和容量控制 |
| Network First | 先网络,失败再缓存 | 页面导航、较新业务数据 | 弱网等待时间较长 |
| Stale While Revalidate | 先返回缓存,后台更新 | 可短暂陈旧的列表和配置 | 用户可能看到旧数据 |
| Network Only | 只访问网络 | 支付、敏感写操作 | 离线不可用 |
| Cache Only | 只使用缓存 | 完全预置资源 | 缓存缺失直接失败 |
10.4 离线任务队列
interface OfflineTask<TPayload> {
id: string;
type: string;
payload: TPayload;
idempotencyKey: string;
retryCount: number;
maxRetryCount: number;
createdAt: number;
nextRetryAt: number;
}
流程:
操作失败或离线
↓
写入持久队列
↓
网络恢复 / 应用重新激活
↓
校验登录态和任务有效性
↓
按顺序或依赖关系重试
↓
成功删除,失败退避或转人工处理
Background Sync 可以辅助延后发送任务,但浏览器支持、调度时机和系统策略存在差异,不能作为唯一保证。关键业务仍需要应用启动后的主动补偿流程。
10.5 Service Worker 更新
需要考虑:
- 新旧页面可能同时由不同 Service Worker 控制。
- 立即
skipWaiting可能让运行中的页面加载到不匹配的资源版本。 - 更新提示应允许用户在安全节点刷新。
- API 响应缓存必须按用户隔离,并在退出登录时清理。
- Cache Storage 不是永久数据库,需要过期、数量和容量控制。
10.6 Web、小程序和 App 的差异
- H5:可使用 Service Worker、Cache Storage、IndexedDB,但受浏览器策略影响。
- 微信和支付宝小程序:没有标准浏览器 Service Worker,应使用各自 Storage、文件系统、网络监听和生命周期。
- App:可使用 SQLite、文件系统和原生后台能力,但 iOS、Android 对后台执行限制不同。
- uni-app:统一 API 不等于底层能力完全一致,必须做条件编译和真机验证。
11. 配置、灰度与多端适配
11.1 动态配置与功能开关
interface FeatureFlags {
draftAutoSaveEnabled: boolean;
newCheckoutEnabled: boolean;
maxUploadCount: number;
}
配置系统需要:
- 本地安全默认值。
- 配置版本。
- 缓存时间。
- 拉取失败降级。
- 生效范围。
- 审计和回滚。
支付、权限和数据访问不能只依赖前端开关。
11.2 稳定灰度
灰度分组应基于稳定标识:
hash(userId + experimentId) % 100 < percentage
不要每次刷新随机分组,否则用户会在新旧版本之间跳动,数据也无法可靠分析。
11.3 多环境配置
常见环境:
development → test → staging → production
前端环境变量最终会进入构建产物,因此不能存储 AppSecret、数据库密码和真正的私密 Token。
11.4 平台适配层
interface PlatformAdapter {
login(): Promise<string>;
chooseImage(): Promise<string[]>;
getLocation(): Promise<{
latitude: number;
longitude: number;
}>;
}
业务层依赖统一接口,各平台实现细节隔离在 Adapter 中:
WechatPlatformAdapterAlipayPlatformAdapterH5PlatformAdapterAppPlatformAdapter
不要让 wx.*、my.* 和条件编译散落在业务页面。
12. 性能方案
12.1 性能治理顺序
- 先测量真实瓶颈。
- 优先减少资源体积和请求数量。
- 再减少主线程计算和 DOM 成本。
- 最后考虑复杂缓存和渲染优化。
12.2 常见方案
- 路由和组件按需加载。
- 图片格式、尺寸、压缩和 CDN 裁剪。
- 首屏资源优先级控制。
- 非首屏图片懒加载。
- 长任务拆分或使用 Web Worker。
- 稳定缓存和内容 Hash。
- 虚拟列表。
- 减少无意义的响应式依赖和重复渲染。
- SSR、SSG、流式渲染按业务选择。
12.3 预加载边界
预加载适合高概率下一步访问的资源。无差别预加载会:
- 抢占首屏带宽。
- 浪费移动流量。
- 增加内存和缓存压力。
- 降低关键请求优先级。
12.4 骨架屏边界
骨架屏用于降低等待感知,不会真正提升接口速度。数据很快时,短暂闪烁的骨架屏反而会降低体验,可设置最短展示延迟。
13. 监控、埋点与稳定性
13.1 错误监控
应采集:
- JavaScript 运行时异常。
- Promise 未处理异常。
- React / Vue 组件异常。
- 接口错误和超时。
- 资源加载失败。
- 白屏和渲染失败。
- 关键性能指标。
- 用户关键操作链路。
interface ErrorReport {
message: string;
stack?: string;
page: string;
userId?: string;
appVersion: string;
platform: string;
requestId?: string;
occurredAt: number;
}
13.2 上报治理
- 敏感字段脱敏。
- 错误指纹聚合。
- 采样和限流。
- 批量上报。
- 离线队列。
- Source Map 安全上传。
- 版本、环境、用户和请求链路关联。
13.3 白屏检测
可组合:
- 根节点有效内容检测。
- 关键节点超时检测。
- 路由完成后 DOM 检测。
- 首屏性能超时。
- 用户反馈入口。
需要排除合法空状态、骨架屏、Loading 和权限拦截页。
13.4 埋点
成熟方案通常是:
基础行为自动采集
+
关键业务手动埋点
埋点系统还需要:
- 事件命名规范。
- 参数字典和版本。
- 曝光去重。
- 批量与离线上报。
- 隐私授权。
- 实验分组关联。
13.5 降级与熔断
推荐失败 → 隐藏推荐模块
评论失败 → 局部重试
配置失败 → 使用安全默认值
图片失败 → 兜底图
核心接口失败 → 错误页和人工恢复入口
不要让非核心模块失败拖垮整个页面。
14. 跨页面与跨标签页通信
14.1 常见方式
| 方式 | 适用场景 |
|---|---|
| 路由参数 | 页面跳转所需的可序列化状态 |
| Store | 同一应用实例内的共享状态 |
postMessage | iframe、WebView、跨窗口通信 |
BroadcastChannel | 同源多标签页广播 |
| Storage Event | 简单同源跨标签页通知 |
| SharedWorker | 多页面共享后台逻辑,兼容性需评估 |
| WebSocket / SSE | 服务端实时推送 |
14.2 多标签页登录同步
需要同步:
- 退出登录。
- Token 更新通知。
- 用户资料刷新。
- 权限变化。
- 草稿或关键数据修改提醒。
不要在消息通道中广播 Refresh Token 等敏感值;可只发送“登录状态已变化”的事件,各标签页自行重新读取安全存储或重新认证。
14.3 WebView 通信
WebView 消息必须校验:
- 消息来源。
- 消息类型。
- 数据结构。
- 当前业务流程状态。
- 是否允许重复触发。
不能因为消息来自 WebView 就默认可信。
15. 安全方案
15.1 输入与输出
- 所有用户输入都不可信,包括小程序输入。
- 前端校验用于体验,服务端校验用于安全。
- H5 禁止直接使用
v-html或dangerouslySetInnerHTML渲染用户输入。 - 必须渲染富文本时,使用明确白名单的 HTML Sanitizer,并在服务端再次处理。
15.2 敏感信息
禁止在前端构建产物或仓库中存储:
- AppSecret。
- 数据库密码。
- 服务端私钥。
- 永久访问 Token。
- 真实生产用户数据。
前端环境变量不是秘密。
15.3 本地存储
不建议直接存储:
- 密码和短信验证码。
- 身份证完整信息。
- 银行卡完整信息。
- 支付凭证。
- 长期有效高权限 Token。
本地缓存需要用户隔离、过期、版本迁移和退出清理。
15.4 URL 安全
URL 不适合承载:
- Token。
- 身份证号。
- 支付参数。
- 私密业务数据。
因为 URL 可能进入浏览器历史、服务器日志、Referer 和埋点系统。
16. 测试与质量门禁
16.1 测试分层
| 层级 | 关注点 |
|---|---|
| 单元测试 | 纯函数、状态机、Mapper、校验规则 |
| 组件测试 | 交互、状态、错误和边界展示 |
| 集成测试 | 请求层、Store、路由、缓存协作 |
| E2E | 登录、提交、支付前流程、草稿恢复 |
| 视觉回归 | 页面样式和布局是否意外变化 |
16.2 技术方案的关键测试
草稿系统:
- 弱网保存。
- 页面退出再恢复。
- 提交与自动保存竞态。
- 多设备版本冲突。
- 结构版本迁移。
请求层:
- 多个并发 401 只刷新一次。
- 旧请求不会覆盖新请求。
- 主动取消不展示错误。
- 非幂等请求不会自动重放。
离线队列:
- 网络恢复补发。
- 幂等去重。
- 登录失效后不错误补发。
- 达到最大重试次数后进入失败状态。
16.3 发布门禁
建议至少包括:
- TypeScript 类型检查。
- ESLint。
- 单元和关键集成测试。
- 构建检查。
- 关键 E2E。
- Bundle 体积阈值。
- Source Map 上传确认。
- 环境配置和敏感信息扫描。
17. 常见反模式
17.1 状态反模式
- 所有状态都放入全局 Store。
- 接口数据复制到多个 Store 后分别修改。
- URL、Store 和组件同时保存筛选条件。
- 将缓存恢复误认为服务端数据仍然有效。
17.2 请求反模式
- 页面直接调用原生请求 API。
- 所有错误统一 Toast“网络错误”。
- 所有请求都自动重试。
- 用防抖代替幂等。
- 组件卸载后仍然无条件写状态。
17.3 缓存反模式
- 只设置缓存时间,不定义失效事件。
- 用户切换账号后继续使用旧缓存。
- API、静态资源和敏感数据共用相同缓存策略。
- Service Worker 长期缓存 HTML,导致页面壳和 JS 版本不匹配。
17.4 多端反模式
- 将
wx.*API 直接复制到支付宝小程序。 - 认为 uni-app API 一致就代表生命周期和能力一致。
- 只在浏览器模拟器验证上传、定位、授权和 WebView。
17.5 架构反模式
- 为小型需求引入微前端、CRDT 或复杂离线同步。
- 只抽象“工具函数”,没有抽象业务状态和失败恢复。
- 方案只覆盖成功路径,没有取消、超时、重试和回滚。
18. 项目落地顺序
18.1 基础阶段
- 统一 TypeScript、Lint 和构建规范。
- 建立请求层和错误模型。
- 明确状态分类和 Store 边界。
- 建立表单、上传和列表基础组件。
- 接入基础错误监控。
18.2 业务阶段
- 登录续期和权限体系。
- 防重复提交和服务端幂等。
- 草稿和页面状态恢复。
- 服务端状态缓存和精确失效。
- 文件临时态与正式态管理。
18.3 稳定性阶段
- 灰度和动态配置。
- 请求取消、重试和降级。
- 埋点和性能监控。
- 离线队列和弱网恢复。
- 关键链路 E2E 和发布门禁。
18.4 平台化阶段
- 多端 Adapter。
- 通用状态机和业务 SDK。
- 统一监控、配置和实验平台。
- 组件库和设计系统。
- 可审计的权限和发布流程。
19. 生产检查清单
数据
- 每类状态有明确的权威数据源。
- 本地数据有版本、过期和用户隔离。
- 敏感数据没有进入 Storage、URL 或日志。
- 正式提交后清理草稿和临时文件。
请求
- 请求可超时、可取消、可分类处理错误。
- 搜索和快速切换场景处理了竞态。
- 写操作有前端锁和服务端幂等。
- 重试仅用于可安全重试的错误。
- 401 刷新使用单一刷新锁。
缓存
- Query Key 包含所有影响响应的参数。
- 明确
staleTime、保留时间和失效事件。 - 退出登录会清理用户级缓存。
- 乐观更新可以回滚。
- 关键金额和支付结果不以客户端缓存为最终依据。
交互
- loading、空状态、错误、完成状态相互独立。
- 未保存离开有草稿或提醒兜底。
- 上传支持进度、失败重试和取消。
- 多弹窗有队列和优先级。
多端
- 微信、支付宝、H5、App 差异已列出。
- 关键流程完成真机测试。
- WebView 消息经过来源、类型和状态校验。
- 条件编译没有散落到核心业务逻辑。
稳定性
- 错误带版本、环境和请求 ID。
- 非核心模块失败可以局部降级。
- 埋点有命名、参数和版本规范。
- 关键链路有 E2E 和发布门禁。
20. 参考资料
以下资料通过 Context7 获取并核对,用于补充本文中的缓存、请求取消和离线方案:
TanStack Query
- TanStack Query 官方仓库:https://github.com/TanStack/query
- TanStack Query v5 文档:https://tanstack.com/query/latest/docs/framework/react/overview
- Query Cancellation:https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation
- Query Invalidation:https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation
- Optimistic Updates:https://tanstack.com/query/latest/docs/framework/react/guides/optimistic-updates
Workbox
- Workbox 官方仓库:https://github.com/GoogleChrome/workbox
- Workbox Strategies:https://developer.chrome.com/docs/workbox/modules/workbox-strategies
- Workbox Precaching:https://developer.chrome.com/docs/workbox/modules/workbox-precaching
- Workbox Background Sync:https://developer.chrome.com/docs/workbox/modules/workbox-background-sync
MDN Web Docs
- AbortController:https://developer.mozilla.org/en-US/docs/Web/API/AbortController
- AbortSignal:https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal
- IndexedDB:https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- Service Worker:https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
- Page Visibility:https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
- BroadcastChannel:https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel
文档核对日期:2026 年 7 月 24 日。库版本、浏览器兼容性和默认行为可能继续变化,落地前应再次核对目标版本官方文档和目标平台兼容矩阵。