JS 代码技巧 vol.9 — 20 个设计模式在真实项目里的应用

JS 代码技巧 vol.9 --- 20 个设计模式在真实项目里的应用

小不的代码技巧系列第 9 期 主题:设计模式实战 这次盘 20 个------每个都对应一个真实 GitHub 仓库的具体源码,看完你也能在项目里复用 😎 20 个技巧一次打包,分创建型、结构型、行为型、其他四组。

哈喽哇!我是小不不简说的不~

作为一个在代码界"翻车"无数次的选手,我算是看明白了:坑这东西吧,要么不踩,踩就踩大的😂

今天这期,主打一个 设计模式实战------不整虚的,全是真实 GitHub 项目里扒出来的实战用法。看不看随你~反正翻车实录又不收钱

Ps:文末有惊喜(不是广告!)


🚨 翻车名场面(不用设计模式的代价)

以下是真实项目里"裸写"出来的反例------作者已跑路,bug 已遗留

名场面 1:把全局变量当单例用

js 复制代码
// ❌ 全局对象 = "我的"单例
window.myConfig = {
  api: "https://api.example.com",
  timeout: 5000,
};
// 同事 A 改了 api,同事 B 改了 timeout
// 排查:"我刚跑还是好的?" ------ 因为你俩改的

名场面 2:一个函数搞定 5 种业务

js 复制代码
// ❌ 上古神兽"万能函数"
function handle(type, data) {
  if (type === "login") {
    /* 200 行 */
  } else if (type === "logout") {
    /* 200 行 */
  } else if (type === "pay") {
    /* 200 行 */
  } else if (type === "refund") {
    /* 200 行 */
  } else if (type === "comment") {
    /* 200 行 */
  }
}
// 加新业务?Ctrl+C/V 吧(叹气)

名场面 3:3 个 tab 抢同一个全局事件

js 复制代码
// ❌ 谁都监听,谁都改
window.addEventListener("user-login", (e) => {
  // tab A 更新头像
  // tab B 更新通知
  // tab C 更新购物车
  // 顺序乱、数据乱、调试乱
});

看完这 3 个,是不是想"我项目里也这样"------别慌,学完这期你就能开刀了


一、创建型(4 个)

1. 单例模式:Pinia / Vue Router

一句话:整个应用只允许一个实例。

真实开源项目Pinia 是 Vue 3 官方推荐的状态库,每个 store 都是单例。

js 复制代码
// Pinia 源码简化:packages/pinia/src/store.ts
export function defineStore(id, setup) {
  // 全局缓存:同一个 id 只创建一次
  if (!stores.has(id)) {
    stores.set(id, createSetupStore(id, setup));
  }
  return stores.get(id);
}

// 业务用法
export const useUserStore = defineStore("user", () => {
  const name = ref("小不");
  return { name };
});
// 任何组件 import useUserStore(),拿到的是同一个实例

自己用 :登录用户、主题、全局配置------只该有一份的东西都用单例

坑在哪 :单例的"全局状态"会让测试变难(互相污染),必要时提供 reset 方法 。前端单例用 ES Module 天然就够了,别再写 class 加 getInstance


2. 工厂模式:React.createElement

一句话:调用一个函数,根据参数返回不同对象------不暴露创建细节。

真实开源项目ReactReact.createElement 是经典工厂。

js 复制代码
// React 源码:packages/react/src/ReactElement.js
export function createElement(type, config, children) {
  return {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    props: { ...config, children },
    // ...
  };
}

// 你写的 JSX
const el = (
  <div className="box" onClick={fn}>
    Hello
  </div>
);
// 编译后 = React.createElement('div', { className: 'box', onClick: fn }, 'Hello')

自己用:UI 组件库的"创建组件"函数、tooltip/popover 的"创建浮层"。

js 复制代码
function createToast(options) {
  return { id: Date.now(), type: "info", ...options };
}
const t1 = createToast({ message: "保存成功" });
const t2 = createToast({ type: "error", message: "网络错误" });

坑在哪 :工厂返回的对象结构要稳定 (React 元素就是固定结构),不然下游逻辑全乱。简单工厂 + 命名约定 > 复杂抽象。


