跳到主要内容

前端常见技术方案

面向 React、Vue、H5、微信小程序、支付宝小程序和 uni-app 项目的工程实践手册。

本文关注“机制、边界、生产风险和选型规则”,不绑定单一框架。示例默认采用 TypeScript 严格模式。

目录​


1. 技术方案的判断框架​

前端技术方案不是 API 清单,而是对以下问题的系统回答:

  1. 谁是权威数据源:组件内存、URL、本地存储还是服务端?
  2. 数据什么时候失效:按时间、按事件、按版本还是手动失效?
  3. 异常后怎么恢复:重试、回滚、降级、离线队列还是提示用户?
  4. 并发时谁覆盖谁:最后写入覆盖、乐观锁、字段合并还是协同算法?
  5. 页面销毁后任务怎么办:取消、忽略结果、继续缓存还是转入后台?
  6. 多端是否同构:H5、小程序和 App 的生命周期、存储和网络能力是否一致?
  7. 安全边界在哪里:哪些校验只是交互优化,哪些必须由服务端保证?

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 提交竞态​

危险流程:

自动保存请求发出
↓
正式提交成功并删除草稿
↓
旧自动保存请求返回
↓
草稿被重新创建

提交前应:

  1. 取消防抖任务。
  2. 中止或忽略在途保存请求。
  3. 将状态置为 submitted。
  4. 服务端拒绝继续更新已提交草稿。
  5. 正式提交成功后再清除本地数据。

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>;
}

可分别实现:

  • WebDraftRepository
  • WechatDraftRepository
  • AlipayDraftRepository
  • UniAppDraftRepository
  • RemoteDraftRepository
  • HybridDraftRepository

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 请求竞态​

搜索、筛选和路由快速切换时,旧请求可能晚于新请求返回。常见处理方式:

  1. 使用 AbortController 取消旧请求。
  2. 使用递增请求序号,只接收最新响应。
  3. 让服务端状态库按 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 成功后通常有三种策略:

  1. 使用响应结果精确更新缓存。
  2. 使相关 Query 失效并重新获取。
  3. 同时更新列表和详情缓存。

失效不是清空所有缓存。应按业务关联范围精确失效,避免一次修改导致全站请求风暴。

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 中:

  • WechatPlatformAdapter
  • AlipayPlatformAdapter
  • H5PlatformAdapter
  • AppPlatformAdapter

不要让 wx.*、my.* 和条件编译散落在业务页面。


12. 性能方案​

12.1 性能治理顺序​

  1. 先测量真实瓶颈。
  2. 优先减少资源体积和请求数量。
  3. 再减少主线程计算和 DOM 成本。
  4. 最后考虑复杂缓存和渲染优化。

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同一应用实例内的共享状态
postMessageiframe、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 基础阶段​

  1. 统一 TypeScript、Lint 和构建规范。
  2. 建立请求层和错误模型。
  3. 明确状态分类和 Store 边界。
  4. 建立表单、上传和列表基础组件。
  5. 接入基础错误监控。

18.2 业务阶段​

  1. 登录续期和权限体系。
  2. 防重复提交和服务端幂等。
  3. 草稿和页面状态恢复。
  4. 服务端状态缓存和精确失效。
  5. 文件临时态与正式态管理。

18.3 稳定性阶段​

  1. 灰度和动态配置。
  2. 请求取消、重试和降级。
  3. 埋点和性能监控。
  4. 离线队列和弱网恢复。
  5. 关键链路 E2E 和发布门禁。

18.4 平台化阶段​

  1. 多端 Adapter。
  2. 通用状态机和业务 SDK。
  3. 统一监控、配置和实验平台。
  4. 组件库和设计系统。
  5. 可审计的权限和发布流程。

19. 生产检查清单​

数据​

  • 每类状态有明确的权威数据源。
  • 本地数据有版本、过期和用户隔离。
  • 敏感数据没有进入 Storage、URL 或日志。
  • 正式提交后清理草稿和临时文件。

请求​

  • 请求可超时、可取消、可分类处理错误。
  • 搜索和快速切换场景处理了竞态。
  • 写操作有前端锁和服务端幂等。
  • 重试仅用于可安全重试的错误。
  • 401 刷新使用单一刷新锁。

缓存​

  • Query Key 包含所有影响响应的参数。
  • 明确 staleTime、保留时间和失效事件。
  • 退出登录会清理用户级缓存。
  • 乐观更新可以回滚。
  • 关键金额和支付结果不以客户端缓存为最终依据。

交互​

  • loading、空状态、错误、完成状态相互独立。
  • 未保存离开有草稿或提醒兜底。
  • 上传支持进度、失败重试和取消。
  • 多弹窗有队列和优先级。

多端​

  • 微信、支付宝、H5、App 差异已列出。
  • 关键流程完成真机测试。
  • WebView 消息经过来源、类型和状态校验。
  • 条件编译没有散落到核心业务逻辑。

稳定性​

  • 错误带版本、环境和请求 ID。
  • 非核心模块失败可以局部降级。
  • 埋点有命名、参数和版本规范。
  • 关键链路有 E2E 和发布门禁。

20. 参考资料​

以下资料通过 Context7 获取并核对,用于补充本文中的缓存、请求取消和离线方案:

TanStack Query​

Workbox​

MDN Web Docs​

文档核对日期:2026 年 7 月 24 日。库版本、浏览器兼容性和默认行为可能继续变化,落地前应再次核对目标版本官方文档和目标平台兼容矩阵。