设计模式在 JS 中的应用:用 TS 重写经典模式,结合 React / Vue 真实业务场景

设计模式在 JS 中的应用:用 TS 重写经典模式,结合 React / Vue 真实业务场景

很多人学设计模式时都有一种感觉: "代码是看懂了,但不知道往哪用。"

问题不在模式本身,而在教材还在用 Java 写鸭子叫、披萨店。本文用 TypeScript 5.x​ 重写 6 个最高频的经典模式,每一个都绑定 React / Vue 的真实业务场景------看完你能直接抄进项目里。


一、为什么前端需要设计模式(而且要用 TS)

前端项目的复杂度早就不是"页面 + 接口"了。状态管理、缓存策略、组件通信、权限控制、埋点监控------这些问题的结构和后端一模一样,只是场景不同。

TypeScript 的价值在于:模式依赖抽象,抽象依赖类型系统。没有类型约束的"模式"只是约定,有了 TS,编译器帮你兜底。

下面进入正题。


二、策略模式(Strategy):表单校验的终极写法

经典意图

定义一系列算法,让它们可以互相替换,客户端不感知具体实现。

场景:一个支持多种校验规则的表单

typescript 复制代码
// 策略接口
interface ValidationStrategy<T = unknown> {
  validate: (value: T) => string | null; // 返回错误信息,null 表示通过
}

// 具体策略
class RequiredStrategy implements ValidationStrategy<string> {
  validate(value: string) {
    return value.trim() ? null : "不能为空";
  }
}

class MinLengthStrategy implements ValidationStrategy<string> {
  constructor(private min: number) {}
  validate(value: string) {
    return value.length >= this.min ? null : `至少 ${this.min} 个字符`;
  }
}

class EmailStrategy implements ValidationStrategy<string> {
  private regex = /^[^\s@]+@[^\s@]+.[^\s@]+$/;
  validate(value: string) {
    return this.regex.test(value) ? null : "邮箱格式不正确";
  }
}

// 上下文:组合使用
class Validator<T> {
  private strategies: ValidationStrategy<T>[] = [];
  add(s: ValidationStrategy<T>) { this.strategies.push(s); return this; }
  validate(value: T): string | null {
    for (const s of this.strategies) {
      const err = s.validate(value);
      if (err) return err;
    }
    return null;
  }
}

React 中的用法

typescript 复制代码
function LoginForm() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string | null>(null);

  const validator = useMemo(() =>
    new Validator<string>()
      .add(new RequiredStrategy())
      .add(new EmailStrategy()),
  []);

  const onSubmit = () => {
    const err = validator.validate(email);
    setError(err);
    if (!err) submit(email);
  };

  return (
    <>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      {error && <span className="error">{error}</span>}
      <button onClick={onSubmit}>提交</button>
    </>
  );
}

Vue 中的用法

csharp 复制代码
// composables/useValidator.ts
export function useValidator<T>(strategies: ValidationStrategy<T>[]) {
  const validator = new Validator<T>();
  strategies.forEach(s => validator.add(s));
  return { validate: validator.validate.bind(validator) };
}

// 组件
const { validate } = useValidator([
  new RequiredStrategy(),
  new EmailStrategy(),
]);
const error = ref<string | null>(null);
const onSubmit = () => { error.value = validate(email.value); };

为什么不用 if-else? ​ 校验规则会不断增长。策略模式让"加规则"变成"加一个类",不改原有代码------符合开闭原则。


三、观察者模式(Observer):跨组件事件总线

经典意图

一对多依赖,一个对象状态变化,所有依赖者自动收到通知。

场景:微前端 / 跨路由 tab 的全局消息通知

typescript 复制代码
// 泛型事件总线
type Handler<T = void> = (payload: T) => void;

class EventBus {
  private handlers = new Map<string, Set<Handler<any>>>();

  on<K extends string, P = unknown>(event: K, handler: Handler<P>): () => void {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event)!.add(handler);
    // 返回取消订阅函数------防止内存泄漏
    return () => this.off(event, handler);
  }

  off<K extends string>(event: K, handler: Handler) {
    this.handlers.get(event)?.delete(handler);
  }

  emit<K extends string, P = unknown>(event: K, payload: P) {
    this.handlers.get(event)?.forEach(h => h(payload));
  }
}