3. 策略模式:dayjs / 插件系统

一句话:定义一族算法,运行时可互换。

真实开源项目dayjs 的插件机制就是策略模式。

js 复制代码
// dayjs 源码:src/index.js
dayjs.extend(utcPlugin); // 策略 1:UTC
dayjs.extend(relativeTime); // 策略 2:相对时间
dayjs.extend(customParseFormat); // 策略 3:自定义解析

// 用法:根据加载的插件决定 dayjs() 行为
const now = dayjs();
now.utc().format(); // UTC 模式
now.fromNow(); // 相对时间(如果加载了插件)

自己用 :表单验证、支付方式、动画曲线、压缩算法------if-else 套娃的克星

js 复制代码
const validators = {
  email: (v) => /\S+@\S+\.\S+/.test(v),
  phone: (v) => /^1\d{10}$/.test(v),
  idCard: (v) => /(^\d{15}$)|(^\d{17}([0-9]|X)$)/.test(v),
};
function validate(type, value) {
  return validators[type]?.(value) ?? false;
}

坑在哪 :策略表用 key-value 形式而不是 switch ------加新策略不动旧代码。TS 项目给 key 加类型约束避免拼错。


4. 抽象工厂:UI 组件库 antd / element-plus

一句话:工厂的工厂------创建"一族相关对象"。

真实开源项目Ant Design 一键切换中英文,靠的就是 i18n 抽象工厂。

js 复制代码
// Ant Design 源码:components/locale-provider/
import zhCN from "./locale/zh_CN";
import enUS from "./locale/en_US";

const locales = { zhCN, enUS };

export function getLocale(name) {
  // 抽象工厂:根据 name 返回一整套本地化配置
  return locales[name] ?? locales.zhCN;
}

// 用法:ConfigProvider 切换整套文案
<ConfigProvider locale={getLocale("enUS")}>
  <App />
</ConfigProvider>;

自己用:多主题(暗/亮)、多平台(H5/小程序/原生)、多租户 UI 适配。

坑在哪 :抽象工厂容易过度设计 ------业务只有 2 个变体就别上。先看变化维度,再决定要不要抽象


二、结构型(5 个)

5. 适配器模式:axios adapter

一句话:把一个接口"翻译"成另一个接口。

真实开源项目axios 的 adapter 设计堪称教科书。

js 复制代码
// axios 源码:lib/adapters/index.js
const adapters = {
  http: require("./adapters/http"), // 浏览器 fetch / Node http
  xhr: require("./adapters/xhr"), // 浏览器 XMLHttpRequest
};

// 自动选择
function getDefaultAdapter() {
  if (typeof XMLHttpRequest !== "undefined") return adapters.xhr;
  if (typeof process !== "undefined") return adapters.http;
}

// 用户也能注入自己的 adapter(微信小程序、鸿蒙、Service Worker...)
const instance = axios.create({
  adapter: wxRequestAdapter, // 适配微信小程序
});

自己用:跨端代码(H5 → 小程序 → 桌面端)、新旧接口过渡、第三方 SDK 包装。

坑在哪 :adapter 接口要稳定 (axios 的 adapter(config) 返回 Promise),下游才能透明切换。


6. 装饰器模式:MobX / TC39 装饰器提案

一句话:不修改原对象,动态加功能。

真实开源项目MobX 大量用装饰器(也是 TC39 提案的主要推动者)。

js 复制代码
// MobX 用法
import { observable, computed, action } from "mobx";

class TodoStore {
  @observable list = []; // 装饰:自动追踪
  @computed get unfinished() {
    // 装饰:自动缓存
    return this.list.filter((t) => !t.done);
  }
  @action add(text) {
    // 装饰:批处理
    this.list.push({ text, done: false });
  }
}

// 等价非装饰器写法
class TodoStore {
  list = observable.box([]);
  // ...
}

自己用:AOP 切面(日志、鉴权、计时)、React HOC、高阶函数本身就是装饰器。

