讲解
学了二十多种模式,回到日常代码环顾四周,会发现它们早就在那里——只是穿着工程实践的便装。本章做一次「认亲」:看看每天用的框架和库里,哪些是哪些模式的化身。认出它们,你读框架源码的速度和使用框架的直觉都会上一个台阶。
模块模式:ES 模块本身就是创建型里「单例 + 封装」的现代形态——模块作用域是天然的私有空间,导出接口是公开的,每个模块进程内只加载一次。Node.js 早年用 IIFE 包出来的模块,今天已经是语言标准。
中间件 = 责任链(洋葱模型):Express/Koa 的 app.use 注册的每个中间件都是链上节点,拿到 (ctx, next),做事、调 next 放行、或不调 next 拦截。洋葱模型指的是「next 之后的代码在内层返回后执行」,请求一层层进去、响应一层层出来。
React 组件树 = 组合模式: 里嵌 里嵌 ,叶子和容器用同一套「渲染」接口对待;父组件不需要知道自己嵌的是按钮还是又一个面板。Hooks 里的自定义 hook 则常常是策略模式:useSort(data, strategyFn) 把「怎么比」作为参数传入。
依赖注入容器简介:NestJS、Angular 这类框架把「谁 new 谁」从代码里收进容器——类用装饰器声明依赖,容器负责装配。这正是依赖倒置原则的工业化:业务类只声明「我需要 PaymentGateway 接口」,容器决定给真的还是 mock 的。思想不新(构造函数注入我们一直在手写),容器只是把装配自动化、集中化。
示例
用一个迷你 Web 框架骨架同时演示两个模式:中间件链(责任链的洋葱变体)+ 路由策略表(策略模式),含鉴权拦截:
import assert from 'node:assert/strict';
// 中间件 = 责任链的洋葱模型变体(Koa/Express 的核心抽象)
interface Context {
path: string;
user: string | null;
log: string[];
}
type Next = () => void;
type Middleware = (ctx: Context, next: Next) => void;
// 把一串中间件组合成一个可执行函数:next() 驱动链式深入
function compose(middlewares: Middleware[]): (ctx: Context) => void {
return (ctx) => {
const dispatch = (index: number): void => {
const mw = middlewares[index];
if (mw === undefined) return;
mw(ctx, () => dispatch(index + 1));
};
dispatch(0);
};
}
// 策略模式:每种路由是一种处理策略,按 path 查表选用
type RouteStrategy = (ctx: Context) => void;
const routes: Record<string, RouteStrategy> = {
'/home': (ctx) => ctx.log.push('渲染首页'),
'/admin': (ctx) => ctx.log.push('渲染管理页'),
};
const logger: Middleware = (ctx, next) => {
ctx.log.push('-> 进入 ' + ctx.path);
next();
ctx.log.push('<- 离开 ' + ctx.path); // next 之后:洋葱的外层在返回时收尾
};
const auth: Middleware = (ctx, next) => {
if (ctx.path === '/admin' && ctx.user === null) {
ctx.log.push('未登录,拦截 /admin');
return; // 不调 next:链就此中断
}
next();
};
const router: Middleware = (ctx, next) => {
const strategy = routes[ctx.path];
if (strategy !== undefined) strategy(ctx);
next();
};
const app = compose([logger, auth, router]);
// 场景一:公开页面,走完整个洋葱
const ctx1: Context = { path: '/home', user: null, log: [] };
app(ctx1);
assert.deepEqual(ctx1.log, ['-> 进入 /home', '渲染首页', '<- 离开 /home']);
// 场景二:未登录访问 /admin,被 auth 拦截,router 根本没执行
const ctx2: Context = { path: '/admin', user: null, log: [] };
app(ctx2);
assert.deepEqual(ctx2.log, ['-> 进入 /admin', '未登录,拦截 /admin', '<- 离开 /admin']);
console.log(ctx1.log.join(' | '));
console.log(ctx2.log.join(' | '));
两个模式的接缝看得很清楚:compose 搭起责任链,logger 在 next 前后各做一件事(洋葱语义);router 本身也是一个中间件,它内部用查表(策略模式)决定渲染什么。场景二展示了责任链的拦截语义:auth 不调 next,后面的 router 压根没机会执行,而 logger 的「离开」日志依然照常——因为洋葱的外层总会收尾。真实的 Koa 就是在这个骨架上加了 async/await 和上下文代理而已。
常见坑
- 知道模式名后看什么都像钉子:框架源码里「找模式」是学习方法,自己写代码时先问「这问题不用模式能不能解」。
- 手写中间件忘了调 next 也不 return:链继续往下走但你以为拦截了,鉴权绕过事故的经典成因;每个分支要么 next 要么 return,必须明确。
- 中间件顺序随意排:鉴权写在日志前面,未登录请求连日志都没留下;顺序即语义,装配处注释清楚每个中间件为什么在这个位置。
- 滥用依赖注入容器:什么类都注册进容器,连纯函数都走注入;容器服务「有状态、需替换、跨层共享」的对象,纯工具函数直接 import。
- 把组合模式理解成「React 专有」:组件树是组合的实例,不是组合的定义;反过来,任何「部分-整体 + 统一操作」都可以这样组织。
小结
模块、中间件、组件树、hooks、DI 容器——日常框架里到处是模式的便装身影;认出它们是为了更快地理解框架,而不是为了到处套模式。最后一章踩刹车:聊聊反模式和「什么时候不用模式」。