装饰器模式
在不修改原对象结构的前提下,动态地为对象添加职责能力
注意:Promise 扁平化是装饰器链成立的语言前提
原因: 装饰器模式要求 被装饰对象与装饰后的对象在类型层面是同态的
补充Promise规则:如果 Promise resolve 的值是 Promise,那么最终 Promise 的结果由内层 Promise 的状态和值决定
/**
* =========================
* 请求函数类型(协议)
* =========================
* 所有被装饰函数 & 装饰器
* 都必须遵守这个函数签名
*/
type RequestFn = (url: string) => Promise<any>;
//(fn: RequestFn): RequestFn
//表示一个装饰器函数:
//输入和输出的函数类型完全一致,只增强能力,不改变使用方式。
/**
* =========================
* 原始请求函数(被装饰者)
* =========================
* 只负责:发请求、拿数据
* 不关心 loading / token / error
*/
const request: RequestFn = async (url) => {
const res = await fetch(url);
if (!res.ok) {
throw new Error('Network error');
}
return res.json();
};
/**
* =========================
* 装饰器 1:Loading
* =========================
* 在请求前后注入 loading 行为
*/
function withLoading(fn: RequestFn): RequestFn {
//async 函数无论内部 return 什么,最终都会返回一个 Promise
return async function (url: string) {
console.log('loading start');
try {
const result = await fn(url);
return result;
} finally {
// finally 保证异常时也能关闭 loading
console.log('loading end');
}
};
}
/**
* =========================
* 装饰器 2:Token 校验
* =========================
* 在真正发请求前进行鉴权
* 若失败,直接中断执行链
*/
function withTokenCheck(fn: RequestFn): RequestFn {
return async function (url: string) {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No token');
}
return fn(`${url}?token=${token}`);
};
}
/**
* =========================
* 装饰器 3:错误处理
* =========================
* 统一捕获下游异常
*/
function withError(fn: RequestFn): RequestFn {
return async function (url: string) {
try {
return await fn(url);
} catch (e) {
console.error('请求失败:', e);
throw e;
}
};
}
/**
* =========================
* 装饰(组合)
* =========================
* 能力从里向外叠加
* 执行从外向里进入
*/
const finalRequest: RequestFn =
withError(
withTokenCheck(
withLoading(request)
)
);
/**
* =========================
* 使用
* =========================
*/
finalRequest('/api/user');
通过函数包装的方式,
- 能力是从里向外叠加的
- 执行是从外向里进入的
- 任意一层不满足条件,直接中断,不再向内执行
执行顺序:
finalRequest
↓ withError (统一兜底)
↓ withTokenCheck (决定能不能继续)
↓ withLoading (before / after)
↓ request (真正的业务)
适配器模式
使接口不兼容的对象能够相互合作
在不改变原有代码的情况下使用新的类
生活中的例子:耳机转接头
场景:
微信支付 / 支付宝 SDK 接口完全不一样
但你的业务代码 只想用一个统一的支付接口
/**
* =========================
* 目标接口(Target Interface)
* =========================
* 业务层只认这个类型:
* - “我只关心能不能付钱”
* - “不关心你是微信 / 支付宝 / 其他”
*
* 这就是被“冻结”的协议
*/
type Payment = (amount: number, userId: string) => void;
/**
* =========================
* 被适配者(Adaptee)
* =========================
* 第三方 SDK / 旧系统 / 不可控代码
*
* 特点:
* - 接口可能不统一
* - 参数可能随时变化
* - 业务层不应该直接依赖它
*/
function wechatPaySDK(amount: number, userId: string): void {
console.log(`微信支付 ${amount} 元,用户 ${userId}`);
}
function aliPaySDK(amount: number, userId: string): void {
console.log(`支付宝支付 ${amount} 元,用户 ${userId}`);
}
/**
* =========================
* 适配器(Adapter)
* =========================
* ⚠️ 注意:
* 适配器本身【不执行支付】
* 它的职责是:
* 👉 生产一个“符合 Payment 协议的函数”
*
* 也就是说:
* - wechatPayAdapter 不是 Payment
* - wechatPayAdapter() 的返回值 才是 Payment
*/
function wechatPayAdapter(): Payment {
return (amount: number, userId: string) => {
// 在这里完成“接口翻译”
wechatPaySDK(amount, userId);
};
}
function aliPayAdapter(): Payment {
return (amount: number, userId: string) => {
aliPaySDK(amount, userId);
};
}
/**
* =========================
* 业务层(Client)
* =========================
* 业务层只依赖 Payment 抽象
* 不知道、也不关心:
* - SDK 是谁
* - Adapter 怎么实现
*
* 这就是依赖倒置原则
*/
function pay(
payment: Payment,
amount: number,
userId: string
): void {
payment(amount, userId);
}
/**
* =========================
* 使用
* =========================
* 先通过 Adapter 得到一个 Payment
* 再交给业务层使用
*/
pay(wechatPayAdapter(), 100, "1234567890");
pay(aliPayAdapter(), 100, "1234567890");
场景:
H5 + 小程序 登录方式不同
业务层期望的统一接口
/**
* =========================
* 目标接口(Target Interface)
* =========================
* 业务层只认这个协议:
* - “我只关心能不能登录成功”
* - “不关心你是 H5 / 小程序 / App”
*
* 这是被「冻结」的登录协议
*/
//返回一个Promise resolve值的类型是User对象 TypeScript 目前无法对 reject 进行强类型约束
type Login = () => Promise<User>;
interface User {
id: string;
name: string;
token: string;
}
/**
* =========================
* 被适配者(Adaptee)
* =========================
* 各个平台原生登录实现
*
* 特点:
* - 接口形态不统一
* - 返回字段不一致
* - 业务层不应该直接依赖
*/
// H5:账号密码登录
async function h5Login(username: string, password: string) {
return {
user_id: '1001',
nick_name: 'Tom',
access_token: 'h5-token'
};
}
// 小程序:code 登录
async function wxLogin() {
return { code: 'wx-code' };
}
async function miniLogin(code: string) {
return {
uid: '2001',
name: 'Jerry',
token: 'mini-token'
};
}
/**
* =========================
* 适配器(Adapter)
* =========================
* ⚠️ 关键点:
* - Adapter 本身【不执行登录】
* - Adapter 的职责是:
* 👉 生产一个「符合 Login 协议的函数」
*
* 也就是说:
* - h5LoginAdapter 不是 Login
* - h5LoginAdapter() 的返回值 才是 Login
*/
// H5 登录适配器
function h5LoginAdapter(
username: string,
password: string
): Login {
return async () => {
const res = await h5Login(username, password);
return {
id: res.user_id,
name: res.nick_name,
token: res.access_token
};
};
}
// 小程序登录适配器
function miniProgramLoginAdapter(): Login {
return async () => {
const { code } = await wxLogin();
const res = await miniLogin(code);
return {
id: res.uid,
name: res.name,
token: res.token
};
};
}
/**
* =========================
* 业务层(Client)
* =========================
* 业务层只依赖 Login 抽象
* 完全不知道:
* - 平台差异
* - SDK 细节
*
* 这就是适配器模式 + 依赖倒置
*/
async function doLogin(login: Login) {
const user = await login();
console.log('登录成功:', user);
}
/**
* =========================
* 使用
* =========================
* 先通过 Adapter 得到一个 Login
* 再交给业务层使用
*/
// H5 场景
doLogin(h5LoginAdapter('admin', '123456'));
// 小程序场景
doLogin(miniProgramLoginAdapter());
外观模式
给复杂系统提供一个简单、稳定、低心智负担的入口
/**
* Storage 外观模式
*
* 目的:
* - 为业务层提供统一、稳定、简单的存储接口
* - 隐藏 localStorage / JSON / 异常等复杂度
*/
/**
* 目标接口(Facade Interface)
*
* 被“冻结”的协议:
* - 业务层只允许使用这 3 个方法
* - 不允许直接访问 localStorage
*/
interface StorageFacade {
get<T>(key: string): T | null;
set<T>(key: string, value: T): void;
remove(key: string): void;
}
/**
* 工厂函数实现外观
*
* ⚠️ 使用闭包保存内部 state(如 prefix)
* ⚠️ 返回对象就是外观
*/
function createStorageFacade(): StorageFacade {
const prefix = '__app__';
const rawStorage = window.localStorage;
function formatKey(key: string) {
return `${prefix}:${key}`;
}
function get<T>(key: string): T | null {
try {
const raw = rawStorage.getItem(formatKey(key));
if (raw === null) return null;
return JSON.parse(raw) as T;
} catch (error) {
console.error('[Storage.get] error:', error);
return null;
}
}
function set<T>(key: string, value: T): void {
try {
rawStorage.setItem(formatKey(key), JSON.stringify(value));
} catch (error) {
console.error('[Storage.set] error:', error);
}
}
function remove(key: string): void {
rawStorage.removeItem(formatKey(key));
}
return { get, set, remove };
}
/**
* =====================================================
* 对业务层暴露的唯一入口(Facade)
* =====================================================
*/
export const storage = createStorageFacade();
/**
* =====================================================
* 业务层使用
* =====================================================
*/
interface User {
id: string;
name: string;
}
// 写入
storage.set<User>('user', { id: '1001', name: 'Tom' });
// 读取
const user = storage.get<User>('user');
console.log('user name:', user?.name);
// 删除
storage.remove('user');
代理模式
为一个对象提供一个代理,通过代理控制对真实对象的访问,同时可以增加额外逻辑(缓存、权限、日志、节流等)。当外界打算访问真实对象的时候,该外界的感觉是访问的真实对象,但其实外界访问的是代理对象。
例:ES6中的Proxy构造方法
// 创建一个要被代理的对象
// 这是真实的对象
const target = {
name: "John",
age: 30,
};
// 定义代理的行为
// 代理对象就会对外界的访问需求进行过滤
const handler = {
// 拦截对象属性的读取
get(target, prop, receiver) {
console.log(`[GET]: ${prop}`);
return Reflect.get(target, prop, receiver);
},
// 拦截对象属性的设置
set(target, prop, value, receiver) {
console.log(`[SET]: ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
},
};
// 使用Proxy构造函数创建代理对象
// new Proxy 会返回一个对象,该对象就是针对真实对象的代理对象
const proxy = new Proxy(target, handler);
// 测试代理的行为
// 之后就通过代理对象来访问真实对象的成员
console.log(proxy.name); // 输出:[GET]: name 以及 John
proxy.age = 31; // 输出:[SET]: age = 31
场景:请求函数 增加token权限控制
/**
* =========================
* 请求函数类型(Target Interface)
* 业务层只关心:
* - 给 URL 和请求参数
* - 返回一个 Promise
*
* requestParams:携带请求的 method/data/headers 等信息
*/
type RequestFn = (
url: string,
requestParams?: Record<string, any>
) => Promise<any>;
/**
* =========================
* 真正的请求对象(Real Subject)
* =========================
* 只负责发送请求,不关心权限、token 等
*/
const realRequest: RequestFn = async (url, requestParams) => {
console.log(`[Real Request] 请求 ${url},参数:`, requestParams);
return axios({ url, ...requestParams });
};
/**
* =========================
* 代理对象(Proxy)
* =========================
* 为 realRequest 增加权限校验逻辑
*
* 代理职责:
* - 请求前检查 token
* - 如果没有 token,直接抛错,阻止请求
* - 如果有 token,则调用真实请求
*/
function createAuthProxy(fn: RequestFn): RequestFn {
return async (url, requestParams) => {
// 获取 token(例如从 localStorage)
const token = localStorage.getItem('token');
// 没有 token,直接中断
if (!token) {
throw new Error('[Proxy] 无权限访问,请先登录');
}
// 有 token,则把 token 放入 headers 并调用真实请求
const paramsWithToken = {
...requestParams,
headers: {
...(requestParams?.headers || {}),
Authorization: `Bearer ${token}`
}
};
console.log(`[Proxy] 权限校验通过,携带 token 请求 ${url}`);
return fn(url, paramsWithToken);
};
}
/**
* =========================
* 使用示例
* =========================
* client 只调用代理函数,不关心内部是权限校验还是请求
*/
const requestWithAuth = createAuthProxy(realRequest);
(async () => {
// 模拟设置 token
localStorage.setItem('token', 'my-secret-token');
// GET 请求
console.log(await requestWithAuth('/api/user', { method: 'GET' }));
// POST 请求
console.log(
await requestWithAuth('/api/product', {
method: 'POST',
data: { name: 'iPhone' }
})
);
// 模拟 token 失效
localStorage.removeItem('token');
try {
await requestWithAuth('/api/order', { method: 'GET' });
} catch (e) {
console.error(e.message); // 输出: [Proxy] 无权限访问,请先登录
}
})();