js 复制代码
function withLog(fn) {
  return (...args) => {
    console.log("调用:", fn.name, args);
    const result = fn(...args);
    console.log("返回:", result);
    return result;
  };
}

坑在哪 :装饰器改变 fn.name ,调试栈追踪可能丢名字。TC39 装饰器还在提案阶段,Babel 还在转译层(Stage 3),生产看目标浏览器再决定用不用。


7. 代理模式:Vue 3 reactivity

一句话:用一个对象"代理"另一个对象,控制访问。

真实开源项目Vue 3 的 reactivity 是 Proxy 的教科书用法。

js 复制代码
// Vue 3 源码:packages/reactivity/src/reactive.ts
export function reactive(target) {
  // 已经是 reactive 的直接返回(避免重复代理)
  if (isReactive(target)) return target;
  return createReactiveObject(
    target,
    mutableHandlers, // 拦截 get/set/deleteProperty 等
    reactiveMap,
  );
}

// mutableHandlers 简化
const mutableHandlers = {
  get(target, key, receiver) {
    track(target, key); // 依赖收集
    return Reflect.get(target, key, receiver);
  },
  set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    trigger(target, key); // 触发更新
    return result;
  },
};

自己用:只读视图、属性校验、对象快照、Vue 响应式。

坑在哪 :Proxy 不能完全替代 defineProperty (Vue 2 时代)------Proxy 不代理不存在的属性。深代理要递归,浅代理 = 半吊子。


8. 外观模式:jQuery / lodash chain

一句话:把多个底层 API 组合成"统一好用的门面"。

真实开源项目lodash_.chain 是外观模式。

js 复制代码
// lodash chain 用法
const result = _(users)
  .filter((u) => u.active)
  .map((u) => u.name)
  .sortBy()
  .take(5)
  .value();
// 一行完成 5 步操作,链式清爽

自己用:复杂 SDK 包装(地图、支付、IM)、统一团队工具方法。

js 复制代码
// 业务里的"业务工具箱"
const _ = {
  formatDate: (d) => dayjs(d).format("YYYY-MM-DD"),
  formatMoney: (n) => n.toFixed(2),
  safeJSON: (s, d = null) => {
    try {
      return JSON.parse(s);
    } catch {
      return d;
    }
  },
  // ...
};

坑在哪 :外观别太"全"------utils 啥都装 = 啥都不好找。按业务域分工具包


9. 享元模式:虚拟列表

一句话:共享细粒度对象,减少内存。

真实开源项目react-virtual / vue-virtual-scroller 都是享元。

js 复制代码
// 思路:只渲染可视区内的 row
const visible = list.slice(startIdx, endIdx);
// 不可见的 row DOM 节点被回收(享元:节点对象复用)

自己用:长列表、地图 marker、canvas 粒子系统。

坑在哪 :享元的"对象池"管理复杂,业务不极致追求性能别上 。React 用 key 复用元素,Vue 用 <keep-alive> 缓存组件------框架帮你做了。


三、行为型(8 个)

10. 观察者模式:Node.js EventEmitter

一句话:对象状态变化时通知所有依赖者。

真实开源项目Node.js 内置 events 模块,每个流都基于它。

js 复制代码
// Node 源码:lib/events.js
class EventEmitter {
  on(event, listener) {
    /* 注册 */
  }
  emit(event, ...args) {
    /* 触发 */
  }
  off(event, listener) {
    /* 解绑 */
  }
}

// 用法
server.on("request", (req, res) => {
  /* 处理请求 */
});
process.on("uncaughtException", (err) => {
  /* 兜底 */
});

自己用:Node 流、文件监听、自定义事件总线。

坑在哪off 漏写 = 内存泄漏元凶最大监听数默认 10------超了 Node 会警告"possible memory leak"。


11. 发布订阅:mitt

一句话:发布者和订阅者通过"事件中心"解耦。

真实开源项目mitt 是个 200 行的发布订阅库。

js 复制代码
// mitt 源码:src/index.ts
export default function mitt(all = new Map()) {
  return {
    on(type, handler) {
      /* ... */
    },
    off(type, handler) {
      /* ... */
    },
    emit(type, event) {
      /* ... */
    },
  };
}

