通用设计模式与前端工程实践
面向现代 Web、React、Vue、小程序与跨端应用的设计模式学习手册
文档定位:先理解通用设计模式解决的变化问题,再使用严格 TypeScript 和前端场景实现,而不是把模式机械翻译成类。
官方资料核对日期:2026-08-07
目录
- 1. 设计模式解决什么问题
- 2. 使用设计模式前的判断框架
- 3. TypeScript 实现模式时的类型工具
- 4. 创建型模式
- 5. 结构型模式
- 6. 行为型模式
- 7. 前端特有的组合与架构模式
- 8. React 中的设计模式映射
- 9. Vue 3 中的设计模式映射
- 10. 综合案例:多端支付流程
- 11. 综合案例:可撤销的页面编辑器
- 12. 测试策略
- 13. 常见反模式
- 14. 模式选型速查表
- 15. 工程落地检查清单
- 16. 参考资料
1. 设计模式解决什么问题
设计模式不是代码模板,而是针对重复出现的软件设计问题形成的可复用解法。它主要处理四类变化:
- 对象如何创建:调用方不应该知道具体类、平台和初始化细节。
- 对象如何组合:系统需要增加能力,但不希望修改稳定核心。
- 对象如何协作:状态、事件和流程需要清晰流动,而不是互相调用。
- 变化如何隔离:新增支付渠道、渲染器或业务规则时,不应修改大量旧代码。
前端中的典型变化点:
平台变化:H5 / 微信小程序 / 支付宝小程序 / App
框架变化:React / Vue / 原生 Web Components
供应商变化:地图、支付、上传、监控、AI 服务
业务变化:订单状态、优惠策略、权限规则、审批流程
交互变化:受控组件、异步状态、撤销重做、实时协作
基础设施变化:REST / GraphQL / WebSocket / IndexedDB
判断一个抽象是否有效,重点不是“用了几个模式”,而是:
增加一种变化时,需要修改多少稳定代码?
如果新增一个渠道只需增加一个实现并完成注册,而不是修改十几处条件分支,说明抽象产生了价值。
2. 使用设计模式前的判断框架
2.1 先找变化轴
不要看到 if/else 就立刻使用策略模式。先判断分支背后是否存在长期、独立的变化轴。
interface ChangeAxis {
name: string
frequency: 'low' | 'medium' | 'high'
businessImpact: 'local' | 'cross-module'
implementations: number
}
适合抽象的信号:
- 已经存在 3 个以上同类实现;
- 业务明确会继续增加实现;
- 分支散落在多个模块;
- 外部 SDK 或后端契约经常变化;
- 不同实现需要独立测试、灰度或替换;
- 错误处理、权限、监控等横切逻辑重复出现。
暂时不适合抽象的信号:
- 只有一个实现;
- 分支很短且不会扩展;
- 抽象后调用链比业务逻辑更难理解;
- 为了“符合设计模式”创建大量空接口和单方法类;
- 无法明确抽象边界和生命周期所有权。
2.2 优先使用最小抽象
推荐顺序:
普通函数
↓
高阶函数 / 函数组合
↓
对象映射表
↓
接口 + 独立实现
↓
类继承体系
↓
框架或插件系统
JavaScript 支持闭包、高阶函数和模块,因此很多传统面向对象模式可以用更轻量的方式表达。
2.3 组合优于继承
继承表达稳定的 “is-a” 关系;组合表达可替换的 “has-a” 能力。
interface Logger {
log(message: string): void
}
interface CacheStore {
get(key: string): string | undefined
set(key: string, value: string): void
}
class ProductService {
public constructor(
private readonly logger: Logger,
private readonly cache: CacheStore,
) {}
}
这里 ProductService 不需要继承 LoggerBase 或 CacheBase,能力可以独立替换和测试。
2.4 模式不是架构层级
同一个模式可以出现在不同层:
| 模式 | UI 层 | 业务层 | 基础设施层 |
|---|---|---|---|
| 策略 | 不同渲染方式 | 优惠算法 | 重试算法 |
| 适配器 | 组件 Props 兼容 | 多端支付 | SDK 响应转换 |
| 观察者 | 响应式更新 | 领域事件 | WebSocket 消息 |
| 外观 | 页面操作入口 | 下单流程 | 上传流程 |
| 代理 | 权限组件 | 幂等控制 | 缓存、重试、鉴权 |
3. TypeScript 实现模式时的类型工具
设计模式不是“多写类”。TypeScript 的核心价值是把模式约束变成编译期契约。
3.1 用 unknown 表示未验证的外部数据
网络响应、缓存数据、postMessage 和第三方 SDK 返回值都不可信。
interface User {
id: string
name: string
}
/** 判断未知数据是否满足 User 契约。 */
function isUser(value: unknown): value is User {
if (typeof value !== 'object' || value === null) {
return false
}
const record = value as Record<string, unknown>
return typeof record.id === 'string' && typeof record.name === 'string'
}
/** 将外部数据解析为可信 User。 */
function parseUser(value: unknown): User {
if (!isUser(value)) {
throw new Error('用户数据格式不合法')
}
return value
}
不要用类型断言代替运行时验证:
// 不推荐:只欺骗编译器,没有验证运行时数据。
const user = response as User
3.2 用判别联合表达互斥状态
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error }
/** 对异步状态执行穷尽处理。 */
function getStatusText<T>(state: AsyncState<T>): string {
switch (state.status) {
case 'idle':
return '未开始'
case 'loading':
return '加载中'
case 'success':
return '加载成功'
case 'error':
return state.error.message
default: {
const exhaustiveCheck: never = state
return exhaustiveCheck
}
}
}
相比多个布尔值,判别联合不会产生 loading、success、error 同时为真的非法组合。
3.3 用 as const 和 satisfies 保留字面量约束
const PAYMENT_TYPE = {
ALIPAY: 'alipay',
WECHAT: 'wechat',
CARD: 'card',
} as const
type PaymentType = (typeof PAYMENT_TYPE)[keyof typeof PAYMENT_TYPE]
interface PaymentMetadata {
label: string
enabled: boolean
}
const PAYMENT_METADATA = {
alipay: { label: '支付宝', enabled: true },
wechat: { label: '微信支付', enabled: true },
card: { label: '银行卡', enabled: false },
} satisfies Record<PaymentType, PaymentMetadata>
satisfies 校验对象满足契约,同时保留属性的精确推断。
3.4 泛型用于保留关系,不用于制造复杂度
interface Repository<TEntity, TId> {
getById(id: TId): Promise<TEntity>
save(entity: TEntity): Promise<TEntity>
}
泛型的价值是表达:输入的 ID 类型和实体类型在整个契约中保持一致。如果所有实现永远只有一种实体,普通类型更清晰。
3.5 接口、抽象类与函数类型如何选择
| 工具 | 适合场景 |
|---|---|
| 函数类型 | 单一算法、策略、转换器、中间件 |
| 接口 | 多个实现共享行为契约,不共享实现 |
| 抽象类 | 需要共享稳定流程或受保护实现 |
| 判别联合 | 有限、互斥、可穷尽的状态或命令 |
| 泛型 | 需要保留输入、输出之间的类型关系 |
4. 创建型模式
创建型模式隔离对象的创建过程,使业务代码依赖能力,而不是依赖具体构造方式。
4.1 单例模式 Singleton
意图
保证某个能力在指定生命周期内只有一个实例,并提供统一访问入口。
前端场景
- 埋点客户端;
- 全局消息调度器;
- 浏览器标签页内的 WebSocket 连接;
- 请求客户端;
- Feature Flag 客户端。
模块级单例
ES Module 本身只初始化一次,前端优先使用模块导出:
class AnalyticsClient {
/** 上报业务事件。 */
public track(eventName: string, properties: Record<string, unknown>): void {
console.log(eventName, properties)
}
}
export const analyticsClient = new AnalyticsClient()
显式单例
class SocketManager {
private static instance: SocketManager | undefined
private constructor(private readonly url: string) {}
/** 获取当前运行时中的唯一实例。 */
public static getInstance(url: string): SocketManager {
if (!SocketManager.instance) {
SocketManager.instance = new SocketManager(url)
}
return SocketManager.instance
}
}
生产边界
- SSR 服务进程中的模块单例会跨请求共享,不能存储用户态数据;
- 微前端有多个 JS Runtime 时,“单例”可能只在子应用内部成立;
- 多标签页需要
BroadcastChannel、SharedWorker 或服务端协调; - 测试时需要可重置或通过依赖注入替换。
4.2 工厂方法 Factory Method
意图
由创建函数决定实例化哪个具体实现,调用方只依赖统一契约。
interface FilePreviewer {
preview(url: string): Promise<void>
}
class ImagePreviewer implements FilePreviewer {
/** 预览图片。 */
public async preview(url: string): Promise<void> {
console.log('打开图片预览', url)
}
}
class PdfPreviewer implements FilePreviewer {
/** 预览 PDF。 */
public async preview(url: string): Promise<void> {
console.log('打开 PDF 预览', url)
}
}
type FileType = 'image' | 'pdf'
/** 根据文件类型创建预览器。 */
function createFilePreviewer(type: FileType): FilePreviewer {
const factories: Record<FileType, () => FilePreviewer> = {
image: () => new ImagePreviewer(),
pdf: () => new PdfPreviewer(),
}
return factories[type]()
}
适用
- 创建逻辑包含平台判断、依赖组装或复杂初始化;
- 调用方不应该知道具体实现;
- 实现列表会扩展。
不适用
只有一个简单对象时,直接构造比工厂更清楚。
4.3 抽象工厂 Abstract Factory
意图
创建一组相互兼容的对象,避免不同产品族混用。
前端场景
为不同运行平台创建完整能力组:授权、支付、导航和存储。
interface AuthClient {
getAuthCode(): Promise<string>
}
interface Navigator {
navigateTo(url: string): Promise<void>
}
interface PlatformFactory {
createAuthClient(): AuthClient
createNavigator(): Navigator
}
class H5PlatformFactory implements PlatformFactory {
/** 创建 H5 授权客户端。 */
public createAuthClient(): AuthClient {
return {
getAuthCode: async () => 'h5-auth-code',
}
}
/** 创建 H5 导航器。 */
public createNavigator(): Navigator {
return {
navigateTo: async url => {
window.location.assign(url)
},
}
}
}
抽象工厂解决的是“产品族一致性”,工厂方法解决的是“一个对象如何创建”。
4.4 建造者模式 Builder
意图
分步骤构建复杂对象,并在最终输出前统一校验。
interface TableQuery {
keyword?: string
page: number
pageSize: number
filters: Readonly<Record<string, string>>
}
class TableQueryBuilder {
private keywordValue: string | undefined
private pageValue = 1
private pageSizeValue = 20
private readonly filterValues: Record<string, string> = {}
/** 设置关键词。 */
public keyword(value: string): this {
this.keywordValue = value.trim() || undefined
return this
}
/** 设置分页。 */
public pagination(page: number, pageSize: number): this {
if (page < 1 || pageSize < 1) {
throw new Error('分页参数必须大于 0')
}
this.pageValue = page
this.pageSizeValue = pageSize
return this
}
/** 添加筛选条件。 */
public filter(name: string, value: string): this {
this.filterValues[name] = value
return this
}
/** 生成不可变查询对象。 */
public build(): Readonly<TableQuery> {
return Object.freeze({
keyword: this.keywordValue,
page: this.pageValue,
pageSize: this.pageSizeValue,
filters: Object.freeze({ ...this.filterValues }),
})
}
}
如果参数数量少,使用普通配置对象和校验函数即可,不要为了链式 API 引入 Builder。
4.5 原型模式 Prototype
意图
通过复制已有对象创建新对象,保留复杂默认配置。
interface DashboardWidget {
id: string
type: 'chart' | 'table'
position: { x: number; y: number }
settings: Record<string, unknown>
}
/** 深复制组件配置并生成新的业务标识。 */
function cloneWidget(
source: DashboardWidget,
createId: () => string,
): DashboardWidget {
return {
...structuredClone(source),
id: createId(),
}
}
生产边界
- 对普通对象优先使用
structuredClone; - 它不能复制函数、DOM 节点及部分宿主对象;
- JSON 序列化会丢失
Date、Map、Set、undefined等语义; - 领域对象复制后可能需要重新建立不变量,不能只做浅拷贝。
5. 结构型模式
结构型模式关注对象如何组合,以及如何在不破坏稳定核心的情况下增加能力。
5.1 适配器模式 Adapter
意图
把不兼容的外部接口转换成业务内部的统一接口。
interface Location {
latitude: number
longitude: number
}
interface LocationService {
getCurrentLocation(): Promise<Location>
}
interface LegacyMapSdk {
locate(
success: (lat: number, lng: number) => void,
failure: (message: string) => void,
): void
}
class LegacyMapAdapter implements LocationService {
public constructor(private readonly sdk: LegacyMapSdk) {}
/** 将回调式 SDK 适配成 Promise 契约。 */
public getCurrentLocation(): Promise<Location> {
return new Promise((resolve, reject) => {
this.sdk.locate(
(latitude, longitude) => resolve({ latitude, longitude }),
message => reject(new Error(message)),
)
})
}
}
适配器层还应负责:
- 字段重命名;
- 错误码转换;
- 单位转换;
- 空值语义转换;
- 回调与 Promise 转换;
- SDK 版本差异隔离。
不要在业务组件中到处读取第三方原始字段。
5.2 桥接模式 Bridge
意图
把两个独立变化维度拆开,使它们可以自由组合。
前端场景
通知内容和发送渠道分别变化:
interface MessageChannel {
send(content: string): Promise<void>
}
class EmailChannel implements MessageChannel {
/** 发送邮件消息。 */
public async send(content: string): Promise<void> {
console.log('Email:', content)
}
}
class Notification {
public constructor(private readonly channel: MessageChannel) {}
/** 发送普通通知。 */
public async send(title: string, body: string): Promise<void> {
await this.channel.send(`${title}\n${body}`)
}
}
class UrgentNotification extends Notification {
/** 发送紧急通知。 */
public async sendUrgent(body: string): Promise<void> {
await this.send('紧急通知', body)
}
}
如果使用继承同时表示“消息类型 × 渠道”,类数量会出现笛卡尔积;桥接模式把维度拆开。
5.3 组合模式 Composite
意图
让叶子节点和容器节点共享统一操作方式。
前端场景
- DOM、组件树、路由树;
- 菜单和权限树;
- 页面搭建器;
- 表单 Schema;
- AST。
interface RenderNode {
render(): string
}
class TextNode implements RenderNode {
public constructor(private readonly text: string) {}
/** 渲染文本节点。 */
public render(): string {
return this.text
}
}
class ContainerNode implements RenderNode {
private readonly children: RenderNode[] = []
/** 添加子节点。 */
public add(child: RenderNode): void {
this.children.push(child)
}
/** 递归渲染所有子节点。 */
public render(): string {
return this.children.map(child => child.render()).join('')
}
}
树结构要明确:节点 ID、父子所有权、循环引用策略、最大深度和批量更新性能。
5.4 装饰器模式 Decorator
意图
通过包装对象动态增加能力,不修改原实现。
前端中高阶函数通常比语言级装饰器更轻量:
type AsyncOperation<TArgs extends unknown[], TResult> = (
...args: TArgs
) => Promise<TResult>
/** 为异步操作增加耗时统计。 */
function withTiming<TArgs extends unknown[], TResult>(
operationName: string,
operation: AsyncOperation<TArgs, TResult>,
): AsyncOperation<TArgs, TResult> {
return async (...args) => {
const startTime = performance.now()
try {
return await operation(...args)
} finally {
const duration = performance.now() - startTime
console.log(`${operationName}: ${duration.toFixed(1)}ms`)
}
}
}
可组合:
const monitoredRequest = withTiming('load-user', loadUser)
装饰顺序会影响语义,例如“先缓存再重试”和“先重试再缓存”并不等价,需要通过测试固定顺序。
5.5 外观模式 Facade
意图
为复杂子系统提供一个稳定、简单的业务入口。
interface CheckoutInput {
skuId: string
quantity: number
}
class CheckoutFacade {
public constructor(
private readonly inventory: InventoryService,
private readonly order: OrderService,
private readonly payment: PaymentService,
) {}
/** 执行完整结算流程。 */
public async checkout(input: CheckoutInput): Promise<string> {
await this.inventory.reserve(input.skuId, input.quantity)
try {
const orderId = await this.order.create(input)
await this.payment.pay(orderId)
return orderId
} catch (error: unknown) {
await this.inventory.release(input.skuId, input.quantity)
throw error
}
}
}
Facade 不应变成包含所有业务的“万能 Service”。它应该协调子系统,领域规则仍由对应模块拥有。
5.6 享元模式 Flyweight
意图
共享大量对象中的公共、不可变数据,降低内存和初始化成本。
前端场景
- Canvas 中数万个图形共享样式;
- 虚拟列表共享行类型配置;
- 富文本编辑器共享字体配置;
- 地图 Marker 共享图标资源。
interface MarkerStyle {
color: string
iconUrl: string
}
class MarkerStyleFactory {
private readonly cache = new Map<string, Readonly<MarkerStyle>>()
/** 获取可共享的不可变 Marker 样式。 */
public getStyle(color: string, iconUrl: string): Readonly<MarkerStyle> {
const key = `${color}:${iconUrl}`
const cached = this.cache.get(key)
if (cached) {
return cached
}
const style = Object.freeze({ color, iconUrl })
this.cache.set(key, style)
return style
}
}
共享对象必须不可变;位置、选中态等外在状态应存放在具体实例中。
5.7 代理模式 Proxy
意图
在访问真实对象前后增加控制逻辑。
前端场景
- 缓存;
- 权限控制;
- 防重复请求;
- 懒加载;
- 重试与熔断;
- Vue 响应式代理。
/** 为请求增加同参数并发去重。 */
function withRequestDeduplication<TArgs extends unknown[], TResult>(
request: AsyncOperation<TArgs, TResult>,
): AsyncOperation<TArgs, TResult> {
const pendingRequests = new Map<string, Promise<TResult>>()
return (...args) => {
const key = JSON.stringify(args)
const pending = pendingRequests.get(key)
if (pending) {
return pending
}
const promise = request(...args).finally(() => {
pendingRequests.delete(key)
})
pendingRequests.set(key, promise)
return promise
}
}
注意:参数中包含函数、循环引用、二进制或字段顺序不稳定时,不能直接用 JSON.stringify 生成生产级缓存键。
6. 行为型模式
行为型模式关注职责分配、状态变化和对象协作。
6.1 责任链模式 Chain of Responsibility
意图
让请求依次经过多个处理器,每个处理器只负责一个横切能力。
interface RequestContext {
url: string
headers: Record<string, string>
signal?: AbortSignal
}
type Middleware = (
context: RequestContext,
next: () => Promise<Response>,
) => Promise<Response>
/** 将中间件组合成可执行请求链。 */
function composeMiddleware(
middleware: Middleware[],
transport: (context: RequestContext) => Promise<Response>,
): (context: RequestContext) => Promise<Response> {
return context => {
const dispatch = (index: number): Promise<Response> => {
const current = middleware[index]
if (!current) {
return transport(context)
}
return current(context, () => dispatch(index + 1))
}
return dispatch(0)
}
}
生产实现必须防止同一个中间件重复调用 next(),并明确错误、取消、超时和重试的传播语义。
6.2 命令模式 Command
意图
把操作封装为对象,使操作可以记录、排队、撤销、重做或远程传输。
interface Command {
execute(): void
undo(): void
}
class UpdateTitleCommand implements Command {
private readonly previousTitle: string
public constructor(
private readonly documentState: { title: string },
private readonly nextTitle: string,
) {
this.previousTitle = documentState.title
}
/** 执行标题修改。 */
public execute(): void {
this.documentState.title = this.nextTitle
}
/** 恢复修改前标题。 */
public undo(): void {
this.documentState.title = this.previousTitle
}
}
class CommandHistory {
private readonly undoStack: Command[] = []
private readonly redoStack: Command[] = []
/** 执行并记录命令。 */
public execute(command: Command): void {
command.execute()
this.undoStack.push(command)
this.redoStack.length = 0
}
/** 撤销最近命令。 */
public undo(): void {
const command = this.undoStack.pop()
if (!command) return
command.undo()
this.redoStack.push(command)
}
/** 重做最近撤销的命令。 */
public redo(): void {
const command = this.redoStack.pop()
if (!command) return
command.execute()
this.undoStack.push(command)
}
}
异步命令还要处理执行中状态、失败补偿、幂等键和撤销是否仍然有效。
6.3 迭代器模式 Iterator
意图
统一遍历集合,而不暴露集合内部结构。
interface TreeNode<T> {
value: T
children: TreeNode<T>[]
}
/** 深度优先遍历树。 */
function* walkTree<T>(root: TreeNode<T>): Generator<T> {
yield root.value
for (const child of root.children) {
yield* walkTree(child)
}
}
使用:
for (const menu of walkTree(menuTree)) {
console.log(menu)
}
生成器适合惰性遍历大集合,但不能代替虚拟列表;DOM 渲染性能仍取决于实际创建的节点数量。
6.4 中介者模式 Mediator
意图
把多个对象之间的网状依赖集中到协调者,减少对象互相引用。
前端场景
复杂筛选面板中的日期、地区、商品和价格字段互相联动。
type FilterValue = string | number | readonly string[] | undefined
class FilterMediator {
private readonly values = new Map<string, FilterValue>()
private readonly listeners = new Set<() => void>()
/** 更新筛选条件并通知所有参与者。 */
public update(name: string, value: FilterValue): void {
this.values.set(name, value)
this.listeners.forEach(listener => listener())
}
/** 读取筛选条件。 */
public get(name: string): FilterValue {
return this.values.get(name)
}
/** 订阅整体变化。 */
public subscribe(listener: () => void): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
}
中介者如果掌握过多领域规则,会退化成 God Object。应按业务上下文拆分协调者。
6.5 备忘录模式 Memento
意图
保存对象某一时刻的状态,在不暴露内部实现的前提下恢复。
interface EditorSnapshot {
readonly content: string
readonly selectionStart: number
readonly selectionEnd: number
}
class TextEditorModel {
public constructor(
private content = '',
private selectionStart = 0,
private selectionEnd = 0,
) {}
/** 创建不可变快照。 */
public createSnapshot(): EditorSnapshot {
return Object.freeze({
content: this.content,
selectionStart: this.selectionStart,
selectionEnd: this.selectionEnd,
})
}
/** 恢复指定快照。 */
public restore(snapshot: EditorSnapshot): void {
this.content = snapshot.content
this.selectionStart = snapshot.selectionStart
this.selectionEnd = snapshot.selectionEnd
}
}
大型文档不要每次保存完整快照,可使用结构共享、增量补丁、Command 或 CRDT/OT 日志。
6.6 观察者模式 Observer
意图
当目标状态变化时,主动通知已注册观察者。
type Listener<T> = (value: T) => void
class ObservableValue<T> {
private readonly listeners = new Set<Listener<T>>()
public constructor(private value: T) {}
/** 获取当前值。 */
public getValue(): T {
return this.value
}
/** 更新值并通知订阅者。 */
public setValue(value: T): void {
if (Object.is(this.value, value)) {
return
}
this.value = value
this.listeners.forEach(listener => listener(value))
}
/** 注册观察者并返回取消订阅函数。 */
public subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener)
return () => this.listeners.delete(listener)
}
}
Vue 响应式、DOM 事件、ResizeObserver、MutationObserver 都体现观察者思想。
6.7 发布订阅模式 Pub/Sub
发布订阅不是 GoF 23 种模式中的独立条目,但在前端非常常见。
type AppEvents = {
'user:login': { userId: string }
'order:created': { orderId: string }
}
class EventBus<TEvents extends Record<string, unknown>> {
private readonly listeners = new Map<
keyof TEvents,
Set<(payload: unknown) => void>
>()
/** 订阅事件。 */
public on<TKey extends keyof TEvents>(
eventName: TKey,
listener: (payload: TEvents[TKey]) => void,
): () => void {
const listeners =
this.listeners.get(eventName) ?? new Set<(payload: unknown) => void>()
listeners.add(listener as (payload: unknown) => void)
this.listeners.set(eventName, listeners)
return () => {
listeners.delete(listener as (payload: unknown) => void)
}
}
/** 发布事件。 */
public emit<TKey extends keyof TEvents>(
eventName: TKey,
payload: TEvents[TKey],
): void {
this.listeners.get(eventName)?.forEach(listener => listener(payload))
}
}
观察者与发布订阅的区别
观察者:Subject 直接持有并通知 Observer
发布订阅:Publisher → Event Bus / Broker → Subscriber
EventBus 适合边界明确的领域事件或微前端通信,不适合作为默认组件通信方式,否则数据流难以追踪。
6.8 状态模式 State
意图
对象根据当前状态改变行为,并显式约束合法状态转换。
type UploadState =
| { status: 'idle' }
| { status: 'uploading'; progress: number }
| { status: 'paused'; progress: number }
| { status: 'success'; fileUrl: string }
| { status: 'failed'; error: Error }
type UploadEvent =
| { type: 'start' }
| { type: 'progress'; progress: number }
| { type: 'pause' }
| { type: 'complete'; fileUrl: string }
| { type: 'fail'; error: Error }
| { type: 'reset' }
/** 根据事件计算上传状态。 */
function uploadReducer(state: UploadState, event: UploadEvent): UploadState {
switch (event.type) {
case 'start':
return { status: 'uploading', progress: 0 }
case 'progress':
if (state.status !== 'uploading') return state
return { status: 'uploading', progress: event.progress }
case 'pause':
if (state.status !== 'uploading') return state
return { status: 'paused', progress: state.progress }
case 'complete':
return { status: 'success', fileUrl: event.fileUrl }
case 'fail':
return { status: 'failed', error: event.error }
case 'reset':
return { status: 'idle' }
default: {
const exhaustiveCheck: never = event
return exhaustiveCheck
}
}
}
高风险流程还应使用状态转移表或正式状态机,避免 reducer 默默接受非法转换。
6.9 策略模式 Strategy
意图
封装一组可替换算法,由运行时上下文选择。
interface PriceContext {
amount: number
isFirstOrder: boolean
}
type PriceStrategy = (context: PriceContext) => number
const PRICE_STRATEGIES = {
normal: ({ amount }) => amount,
vip: ({ amount }) => amount * 0.9,
campaign: ({ amount, isFirstOrder }) =>
isFirstOrder ? Math.max(0, amount - 30) : amount,
} satisfies Record<string, PriceStrategy>
type PriceStrategyName = keyof typeof PRICE_STRATEGIES
/** 按策略计算价格。 */
function calculatePrice(
strategyName: PriceStrategyName,
context: PriceContext,
): number {
return PRICE_STRATEGIES[strategyName](context)
}
策略模式隔离算法差异;如果每个分支还负责流程编排、请求和 UI 更新,应该继续拆分职责。
6.10 模板方法 Template Method
意图
定义稳定流程骨架,把差异步骤留给子类实现。
abstract class FileUploader {
/** 执行标准上传流程。 */
public async upload(file: File): Promise<string> {
this.validate(file)
const processedFile = await this.preprocess(file)
return this.send(processedFile)
}
/** 校验文件。 */
protected validate(file: File): void {
if (file.size > 10 * 1024 * 1024) {
throw new Error('文件不能超过 10 MB')
}
}
/** 预处理文件。 */
protected async preprocess(file: File): Promise<File> {
return file
}
/** 发送文件。 */
protected abstract send(file: File): Promise<string>
}
现代前端更常用函数管线替代继承:
interface UploadPipeline {
validate(file: File): void
preprocess(file: File): Promise<File>
send(file: File): Promise<string>
}
/** 执行组合式上传流程。 */
async function upload(
pipeline: UploadPipeline,
file: File,
): Promise<string> {
pipeline.validate(file)
return pipeline.send(await pipeline.preprocess(file))
}
6.11 访问者模式 Visitor
意图
在不修改节点类型的情况下,为稳定对象结构增加新操作。
前端场景
AST、表单 Schema、页面搭建器节点树。
type SchemaNode =
| { type: 'text'; value: string }
| { type: 'image'; src: string; alt: string }
| { type: 'container'; children: SchemaNode[] }
interface SchemaVisitor<TResult> {
visitText(node: Extract<SchemaNode, { type: 'text' }>): TResult
visitImage(node: Extract<SchemaNode, { type: 'image' }>): TResult
visitContainer(node: Extract<SchemaNode, { type: 'container' }>): TResult
}
/** 使用访问者处理节点。 */
function visitNode<TResult>(
node: SchemaNode,
visitor: SchemaVisitor<TResult>,
): TResult {
switch (node.type) {
case 'text':
return visitor.visitText(node)
case 'image':
return visitor.visitImage(node)
case 'container':
return visitor.visitContainer(node)
default: {
const exhaustiveCheck: never = node
return exhaustiveCheck
}
}
}
访问者适合“节点类型稳定、操作经常增加”。如果节点类型经常增加,判别联合和模式匹配往往更直接。
6.12 解释器模式 Interpreter
意图
为有限语法定义解释规则。
前端场景
- 权限表达式;
- 表单显隐规则;
- 搜索过滤 DSL;
- 模板表达式;
- Feature Flag 条件。
type Expression =
| { type: 'equals'; field: string; value: string }
| { type: 'and'; expressions: Expression[] }
| { type: 'or'; expressions: Expression[] }
| { type: 'not'; expression: Expression }
/** 解释并执行安全、有限的条件表达式。 */
function evaluateExpression(
expression: Expression,
context: Readonly<Record<string, string>>,
): boolean {
switch (expression.type) {
case 'equals':
return context[expression.field] === expression.value
case 'and':
return expression.expressions.every(item =>
evaluateExpression(item, context),
)
case 'or':
return expression.expressions.some(item =>
evaluateExpression(item, context),
)
case 'not':
return !evaluateExpression(expression.expression, context)
default: {
const exhaustiveCheck: never = expression
return exhaustiveCheck
}
}
}
禁止使用 eval 或 new Function 执行后端下发的表达式。生产系统需要限制递归深度、节点数量和可访问字段。
7. 前端特有的组合与架构模式
这些模式不都属于 GoF,但在现代前端中的使用频率更高。
7.1 依赖注入 Dependency Injection
依赖注入把“依赖什么”和“如何创建依赖”分开。
interface UserRepository {
getById(userId: string): Promise<User>
}
class UserService {
public constructor(private readonly repository: UserRepository) {}
/** 加载用户。 */
public getUser(userId: string): Promise<User> {
return this.repository.getById(userId)
}
}
收益:
- 测试可替换 Mock;
- 平台实现可替换;
- 业务逻辑不依赖请求库;
- 生命周期集中管理。
不要为每个纯函数引入 DI 容器。前端多数场景通过构造参数、函数参数、React Context 或 Vue provide/inject 即可。
7.2 Repository 模式
Repository 隔离领域模型与数据来源。
interface Product {
id: string
name: string
price: number
}
interface ProductRepository {
getById(productId: string): Promise<Product>
search(keyword: string): Promise<Product[]>
}
class HttpProductRepository implements ProductRepository {
public constructor(private readonly client: HttpClient) {}
/** 查询商品。 */
public async getById(productId: string): Promise<Product> {
const response: unknown = await this.client.get(`/products/${productId}`)
return parseProduct(response)
}
/** 搜索商品。 */
public async search(keyword: string): Promise<Product[]> {
const response: unknown = await this.client.get('/products', { keyword })
return parseProductList(response)
}
}
Repository 应返回领域模型,不应把 Axios Response、GraphQL Edge 或后端 DTO 传播到组件层。
7.3 DTO Mapper 模式
interface UserDto {
user_id: string
user_name: string
created_at: string
}
interface User {
id: string
name: string
createdAt: Date
}
/** 将后端 DTO 转换为前端领域模型。 */
function mapUserDto(dto: UserDto): User {
return {
id: dto.user_id,
name: dto.user_name,
createdAt: new Date(dto.created_at),
}
}
映射层应处理字段重命名、日期、单位、空值、枚举和兼容策略。后端契约统一后,应删除过期兼容分支,避免长期双写和静默兜底。
7.4 Service Layer
Service 负责业务用例和跨模块流程,不负责 UI。
class CreateOrderService {
public constructor(
private readonly orderRepository: OrderRepository,
private readonly inventoryService: InventoryService,
) {}
/** 创建订单并占用库存。 */
public async execute(input: CreateOrderInput): Promise<Order> {
await this.inventoryService.assertAvailable(input.items)
return this.orderRepository.create(input)
}
}
推荐依赖方向:
Page / Component
↓
Composable / Hook
↓
Application Service
↓
Repository / Adapter
↓
HTTP、SDK、Storage
7.5 单向数据流
State → View → Event → Action → New State
原则:
- 状态有唯一所有者;
- 子组件通过 Props 接收数据;
- 子组件通过事件表达意图;
- 不直接修改 Props;
- 可计算的数据使用派生状态,不重复存储。
7.6 Reducer 模式
Reducer 是状态模式和命令消息的函数式表达。
interface CartState {
items: Readonly<Record<string, number>>
}
type CartAction =
| { type: 'add'; skuId: string }
| { type: 'remove'; skuId: string }
| { type: 'clear' }
/** 计算购物车下一状态。 */
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'add':
return {
items: {
...state.items,
[action.skuId]: (state.items[action.skuId] ?? 0) + 1,
},
}
case 'remove': {
const nextItems = { ...state.items }
delete nextItems[action.skuId]
return { items: nextItems }
}
case 'clear':
return { items: {} }
default: {
const exhaustiveCheck: never = action
return exhaustiveCheck
}
}
}
Reducer 必须是纯函数:相同输入得到相同输出,不直接发送请求、写缓存或修改参数。
7.7 Composable / Custom Hook
用于复用有状态逻辑和生命周期逻辑,不规定 UI。
职责边界:
纯函数:无生命周期、无响应式状态
Composable / Hook:状态、订阅、生命周期
组件:渲染、交互和可访问性
Service:业务用例
Repository:数据访问
7.8 Headless Component
Headless 组件提供行为、状态和可访问性契约,把视觉实现交给业务。
适合:
- Select、Dialog、Tabs;
- Pagination;
- Upload;
- Data Table;
- Form;
- Combobox。
它通常组合了:状态模式、Provider、受控组件、组合组件和 Render Props/Scoped Slots。
7.9 受控与非受控组件
受控组件由父级拥有状态:
interface ControlledInputProps {
value: string
onChange(value: string): void
}
非受控组件由自身拥有状态,只提供初始值和结果事件:
interface UncontrolledInputProps {
defaultValue?: string
onCommit?(value: string): void
}
组件库可以同时支持两种模式,但必须定义:
value和defaultValue同时传入时如何处理;- 运行中能否从非受控切换为受控;
- 清空值与未传值的区别;
- 表单重置如何同步。
7.10 Compound Components
多个相关子组件通过上下文协作:
<Tabs value={activeTab} onValueChange={setActiveTab}>
<Tabs.List>
<Tabs.Trigger value="profile">个人信息</Tabs.Trigger>
<Tabs.Trigger value="security">安全设置</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="profile">...</Tabs.Content>
<Tabs.Content value="security">...</Tabs.Content>
</Tabs>
适用于 Tabs、Menu、Form、Select、Accordion。实现时必须处理注册顺序、键盘导航、焦点管理和无障碍属性。
7.11 Container / Presentational
容器负责数据和业务,展示组件负责渲染。
在 Hooks/Composable 普及后,不一定需要固定拆成两个组件;关键是保持:
- 展示组件不直接请求数据;
- 业务状态可以独立测试;
- UI 组件不依赖具体后端结构。
7.12 Cache-Aside
读取:缓存命中 → 返回
缓存未命中 → 请求远端 → 写缓存 → 返回
写入:写远端成功 → 失效或更新缓存
生产系统需要定义:
- 缓存键;
- 新鲜度和回收时间;
- 失败是否缓存;
- 并发去重;
- 乐观更新和回滚;
- 用户身份隔离;
- SSR 跨请求隔离。
7.13 MVVM 与组件架构
Vue 常被描述为 MVVM 风格,但工程中不必机械创建 models/、view-models/、views/。
可以把职责映射为:
Model:领域类型、Repository、Service
ViewModel:Composable、Store、页面状态
View:Vue 组件和模板
React 更常采用单向数据流和组件组合,而不是经典双向绑定式 MVVM。
8. React 中的设计模式映射
Context7 核对的 React 官方资料强调:组件和 Hooks 应保持纯净;Props 和 State 是单次渲染中的不可变快照;Effect 用于与外部系统同步,而不是默认的数据推导工具。
8.1 自定义 Hook:复用观察和订阅逻辑
import { useEffect, useState } from 'react'
interface WindowSize {
width: number
height: number
}
/** 订阅浏览器窗口尺寸。 */
export function useWindowSize(): WindowSize {
const [size, setSize] = useState<WindowSize>(() => ({
width: window.innerWidth,
height: window.innerHeight,
}))
useEffect(() => {
const handleResize = (): void => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
})
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
return size
}
这里 Effect 合理,因为它同步的是浏览器外部事件系统。
不要用 Effect 维护可直接计算的派生状态:
// 不推荐
const [fullName, setFullName] = useState('')
useEffect(() => {
setFullName(`${firstName} ${lastName}`)
}, [firstName, lastName])
// 推荐
const fullName = `${firstName} ${lastName}`
8.2 Context + Reducer:Provider、Reducer、命令消息
import {
createContext,
type Dispatch,
type PropsWithChildren,
useContext,
useReducer,
} from 'react'
interface Todo {
id: string
title: string
completed: boolean
}
type TodoAction =
| { type: 'added'; todo: Todo }
| { type: 'toggled'; todoId: string }
| { type: 'removed'; todoId: string }
const TodoStateContext = createContext<readonly Todo[] | undefined>(undefined)
const TodoDispatchContext = createContext<Dispatch<TodoAction> | undefined>(
undefined,
)
/** 计算 Todo 下一状态。 */
function todoReducer(state: readonly Todo[], action: TodoAction): Todo[] {
switch (action.type) {
case 'added':
return [...state, action.todo]
case 'toggled':
return state.map(todo =>
todo.id === action.todoId
? { ...todo, completed: !todo.completed }
: todo,
)
case 'removed':
return state.filter(todo => todo.id !== action.todoId)
default: {
const exhaustiveCheck: never = action
return exhaustiveCheck
}
}
}
/** 提供 Todo 状态和命令入口。 */
export function TodoProvider({ children }: PropsWithChildren): JSX.Element {
const [todos, dispatch] = useReducer(todoReducer, [])
return (
<TodoStateContext value={todos}>
<TodoDispatchContext value={dispatch}>
{children}
</TodoDispatchContext>
</TodoStateContext>
)
}
/** 获取 Todo 状态。 */
export function useTodos(): readonly Todo[] {
const value = useContext(TodoStateContext)
if (!value) throw new Error('useTodos 必须在 TodoProvider 内使用')
return value
}
/** 获取 Todo 命令分发器。 */
export function useTodoDispatch(): Dispatch<TodoAction> {
const value = useContext(TodoDispatchContext)
if (!value) throw new Error('useTodoDispatch 必须在 TodoProvider 内使用')
return value
}
生产边界:
- Context 值变化会影响消费者更新范围;
- 高频、大规模状态不应只靠一个巨型 Context;
- 状态和 Dispatch 分离可以减少不必要依赖;
- 服务端状态应优先交给查询缓存层,而不是手写全局 Store;
- Reducer 不执行请求,异步流程放在事件处理器、Service 或专用数据层。
8.3 组合优于继承
React 组件通过 children、Props 和 Slots 风格组合能力:
interface PermissionBoundaryProps extends PropsWithChildren {
allowed: boolean
fallback?: React.ReactNode
}
/** 根据权限决定是否渲染内容。 */
function PermissionBoundary({
allowed,
fallback = null,
children,
}: PermissionBoundaryProps): React.ReactNode {
return allowed ? children : fallback
}
不要创建多层组件继承体系。
8.4 外部 Store 适配
外部可变数据源应提供稳定的订阅和快照契约,再通过 React 对应订阅 API 接入。不要在渲染期间直接读取并修改外部 Store,也不要用 Effect 手工复制出第二份状态。
9. Vue 3 中的设计模式映射
Context7 核对的 Vue 官方资料将 Composable 定义为:使用 Composition API 封装和复用有状态逻辑的函数。每个组件调用 Composable 时默认获得独立状态,除非状态被放在模块作用域中共享。
9.1 Composable:封装观察者和生命周期
import { onMounted, onUnmounted, readonly, ref, type Ref } from 'vue'
interface MousePosition {
x: Readonly<Ref<number>>
y: Readonly<Ref<number>>
}
/** 订阅鼠标位置并在组件卸载时释放监听。 */
export function useMousePosition(): MousePosition {
const x = ref(0)
const y = ref(0)
const update = (event: MouseEvent): void => {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return {
x: readonly(x),
y: readonly(y),
}
}
无状态逻辑使用普通函数,不必包装成 Composable:
/** 格式化金额。 */
export function formatAmount(amount: number): string {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY',
}).format(amount)
}
9.2 provide/inject:依赖注入和 Provider
import {
inject,
provide,
readonly,
ref,
type InjectionKey,
type Ref,
} from 'vue'
interface CounterContext {
count: Readonly<Ref<number>>
increment(): void
}
const COUNTER_CONTEXT_KEY: InjectionKey<CounterContext> = Symbol('counter')
/** 在上层组件提供计数器上下文。 */
export function provideCounter(): void {
const count = ref(0)
provide(COUNTER_CONTEXT_KEY, {
count: readonly(count),
increment: () => {
count.value += 1
},
})
}
/** 获取计数器上下文。 */
export function useCounter(): CounterContext {
const context = inject(COUNTER_CONTEXT_KEY)
if (!context) {
throw new Error('useCounter 必须在 provideCounter 后使用')
}
return context
}
官方建议让状态修改函数留在 Provider 中,并向下提供只读状态,避免消费方任意修改共享状态。
9.3 Props Down / Events Up
<script setup lang="ts">
defineProps<{
value: string
}>()
const emit = defineEmits<{
change: [value: string]
}>()
</script>
<template>
<input
:value="value"
@input="emit('change', ($event.target as HTMLInputElement).value)"
/>
</template>
Props 表达数据,事件表达意图。避免子组件直接修改父级对象内部字段。
9.4 v-model:受控组件协议
<script setup lang="ts">
const modelValue = defineModel<string>({ required: true })
</script>
<template>
<input v-model="modelValue" />
</template>
复杂组件应使用具名 v-model 区分多个状态:
<DateRangePicker
v-model:start="startDate"
v-model:end="endDate"
/>
9.5 派生状态使用 computed
import { computed, ref } from 'vue'
const firstName = ref('Ada')
const lastName = ref('Lovelace')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
不要用 watch 把可计算数据复制到另一份 Ref。watch 更适合副作用、异步请求、日志和与外部系统同步。
9.6 共享状态边界
推荐顺序:
组件局部 ref/reactive
↓
父子 Props / Emits
↓
局部 Composable
↓
provide/inject
↓
模块级共享 Composable
↓
Pinia 等 Store
只有跨页面、跨模块且具有明确业务所有权的状态才进入全局 Store。
10. 综合案例:多端支付流程
这个案例组合:抽象工厂、适配器、策略、责任链、外观和依赖注入。
10.1 目标
支持:
- H5;
- 微信小程序;
- 支付宝小程序;
- 后续增加新的支付渠道;
- 统一业务错误;
- 支付前校验和防重复提交;
- 业务组件不直接调用平台原生 API。
10.2 领域契约
const PAYMENT_STATUS = {
SUCCESS: 'success',
CANCELLED: 'cancelled',
FAILED: 'failed',
} as const
type PaymentStatus =
(typeof PAYMENT_STATUS)[keyof typeof PAYMENT_STATUS]
interface PaymentRequest {
orderId: string
amount: number
}
interface PaymentResult {
status: PaymentStatus
transactionId?: string
}
interface PaymentGateway {
pay(request: PaymentRequest): Promise<PaymentResult>
}
10.3 平台适配器
interface AlipaySdk {
tradePay(options: {
tradeNO: string
success(result: unknown): void
fail(error: unknown): void
}): void
}
class AlipayPaymentAdapter implements PaymentGateway {
public constructor(private readonly sdk: AlipaySdk) {}
/** 调用支付宝支付并转换为领域结果。 */
public pay(request: PaymentRequest): Promise<PaymentResult> {
return new Promise((resolve, reject) => {
this.sdk.tradePay({
tradeNO: request.orderId,
success: rawResult => resolve(parseAlipayPaymentResult(rawResult)),
fail: rawError => reject(normalizePaymentError(rawError)),
})
})
}
}
10.4 支付前责任链
interface PaymentContext {
request: PaymentRequest
userId: string
}
type PaymentGuard = (
context: PaymentContext,
next: () => Promise<PaymentResult>,
) => Promise<PaymentResult>
const validateAmountGuard: PaymentGuard = async (context, next) => {
if (!Number.isFinite(context.request.amount) || context.request.amount <= 0) {
throw new Error('支付金额不合法')
}
return next()
}
/** 创建防重复支付守卫。 */
function createDeduplicateGuard(): PaymentGuard {
const pendingOrders = new Set<string>()
return async (context, next) => {
const { orderId } = context.request
if (pendingOrders.has(orderId)) {
throw new Error('订单正在支付,请勿重复操作')
}
pendingOrders.add(orderId)
try {
return await next()
} finally {
pendingOrders.delete(orderId)
}
}
}
10.5 支付外观
class PaymentFacade {
public constructor(
private readonly gateway: PaymentGateway,
private readonly guards: PaymentGuard[],
) {}
/** 执行完整支付流程。 */
public pay(context: PaymentContext): Promise<PaymentResult> {
const dispatch = (index: number): Promise<PaymentResult> => {
const guard = this.guards[index]
if (!guard) {
return this.gateway.pay(context.request)
}
return guard(context, () => dispatch(index + 1))
}
return dispatch(0)
}
}
10.6 关键边界
- 前端金额仅用于展示和初步校验,最终金额由服务端确认;
- 不在前端保存支付密钥;
- 支付成功以后仍需要服务端查单确认;
- “用户取消”和“支付失败”是不同业务状态;
- 小程序回调成功不等于订单最终完成;
- 防重复集合只在当前 JS Runtime 生效,服务端仍必须保证幂等;
- 平台 SDK 原始响应只存在于 Adapter 内。
11. 综合案例:可撤销的页面编辑器
组合模式负责节点树,命令模式负责操作,备忘录负责复杂快照,访问者负责导出和校验。
Editor
├── DocumentTree(组合模式)
├── CommandHistory(命令模式)
├── SnapshotStore(备忘录)
└── ExportVisitor / ValidateVisitor(访问者模式)
11.1 节点模型
type EditorNode =
| { id: string; type: 'text'; content: string }
| { id: string; type: 'image'; src: string; alt: string }
| { id: string; type: 'container'; children: EditorNode[] }
11.2 修改命令
class ReplaceNodeCommand implements Command {
private previousNode: EditorNode | undefined
public constructor(
private readonly documentModel: EditorDocument,
private readonly nodeId: string,
private readonly nextNode: EditorNode,
) {}
/** 替换节点并保存旧节点。 */
public execute(): void {
this.previousNode = this.documentModel.getNode(this.nodeId)
this.documentModel.replaceNode(this.nodeId, this.nextNode)
}
/** 恢复旧节点。 */
public undo(): void {
if (!this.previousNode) {
throw new Error('命令尚未执行,无法撤销')
}
this.documentModel.replaceNode(this.nodeId, this.previousNode)
}
}
11.3 生产边界
- 输入频率高的文本编辑应合并命令,避免每个字符产生一条历史;
- 图片、视频等大对象只保存引用,不复制二进制;
- 历史记录需要上限和内存监控;
- 协同编辑不能直接把本地 Undo 栈当成全局历史;
- 持久化命令必须包含 Schema 版本;
- 远程命令必须进行权限和数据校验。
12. 测试策略
设计模式的测试重点是契约和组合顺序,而不是覆盖类名。
12.1 策略模式
使用表驱动测试:
interface PriceCase {
name: PriceStrategyName
context: PriceContext
expected: number
}
const cases: PriceCase[] = [
{
name: 'normal',
context: { amount: 100, isFirstOrder: false },
expected: 100,
},
{
name: 'vip',
context: { amount: 100, isFirstOrder: false },
expected: 90,
},
]
12.2 适配器模式
验证:
- 原始字段是否正确转换;
- 缺失字段是否报错;
- 错误码是否统一;
- 回调是否只结算一次;
- 超时和取消是否透传。
12.3 责任链和装饰器
验证执行顺序:
auth before
cache before
transport
cache after
auth after
还要覆盖:
- 中途抛错后后续节点是否停止;
finally是否执行;- 重试是否重复触发不可重入操作;
AbortSignal是否传递到真实请求。
12.4 状态模式和 Reducer
测试合法和非法转换:
idle → uploading:允许
uploading → paused:允许
success → progress:拒绝或忽略
failed → reset:允许
12.5 Observer 和 Pub/Sub
验证:
- 取消订阅后不再接收事件;
- 同一监听器是否允许重复注册;
- 一个监听器抛错是否影响其他监听器;
- 组件卸载后是否泄漏;
- 事件负载是否经过类型和运行时校验。
12.6 测试替身
优先通过接口注入 Fake:
class FakeProductRepository implements ProductRepository {
public constructor(private readonly products: Product[]) {}
/** 从内存中查找商品。 */
public async getById(productId: string): Promise<Product> {
const product = this.products.find(item => item.id === productId)
if (!product) throw new Error('商品不存在')
return product
}
/** 从内存中搜索商品。 */
public async search(keyword: string): Promise<Product[]> {
return this.products.filter(product => product.name.includes(keyword))
}
}
相比对模块内部实现做大量 Mock,Fake 更接近真实契约,也更不容易因重构失效。
13. 常见反模式
13.1 模式驱动开发
先决定“我要用工厂模式”,再找问题套进去。正确顺序是先识别变化,再选择最小抽象。
13.2 一个接口只有一个实现
如果没有替换、测试或边界隔离需求,接口可能只是增加跳转成本。
13.3 God Service / God Facade
表现:
- 一个 Service 注入十几个依赖;
- 同时处理请求、状态、权限、埋点和 UI;
- 任何功能都要修改它。
修复:按业务用例和领域边界拆分,不按技术动作机械拆分。
13.4 EventBus 滥用
所有组件都通过字符串事件通信,导致:
- 来源不明;
- 时序不明;
- 重命名不安全;
- 页面卸载后泄漏;
- 调试依赖全局日志。
优先 Props/Events、Context、provide/inject 或 Store。EventBus 只用于明确的领域事件和跨边界消息。
13.5 条件分支伪装成策略
把原来的 switch 移到工厂里,但所有策略仍共享大量条件和可变全局状态,复杂度并未降低。策略应该有独立契约、独立测试和明确输入输出。
13.6 继承层级过深
前端 UI 需求组合变化频繁,深继承容易产生脆弱基类问题。优先使用 Props、Slots、Hooks、Composable 和依赖注入。
13.7 重复保存派生状态
原始状态 + 过滤结果 + 数量 + 是否为空
如果后 3 个都能计算,就不应分别保存。React 在渲染中计算,Vue 使用 computed。
13.8 Effect / Watch 作为默认业务编排器
Effect 和 Watch 适合与外部系统同步,不适合维护组件内部可以直接计算的数据流。过多监听器会造成循环更新、竞态和难以复现的时序问题。
13.9 全局单例保存用户态数据
SSR、微前端和测试环境中容易污染。用户态数据必须绑定到请求、应用实例或明确 Store 生命周期。
13.10 兼容层永久存在
短期 Adapter 同时读取新旧字段可以降低迁移风险,但后端契约统一后必须删除旧分支,并搜索下游等价兜底。永久兼容会掩盖数据错误。
13.11 类型断言代替校验
const payload = JSON.parse(raw) as PaymentResult
这不会保护运行时。边界数据应先作为 unknown,再通过 Schema 或类型守卫验证。
14. 模式选型速查表
| 问题 | 优先考虑 | 前端例子 |
|---|---|---|
| 创建哪一种实现 | 工厂方法 | 文件预览器、支付渠道 |
| 创建一整套平台能力 | 抽象工厂 | H5/微信/支付宝能力组 |
| 分步骤构建复杂配置 | Builder | 查询、图表、表单 Schema |
| 从模板复制复杂对象 | Prototype | 页面搭建器复制组件 |
| 统一第三方不兼容接口 | Adapter | 地图、上传、支付 SDK |
| 拆开两个独立变化维度 | Bridge | 通知类型 × 发送渠道 |
| 统一处理树的叶子和容器 | Composite | 菜单、路由、AST |
| 动态增加横切能力 | Decorator | 日志、监控、缓存 |
| 简化复杂子系统入口 | Facade | 登录、结算、上传 |
| 共享大量不可变数据 | Flyweight | Canvas 样式、Marker 图标 |
| 控制真实对象访问 | Proxy | 缓存、权限、懒加载 |
| 顺序执行多个处理器 | Chain | 请求中间件、表单校验 |
| 操作需要撤销或排队 | Command | 编辑器、离线队列 |
| 惰性遍历集合 | Iterator | 树、分页数据流 |
| 多对象依赖过于网状 | Mediator | 复杂筛选面板 |
| 保存并恢复状态 | Memento | 编辑器历史、草稿 |
| 状态变化通知订阅者 | Observer | 响应式、DOM Observer |
| 跨边界事件解耦 | Pub/Sub | 微前端、领域事件 |
| 行为依赖当前状态 | State | 上传、订单、播放器 |
| 算法需要运行时替换 | Strategy | 优惠、排序、重试 |
| 流程稳定、步骤可变 | Template Method | 上传、导入导出 |
| 稳定节点增加新操作 | Visitor | AST 导出、Schema 校验 |
| 执行有限规则语言 | Interpreter | 权限和表单规则 DSL |
| 隔离业务与数据来源 | Repository | REST/GraphQL/IndexedDB |
| 复用有状态 UI 逻辑 | Hook/Composable | 请求、订阅、分页 |
| 行为复用但 UI 自定义 | Headless Component | Select、Dialog、Table |
| 深层组件共享依赖 | Provider/DI | 表单、主题、权限 |
| 复杂状态集中转换 | Reducer | 表单、购物车、编辑器 |
15. 工程落地检查清单
15.1 设计前
- 是否明确了真正的变化轴?
- 当前已有多少个实现?
- 普通函数或映射表是否已经足够?
- 抽象是否降低未来修改范围?
- 生命周期和资源所有权是否明确?
15.2 类型设计
- 外部数据是否先使用
unknown? - 状态是否可以使用判别联合?
-
switch是否有never穷尽检查? - 是否避免裸
any? - 泛型是否表达真实关系,而不是增加复杂度?
15.3 模块边界
- 组件是否直接依赖第三方 SDK?
- DTO 是否泄漏到整个 UI?
- Service、Repository、Adapter 的职责是否清晰?
- 是否存在循环依赖?
- 平台差异是否集中处理?
15.4 状态和事件
- 状态是否有唯一所有者?
- 派生状态是否通过计算得到?
- EventBus 是否真的跨越了合理边界?
- 订阅是否在卸载时释放?
- 异步请求是否支持取消和竞态保护?
15.5 生产可靠性
- 请求是否有超时、错误归一化和幂等策略?
- 缓存是否隔离用户身份?
- SSR 是否存在跨请求单例污染?
- 小程序平台 API 是否通过适配层调用?
- 支付和订单最终状态是否以服务端为准?
- 旧兼容路径是否有明确删除计划?
15.6 测试
- 每个实现是否通过同一组契约测试?
- 策略是否使用表驱动测试?
- 中间件顺序是否被测试固定?
- Reducer 是否覆盖非法状态转换?
- Adapter 是否覆盖异常和缺失字段?
- 是否区分静态检查、单元测试和真实平台验收?
16. 参考资料
16.1 经典设计模式
- Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides,《Design Patterns: Elements of Reusable Object-Oriented Software》。
- Martin Fowler,《Patterns of Enterprise Application Architecture》。
- Martin Fowler,《Refactoring: Improving the Design of Existing Code》。
16.2 Context7 核对的官方资料
TypeScript
- Context7 Library ID:
/microsoft/typescript-website - TypeScript Handbook:https://www.typescriptlang.org/docs/handbook/intro.html
- Narrowing 与
never穷尽检查:https://www.typescriptlang.org/docs/handbook/2/narrowing.html - Classes:https://www.typescriptlang.org/docs/handbook/2/classes.html
- Generics:https://www.typescriptlang.org/docs/handbook/2/generics.html
本文据此核对了:接口实现、私有构造函数、unknown、类型守卫、判别联合与 never 穷尽检查。
React
- Context7 Library ID:
/reactjs/react.dev - React 官方文档:https://react.dev/
- Sharing State Between Components:https://react.dev/learn/sharing-state-between-components
- Reusing Logic with Custom Hooks:https://react.dev/learn/reusing-logic-with-custom-hooks
- Scaling Up with Reducer and Context:https://react.dev/learn/scaling-up-with-reducer-and-context
- You Might Not Need an Effect:https://react.dev/learn/you-might-not-need-an-effect
- Rules of React:https://react.dev/reference/rules
本文据此核对了:状态唯一来源、自定义 Hook、Context + Reducer、组件和 Hook 纯净性,以及 Effect 只用于与外部系统同步的边界。
Vue 3
- Context7 Library ID:
/websites/vuejs_guide - Vue 官方指南:https://vuejs.org/guide/introduction.html
- Composables:https://vuejs.org/guide/reusability/composables.html
- Provide / Inject:https://vuejs.org/guide/components/provide-inject.html
- Component
v-model:https://vuejs.org/guide/components/v-model.html - State Management:https://vuejs.org/guide/scaling-up/state-management.html
本文据此核对了:Composable 的定义和生命周期清理、provide/inject 的只读状态与修改入口、组件双向绑定协议及共享状态边界。
结语
设计模式的价值不是让代码看起来“更高级”,而是让变化发生在正确的位置:
平台变化 → Adapter / Abstract Factory
算法变化 → Strategy
流程变化 → Chain / Template Method / Facade
状态变化 → State / Reducer
结构变化 → Composite
横切能力 → Decorator / Proxy
数据来源变化 → Repository
UI 逻辑复用 → Hook / Composable / Headless Component
真正成熟的前端设计通常不是单独使用某个模式,而是组合多个小模式,同时保持依赖方向、状态所有权、运行时边界和测试契约清晰。