同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有24小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。
一、开篇:为什么要学设计模式?
你可能会想:我写业务能跑就行,为什么要管设计模式?
实际工作中常见这些情况:
- 权限控制:路由守卫、按钮权限、接口权限各处都在写,逻辑重复、难维护。
- 消息通知:多个模块要弹 Toast,互相耦合,改一处影响一片。
- 表单校验:不同字段用不同规则,全写在一个大
if-else里,难扩展。
这些问题都可以用少量设计模式来简化。下面用单例、发布订阅、策略 三种模式,搭配权限、通知、表单校验三个场景,讲清楚:日常怎么写、为什么这么写、容易踩哪些坑。
二、概念扫盲
先把三个模式用一句话说清楚:
| 模式 | 一句话 | 适合场景 |
|---|---|---|
| 单例 | 全局只存在一个实例,多次调用拿到同一个对象 | 全局唯一的东西:权限管理器、通知中心、全局配置 |
| 发布订阅 | 发布者发事件,订阅者监听,彼此解耦 | 一对多、多对多通知:消息通知、事件总线 |
| 策略 | 把多种"算法/规则"封装成可替换的策略 | 同种操作多种规则:表单校验、支付方式选择 |
下面按「概念 → 代码 → 实战」来展开。
三、单例模式:权限控制里的"唯一管家"
3.1 是什么?
单例保证:不管你怎么 new、怎么调用,拿到的一定是同一个实例。
3.2 最简单的实现
javascript
// 懒汉式单例:用到的时候才创建
function createPermissionManager() {
let instance = null;
return function () {
if (!instance) {
instance = {
role: 'guest',
permissions: [],
check(perm) {
return this.permissions.includes(perm);
},
setRole(role, perms) {
this.role = role;
this.permissions = perms;
}
};
}
return instance;
};
}
const getPermissionManager = createPermissionManager();
const pm1 = getPermissionManager();
const pm2 = getPermissionManager();
console.log(pm1 === pm2); // true
3.3 用 class 实现(更贴近日常写法)
javascript
class PermissionManager {
static instance = null;
constructor() {
if (PermissionManager.instance) {
return PermissionManager.instance;
}
this.role = 'guest';
this.permissions = [];
PermissionManager.instance = this;
}
check(perm) {
return this.permissions.includes(perm);
}
setRole(role, perms) {
this.role = role;
this.permissions = perms;
}
}
// 无论怎么 new,都是同一个
const pm1 = new PermissionManager();
const pm2 = new PermissionManager();
console.log(pm1 === pm2); // true
3.4 实际用法:按钮权限、路由守卫
javascript
// permission.js - 全局唯一的权限管理器
class PermissionManager {
static instance = null;
constructor() {
if (PermissionManager.instance) return PermissionManager.instance;
this.permissions = [];
PermissionManager.instance = this;
}
init(perms) {
this.permissions = perms;
}
has(perm) {
return this.permissions.includes(perm);
}
// 用于 v-if 指令:<button v-if="permission.has('user:delete')">
hasPermission(perm) {
return () => this.has(perm);
}
}
export const permission = new PermissionManager();
// 在路由守卫里用
// router.beforeEach((to, from, next) => {
// if (to.meta.perm && !permission.has(to.meta.perm)) {
// next('/403');
// return;
// }
// next();
// });
要点:路由、按钮、接口都用同一个 permission 实例,权限数据统一维护,避免到处复制逻辑。
3.5 单例的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 测试时状态残留 | 单例在测试间共享 | 提供 reset() 或在测试前 permission.permissions = [] |
| 滥用单例 | 不是全局唯一的东西也做成单例 | 只对真正"全局唯一"的用单例 |
| 忘了初始化 | 直接用 check 但没 init |
在登录成功后统一 permission.init(perms) |
四、发布订阅模式:消息通知解耦
4.1 是什么?
发布者发事件,订阅者订阅事件,彼此不直接依赖。发布者不关心谁在监听,订阅者不关心谁在发。
4.2 核心:EventBus
javascript
class EventBus {
constructor() {
this.events = {}; // { eventName: [fn1, fn2, ...] }
}
on(eventName, fn) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(fn);
}
off(eventName, fn) {
if (!this.events[eventName]) return;
this.events[eventName] = this.events[eventName].filter(cb => cb !== fn);
}
emit(eventName, ...args) {
if (!this.events[eventName]) return;
this.events[eventName].forEach(fn => fn(...args));
}
}
4.3 实战:消息通知中心
javascript
// 通知中心:单例 + 发布订阅
class NotificationCenter {
static instance = null;
constructor() {
if (NotificationCenter.instance) return NotificationCenter.instance;
this.events = {};
NotificationCenter.instance = this;
}
// 订阅某种类型的通知
on(type, handler) {
if (!this.events[type]) this.events[type] = [];
this.events[type].push(handler);
}
off(type, handler) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(h => h !== handler);
}
// 发布:触发所有订阅者
emit(type, payload) {
if (!this.events[type]) return;
this.events[type].forEach(handler => handler(payload));
}
}
const notify = new NotificationCenter();
// 业务 A:订单成功
notify.on('orderSuccess', (orderId) => {
Toast.success(`订单 ${orderId} 创建成功`);
// 可能还要更新购物车、统计等
});
// 业务 B:支付成功
notify.on('paymentSuccess', (data) => {
Toast.success('支付成功');
router.push('/orders');
});
// 某处触发
notify.emit('orderSuccess', 'ORD123');
4.4 完整示例:登录后多处联动
javascript
// 用户登录成功后,多个模块要同时反应
notify.on('loginSuccess', (user) => {
// 模块 1:更新 header 头像
header.updateAvatar(user.avatar);
});
notify.on('loginSuccess', (user) => {
// 模块 2:拉取用户权限
permission.init(user.permissions);
});
notify.on('loginSuccess', () => {
// 模块 3:刷新待办数量
todoBadge.refresh();
});
// 登录接口成功后,只发一次
loginApi().then(user => {
notify.emit('loginSuccess', user);
});
发布者只管 emit,订阅者各自处理,互不依赖,修改一处不影响其他模块。
4.5 发布订阅的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 内存泄漏 | 组件销毁后没 off |
在 beforeUnmount 里统一 off |
| 事件名魔法字符串 | 到处写 'orderSuccess' 易 typo |
抽成常量 EVENTS.ORDER_SUCCESS |
| 过度解耦 | 简单父子通信也用 EventBus | 能用 props/emit 就用,只在跨层、多对多时用 |
| 回调地狱 | 用事件代替 Promise | 异步流程优先用 async/await,事件只做"通知" |
五、策略模式:表单校验规则可插拔
5.1 是什么?
把不同校验规则封装成独立策略,用配置或映射表选择执行哪个,避免大段 if-else。
5.2 没策略时:容易变成"面条码"
javascript
// 反面教材:每加一个规则就要改这里
function validate(value, rule) {
if (rule === 'required') {
return value !== '' && value != null;
}
if (rule === 'email') {
return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);
}
if (rule === 'phone') {
return /^1\d{10}$/.test(value);
}
if (rule === 'minLength') {
return value.length >= 6;
}
// 越加越多...
}
5.3 用策略重构
javascript
// 策略对象:每个规则是独立函数
const strategies = {
required(value) {
return value !== '' && value != null && String(value).trim() !== '';
},
email(value) {
return /^[\w-]+(\.[\w-]+)*@[\w-]+(\.[\w-]+)+$/.test(value);
},
phone(value) {
return /^1\d{10}$/.test(value);
},
minLength(value, min) {
return (value || '').length >= min;
},
maxLength(value, max) {
return (value || '').length <= max;
}
};
// 校验器:根据规则名调用对应策略
function validate(value, ruleName, ...ruleArgs) {
const fn = strategies[ruleName];
if (!fn) return true; // 未知规则默认通过
return fn(value, ...ruleArgs);
}
// 使用
validate('', 'required'); // false
validate('a@b.com', 'email'); // true
validate('12345', 'minLength', 6); // false
5.4 实战:表单校验配置化
javascript
// 表单校验配置
const formRules = {
username: [
{ strategy: 'required', message: '用户名不能为空' },
{ strategy: 'minLength', params: [3], message: '至少 3 个字符' }
],
email: [
{ strategy: 'required', message: '邮箱不能为空' },
{ strategy: 'email', message: '邮箱格式不正确' }
],
phone: [
{ strategy: 'phone', message: '手机号格式不正确' }
]
};
function validateForm(formData) {
const errors = {};
for (const [field, rules] of Object.entries(formRules)) {
for (const rule of rules) {
const { strategy, params = [], message } = rule;
const value = formData[field];
const valid = validate(value, strategy, ...params);
if (!valid) {
errors[field] = message;
break; // 一个字段只保留第一个错误
}
}
}
return { valid: Object.keys(errors).length === 0, errors };
}
// 使用
const result = validateForm({
username: 'ab',
email: 'invalid',
phone: '13800138000'
});
console.log(result);
// { valid: false, errors: { username: '至少 3 个字符', email: '邮箱格式不正确' } }
新增字段或规则时,只改配置,不改 validate 核心逻辑。
5.5 策略的坑
| 坑 | 原因 | 建议 |
|---|---|---|
| 策略与业务混在一起 | 策略里写请求、跳转 | 策略只做"规则判断",返回 boolean |
| 规则参数传错 | minLength 要数字,传了字符串 |
做参数校验或封装成 createMinLength(6) |
| 和单例搞混 | 策略不需要全局唯一 | 策略是无状态的纯函数,不共享实例 |
六、三者怎么选?
| 场景 | 推荐模式 | 理由 |
|---|---|---|
| 全局唯一:权限、配置、通知中心 | 单例 | 避免多处实例、状态不一致 |
| 一对多/多对多:登录联动、订单状态 | 发布订阅 | 解耦,扩展新监听者不改原有逻辑 |
| 多种规则:表单校验、支付方式、折扣计算 | 策略 | 规则可插拔,易维护和扩展 |
可以组合用,例如:单例的通知中心 + 发布订阅 ,或 策略模式 + 单例的校验器。
七、小结
| 模式 | 一句话 | 典型用法 |
|---|---|---|
| 单例 | 全局只一个实例 | 权限管理器、通知中心、全局配置 |
| 发布订阅 | 发布事件、订阅处理,彼此解耦 | 消息通知、登录后联动、跨模块通信 |
| 策略 | 多种规则封装成可替换策略 | 表单校验、支付方式、折扣规则 |
记住三点:
- 单例只给"真正全局唯一"的东西用,并注意测试时重置。
- 发布订阅适合跨模块通知,简单通信优先用 props/emit,用完记得
off。 - 策略模式把规则抽成独立函数,用配置驱动,方便扩展。
设计模式不是炫技,而是让代码更好改、更好测、更少 bug 的工具。先能用、再好用,逐步引入即可。
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~