// 用法
const bus = mitt();
bus.on("login", (user) => console.log(user.name, "登录"));
bus.emit("login", { name: "小不" });
// 跟观察者的区别:发布者不知道订阅者是谁(解耦)

自己用 :跨组件通信、跨 tab 通信、BroadcastChannel 包装。

坑在哪 :事件名用常量别裸字符串------TS 联合类型防拼错。


12. 中间件模式:Koa / Redux

一句话:请求/动作通过"管道"层层处理。

真实开源项目Koa 的"洋葱模型"是中间件范式。

js 复制代码
// Koa 源码:lib/application.js
function compose(middleware) {
  return function (context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;
      let fn = middleware[i];
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err);
      }
    }
  };
}

// 业务用法:鉴权 → 日志 → 业务 → 错误处理
app.use(authMiddleware);
app.use(loggerMiddleware);
app.use(errorHandler);

自己用:HTTP 请求管道、Redux 中间件、Express 路由。

坑在哪next() 调多次 = 报错;洋葱模型 = 先进后出,日志中间件"前记录 + 后记录"配对写。


13. 命令模式:Redux actions

一句话:把"请求"封装成对象,可以排队、撤销、记录。

真实开源项目Redux 的 action 是命令模式。

js 复制代码
// Redux 用法
const action = { type: "ADD_TODO", payload: { text: "学设计模式" } };
dispatch(action);
// 撤销:dispatch 反向 action
const undo = { type: "UNDO" };
dispatch(undo);
// 记录:Redux DevTools 看所有 action 历史

自己用:撤销/重做、操作历史、异步任务队列。

坑在哪 :action 是纯数据 (不要在 action 里写业务逻辑)。reducer 才是业务的归宿


14. 责任链模式:jQuery 事件冒泡 / Express

一句话:请求沿链传递,每个节点决定"处理 or 传给下一个"。

真实开源项目jQuery 事件委托是经典责任链。

js 复制代码
// jQuery on() 实现
$(document).on("click", ".btn", handler);
// 事件从 document 向下冒泡(或捕获向上)
// 任何一层可以 stopPropagation 截断

自己用:表单字段校验(依次校验每个字段)、权限校验链、Express 中间件。

坑在哪 :链太长 = 调试地狱。每个节点只做一件事,别越权。


15. 迭代器模式:Generator / Array.from

一句话:顺序访问集合元素,不暴露内部结构。

真实开源项目 :JavaScript 内置 Symbol.iterator + Babel 编译器大量用。

js 复制代码
// Array 自带迭代器
const arr = [1, 2, 3];
const iter = arr[Symbol.iterator]();
iter.next(); // { value: 1, done: false }
iter.next(); // { value: 2, done: false }

// 自定义迭代器(Generator)
function* fib() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
const f = fib();
f.next(); // { value: 0, done: false }
f.next(); // { value: 1, done: false }

自己用:流式数据、无限序列、懒加载列表。

坑在哪 :迭代器只能遍历一次别手写迭代器,Generator 是甜点


16. 状态模式:XState

一句话:对象行为随状态改变而改变。

真实开源项目XState 是正经的状态机库。

js 复制代码
// XState 用法
import { createMachine } from "xstate";

const lightMachine = createMachine({
  initial: "green",
  states: {
    green: { on: { TIMER: "yellow" } },
    yellow: { on: { TIMER: "red" } },
    red: { on: { TIMER: "green" } },
  },
});
// 状态转换可视化、可测试

自己用:订单状态(待支付/已支付/已发货/已退款)、播放器状态、表单步骤。

坑在哪 :状态机**适合"状态有限且明确"**的场景,别把整个业务塞状态机------简单 if-else 足够就别上。


17. 备忘录模式:Redux DevTools

一句话:保存对象状态,必要时恢复(撤销/快照)。

真实开源项目Redux DevTools Extension 时间旅行调试 = 备忘录模式。

js 复制代码
// 每次 dispatch 都保存完整 state
const stateHistory = [initialState, state1, state2, ...];
// DevTools 可以"穿越"到任意历史 state
store.dispatch(action);
console.log(stateHistory);  // 完整时间线