export const bus = new EventBus();

React Hook 封装

csharp 复制代码
function useEventBus<K extends string, P>(event: K, handler: Handler<P>) {
  useEffect(() => bus.on(event, handler), [event, handler]);
}

Vue Composable 封装

scss 复制代码
export function useEventListener<K extends string, P>(
  event: K, handler: Handler<P>
) {
  onMounted(() => bus.on(event, handler));
  onUnmounted(() => bus.off(event, handler));
}

对比 Pinia / Redux:事件总线适合"通知型"通信(如"用户登录了"、"主题切换了"),不适合共享状态本身。选对工具。


四、单例模式(Singleton):全局配置 & 服务实例

经典意图

保证一个类只有一个实例,提供全局访问点。

场景:API 客户端、WebSocket 连接、日志服务

typescript 复制代码
class ApiClient {
  private static instance: ApiClient;
  private token = "";

  private constructor() {} // 私有构造,禁止 new

  static getInstance(): ApiClient {
    if (!ApiClient.instance) {
      ApiClient.instance = new ApiClient();
    }
    return ApiClient.instance;
  }

  setToken(t: string) { this.token = t; }

  async request<T>(url: string): Promise<T> {
    return fetch(url, { headers: { Authorization: `Bearer ${this.token}` } })
      .then(r => r.json());
  }
}

// 使用
const api = ApiClient.getInstance();

TS 5.x 更优雅的写法:satisfies + 对象字面量

typescript 复制代码
// 很多"单例"其实不需要 class,一个模块作用域对象就够了
export const apiClient = {
  token: "",
  setToken(t: string) { this.token = t; },
  async request<T>(url: string): Promise<T> { /* ... */ },
} satisfies Record<string, unknown>;

// 配合 import type 做 tree-shake 友好导出

注意:单例的代价是全局状态。测试时难 mock,SSR 时注意实例生命周期。


五、工厂模式(Factory):组件动态渲染

经典意图

把对象的创建逻辑封装起来,调用方不关心具体类型。

场景:后台管理系统的表单生成器 / 低代码编辑器

typescript 复制代码
// 产品接口
interface FormField {
  render(): VNode; // Vue 用 VNode,React 用 ReactNode
  getValue(): unknown;
}

// 具体产品
class InputField implements FormField {
  constructor(private props: { label: string; placeholder?: string }) {}
  render() { /* 返回输入框 */ }
  getValue() { return ""; }
}

class SelectField implements FormField {
  constructor(private props: { label: string; options: string[] }) {}
  render() { /* 返回下拉框 */ }
  getValue() { return ""; }
}

// 工厂
class FieldFactory {
  private constructors = new Map<string, new (...a: any[]) => FormField>();

  register(type: string, ctor: new (...a: any[]) => FormField) {
    this.constructors.set(type, ctor);
  }

  create(config: { type: string; [k: string]: any }): FormField {
    const Ctor = this.constructors.get(config.type);
    if (!Ctor) throw new Error(`Unknown field type: ${config.type}`);
    return new Ctor(config);
  }
}

// 注册
const factory = new FieldFactory();
factory.register("input", InputField);
factory.register("select", SelectField);

// 使用:配置驱动
const schema = [
  { type: "input", label: "姓名", placeholder: "请输入" },
  { type: "select", label: "城市", options: ["北京", "上海"] },
];
const fields = schema.map(s => factory.create(s));

React 中的等价思路 :用 componentMap 对象替代 class 工厂

typescript 复制代码
const componentMap = {
  input: InputComponent,
  select: SelectComponent,
} as const;

type FieldType = keyof typeof componentMap;

function renderField(type: FieldType, props: any) {
  const Comp = componentMap[type];
  return <Comp {...props} />;
}

六、装饰器模式(Decorator):权限、日志、缓存

经典意图

动态地给对象添加职责,不改变原有结构。

TS 5.x 场景:方法装饰器做权限校验

typescript 复制代码
// 权限装饰器
function RequirePermission(permission: string) {
  return function (
    target: any,
    context: ClassMethodDecoratorContext
  ) {
    return function (this: any, ...args: any[]) {
      if (!this.currentUser?.permissions?.includes(permission)) {
        throw new Error(`缺少权限: ${permission}`);
      }
      return target.apply(this, args);
    };
  };
}

