跳到主要内容

前端常见技术方案

面向 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 staleTimegcTime

以 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-htmldangerouslySetInnerHTML 渲染用户输入。
  • 必须渲染富文本时,使用明确白名单的 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 日。库版本、浏览器兼容性和默认行为可能继续变化,落地前应再次核对目标版本官方文档和目标平台兼容矩阵。