自己用:撤销/重做、表单"草稿"、画板的"快照"。

坑在哪 :备忘录吃内存 ------大型 state 别每个都快照。structuredClone 比 JSON 快又稳


18. 访问者模式:Babel AST 遍历

一句话:分离算法和对象结构,在不修改对象的前提下新增操作。

真实开源项目Babel 编译器遍历 AST 用访问者。

js 复制代码
// Babel 用法
const visitor = {
  Identifier(path) {
    console.log("发现标识符:", path.node.name);
  },
  BinaryExpression(path) {
    console.log("二元运算:", path.node.operator);
  },
};
babel.transform(code, { plugins: [{ visitor }] });

自己用:代码检查工具(ESLint)、AST 转换、报表数据处理。

坑在哪 :访问者模式对 AST 友好但对业务过重别为了"模式"强行套


四、其他常用(2 个)

19. 组合模式:React / Vue 组件树

一句话:把对象组合成树形结构,"部分-整体"统一处理。

真实开源项目React 组件树就是组合模式。

jsx 复制代码
<Card>
  <Header>
    <Avatar />
    <Title>小不</Title>
  </Header>
  <Body>
    <Text>...</Text>
    <Button>点我</Button>
  </Body>
</Card>
// 整棵树统一 props 传递、统一事件冒泡

自己用:UI 组件树、菜单、文件树、组织架构树。

坑在哪 :组合的"叶子"和"枝干"接口要一致(React 的 props.children),不然难统一渲染。


20. 解释器模式:模板引擎(Handlebars / ejs)

一句话:给定一个语言,定义它的文法表示和解释器。

真实开源项目Handlebars / EJS 都是解释器。

js 复制代码
// Handlebars 用法
const template = Handlebars.compile("你好,{{name}}!");
template({ name: "小不" }); // "你好,小不!"
// 条件、循环、子模板------都靠解释器解析

自己用:模板引擎、DSL 配置、业务规则引擎。

坑在哪 :解释器模式实现复杂业务里几乎用不到手写------用现成库就行。


📦 收个尾

这 20 个设计模式浓缩一下:

  • 最高频踩坑 :全局变量当单例、把 if-else 嵌套当工厂、责任链传错 next 死循环
  • 最值得收藏:Vue 3 Proxy 响应式、Koa 中间件洋葱模型、axios adapter 跨端
  • 核心思路模式不是炫技,是被问题逼出来的------能拍扁 if-else 就拍扁,但别为了"模式"强行套

学到了就是赚到了,犹豫徘徊等于白来~


写到最后

想要啥技巧?评论区甩个题目过来~

  • 你刚踩的坑
  • 项目里反复写的代码
  • 想搞清楚但一直懒得查的 API

小不看到...不一定回 😂 毕竟代码里翻车太多,腾不出手~

Ps:三连随缘,催更的会被打 😂

相关推荐
CoderLiu1 小时前
Agent 工程的下一层:为什么「把流程写成图」还不够,还需要 Graph Engineering
前端·人工智能·后端
勇往直前plus1 小时前
Vue3(篇三) Element Plus
前端·javascript·typescript·vue
爱勇宝1 小时前
《道德经》第 8 章:真正高级的能力,像水一样成事
前端·后端·程序员
Revolution611 小时前
数组本身没有 map,为什么还能直接调用:原型链怎样查找属性
前端·javascript·面试
bonechips2 小时前
RAG实战(一):EPUB 加载、文本切割与向量入库
前端·数据库
swipe2 小时前
10|(前端转全栈)库存扣减为什么最容易出事故?SKU、并发与原子更新
前端·后端·全栈
cdcdhj2 小时前
vue3中的watchEffect()监听,什么时候监听,什么时候清理,什么时候停止
前端·javascript·vue.js
huabuyu2 小时前
几百 MB 的文件为什么等几分钟才能打开?文件分片下载与渐进预览原理
前端·javascript
虚惊一场2 小时前
麻将桌上的并发控制:扫码进房、乐观版本与零和结算
前端·javascript