class UserService {
  currentUser: { permissions: string[] } | null = null;

  @RequirePermission("user:delete")
  deleteUser(id: string) {
    console.log("删除用户", id);
  }
}

React 中的高阶组件(HOC)------装饰器的组件版

javascript 复制代码
// 日志装饰
function withLogger<P extends object>(
  Wrapped: ComponentType<P>
) {
  return function WithLogger(props: P) {
    useEffect(() => {
      console.log("mounted", Wrapped.name);
      return () => console.log("unmounted", Wrapped.name);
    }, []);
    return <Wrapped {...props} />;
  };
}

// 使用
const DashboardWithLog = withLogger(Dashboard);

Vue 中的等价写法:自定义指令

ini 复制代码
// 权限指令 = 装饰器思路
app.directive("permission", {
  mounted(el, binding) {
    const { value } = binding;
    const hasPerm = checkPermission(value);
    if (!hasPerm) el.parentNode?.removeChild(el);
  },
});

// 使用
// <button v-permission="'user:delete'">删除</button>

七、发布-订阅 + 命令模式组合:撤销/重做

场景:富文本编辑器、画布工具、表单撤销

kotlin 复制代码
// 命令接口
interface Command {
  execute(): void;
  undo(): void;
}

// 具体命令
class AddTextCommand implements Command {
  constructor(
    private editor: TextEditor,
    private text: string,
    private pos: number
  ) {}
  execute() { this.editor.insert(this.text, this.pos); }
  undo() { this.editor.delete(this.pos, this.text.length); }
}

// 调用者 + 历史栈
class CommandManager {
  private history: Command[] = [];
  private index = -1;

  execute(cmd: Command) {
    cmd.execute();
    this.history = this.history.slice(0, this.index + 1); // 截断重做栈
    this.history.push(cmd);
    this.index++;
  }

  undo() {
    if (this.index < 0) return;
    this.history[this.index].undo();
    this.index--;
  }

  redo() {
    if (this.index >= this.history.length - 1) return;
    this.index++;
    this.history[this.index].execute();
  }
}

React 中接 useUndo hook,Vue 中接 useRef + reactive------模式是框架无关的,只是胶水不同。


八、怎么判断"该用模式了"

信号 该考虑的模式
if (type === 'A') ... else if (type === 'B') ... 超过 3 个分支 策略 / 工厂
一个状态变化要通知多个不相关的模块 观察者 / 发布订阅
全局只需要一个实例,但到处都在 new 单例
组件/函数需要动态加功能(日志、权限、缓存) 装饰器
用户操作需要撤销 命令
对象创建很复杂,调用方不需要知道细节 工厂 / 建造者

九、一句话总结

设计模式不是"炫技",是"把变化封装起来"。 ​ 用 TS 写模式,类型系统帮你守住边界;绑到 React / Vue 场景里,模式才真正活起来。别背 23 个模式的名字,记住上面这 6 个,遇到对应信号直接套------这就是老手和新手的分水岭。

相关推荐
MacroZheng4 小时前
同事问我:"Claude Code经常失忆,不怕它把项目搞炸?",我:"怕,三个Markdown文件给它装个永不丢失的外置大脑!"
java·人工智能·后端
用户29975997127074 小时前
深分页的四种解法,以及为什么说它多半是个伪需求
后端
苏三说技术5 小时前
为什么越来越多人用 OpenSearch?
后端
大白805 小时前
JS 内存泄漏排查:用真实案例教你如何用 Chrome DevTools 定位并解决,顺带搞懂闭包和 GC
后端
泡海椒5 小时前
Lambda 极简调用:不定义接口直接执行 curl 命令,JQuick-Curl 让 Java HTTP 调用更短更快
后端
步行cgn5 小时前
Maven 中:作为父项目和作为依赖的本质区别
后端
65岁退休Coder7 小时前
LangGraph v1.2.9 节点容错策略 & 流式输出 & 持久化记忆管理
后端·python·langchain
元界metalite9 小时前
SpringBoot整合RocketMQ-毒丸消息还要无限重试吗
后端
SimonKing9 小时前
升级Spring Boot 4后,从 Jackson 2 到 3,到底有哪些变化
java·后端·程序员