JavaScript Promise装饰模式:安全增强异步操作的实践指南
1. 项目概述:当“装饰”遇上“承诺”
在JavaScript的异步编程世界里, Promise 对象是我们处理异步操作的核心工具,它代表了一个最终可能完成(或失败)的操作及其结果值。而“装饰器”(Decorator)作为一种设计模式,其核心思想是在不改变原对象结构的前提下,动态地为其添加新的功能或行为。那么,一个自然而然的问题就产生了:我们能否像装饰一个普通函数或类那样,去装饰一个 Promise 呢?更进一步,在装饰的过程中,我们如何确保不破坏 Promise 本身的核心契约——即它的状态(pending, fulfilled, rejected)和链式调用(then/catch/finally)的完整性?
这个项目标题“Decorating Promises Without Breaking Them”精准地指向了JavaScript中一个既实用又充满陷阱的高级主题。它探讨的不是简单地使用 Promise ,而是如何以更高级、更优雅的方式去“包装”或“增强” Promise ,同时保证其原有的、被广泛依赖的行为特性不被破坏。这听起来像是一个纯粹的学术问题,但实际上,它在构建健壮、可维护的现代前端应用、Node.js服务端应用乃至任何重度依赖异步流程的JavaScript项目中,都扮演着至关重要的角色。
想象一下这些场景:你需要为所有发起的网络请求自动添加超时控制;你想为每个异步操作统一添加日志记录,追踪其开始、成功或失败;你希望实现一个自动重试机制,当某个异步操作因网络抖动失败时能自动重试若干次;或者,你需要为所有异步操作注入统一的认证令牌或请求头。这些需求本质上都是在“装饰”一个原始的 Promise (比如 fetch 返回的 Promise )。如果装饰不当,你可能会遇到 Promise 链断裂、错误被静默吞没、状态管理混乱等难以调试的问题。因此,“不破坏承诺”是这项技术的底线和最高准则。
本文将从一名一线开发者的视角,深入拆解如何安全、有效地装饰 Promise 。我们将从理解 Promise 的核心契约开始,逐步构建起装饰器的几种经典模式,分析每种模式的适用场景与潜在风险,并分享在实际大型项目中积累的避坑经验和性能考量。无论你是想提升代码的抽象能力,还是正在为复杂的异步流程管理寻找优雅的解决方案,这篇文章都将为你提供一套可直接复用的“工具箱”和清晰的“安全操作指南”。
2. Promise的核心契约与装饰的边界
在动手装饰任何东西之前,我们必须彻底理解被装饰对象的“原厂规格”。对于 Promise ,它的核心契约远不止于“将来会有一个值”这么简单。破坏这些契约,轻则导致功能异常,重则引发难以追踪的幽灵bug。
2.1 不可变的状态机
Promise 是一个状态机,其状态一旦改变就不可逆:
- Pending(进行中) :初始状态。
- Fulfilled(已成功) :操作成功完成,拥有一个不可变的决议值(value)。
- Rejected(已失败) :操作失败,拥有一个不可变的拒绝原因(reason)。
装饰的第一条铁律:绝不能改变一个已决议(settled)Promise的状态或值。 这意味着,如果你的装饰器在 Promise 已经成功或失败后,试图去修改它最终传递的值,或者强行将其从失败转为成功(反之亦然),就彻底违背了 Promise 的设计哲学,会使得依赖于该 Promise 的下游代码行为完全不可预测。
2.2 Then方法的契约
Promise.prototype.then 方法是整个 Promise 链式调用的基石。根据规范, then 方法必须返回一个新的 Promise (记作 promise2 )。这个新 Promise 的决议行为,严格取决于 then 中传入的成功回调( onFulfilled )和失败回调( onRejected )的执行结果。
装饰的第二条铁律:必须保持thenable的链式特性。 装饰后的对象,必须仍然是一个标准的、行为可预测的 Thenable 对象(即拥有 then 方法)。装饰器最常见的错误之一,就是返回了一个非 Promise 对象,或者一个行为怪异的 Promise ,导致链式调用在某一环突然断裂。
2.3 错误传播与吞没
Promise 的错误处理机制是自动的、向下传播的。如果一个 Promise 被拒绝(rejected),并且没有对应的 catch 处理,这个拒绝状态会一直沿着链条向后传递,直到被捕获。这是 Promise 相较于传统回调模式的一大优势。
装饰的第三条铁律:绝不能无意中吞没错误。 这是装饰 Promise 时最高发的“事故”。例如,在装饰器添加的 then 回调中,如果发生了同步错误且未被捕获,或者错误被不当处理,就可能导致原始的拒绝原因被掩盖,替换成一个新的、可能信息量更少的错误,甚至让错误完全消失,使得调试变得极其困难。
2.4 微任务调度
Promise 的回调( then 、 catch 、 finally )被调度为微任务(microtask)。这意味着它们会在当前同步任务执行完毕、下一个宏任务开始之前被执行。这个特性保证了异步操作的时序可预测性。
装饰的第四条铁律:注意装饰逻辑的执行时机。 如果你的装饰逻辑涉及同步操作,它会被立即执行;如果涉及异步操作,你需要考虑它是否会引入不必要的宏任务延迟,从而影响整个应用的响应性或与其他微任务的交互顺序。一个设计不良的装饰器可能会破坏原有的、精密的微任务时序。
理解并尊重这些边界,是我们进行任何 Promise 装饰操作的前提。接下来,我们将看到,安全的装饰模式都是建立在对这些契约的严格遵守之上的。
3. 安全的Promise装饰模式解析
掌握了核心契约,我们就可以开始探讨具体的装饰模式了。根据装饰逻辑发生的位置和方式,我们可以将其归纳为几种经典模式,每种都有其特定的适用场景和需要警惕的陷阱。
3.1 包装模式:最通用与安全的基础
包装模式是最直观、也是最安全的装饰方式。核心思想是:接收一个原始 Promise (或一个返回 Promise 的函数),返回一个新的、包装过的 Promise 。新 Promise 内部会等待原始 Promise 决议,并在决议前后执行我们的装饰逻辑。
function decorateWithLogging(promise) {
// 立即记录开始,这是同步执行的装饰逻辑
console.log(‘[Async Operation] Started’);
// 返回一个新的Promise,它是我们装饰后的产物
return promise
.then(value => {
// 原始Promise成功后的装饰逻辑
console.log(‘[Async Operation] Succeeded:’, value);
// 重要:将原始值原封不动地传递下去
return value;
})
.catch(error => {
// 原始Promise失败后的装饰逻辑
console.error(‘[Async Operation] Failed:’, error);
// 重要:重新抛出错误,保持错误传播链
throw error;
});
}
// 使用示例
const rawPromise = fetch(‘/api/data’);
const decoratedPromise = decorateWithLogging(rawPromise);
decoratedPromise.then(data => console.log(‘Got data’, data));
为什么这是安全的?
- 状态不可变性 :我们从未试图修改原始
promise的状态。我们只是创建了一个新的promise,它“观察”原始promise并根据其结果执行额外操作。 - 链式完整性 :
decorateWithLogging函数返回的仍然是一个标准的Promise,可以继续调用then、catch。 - 错误传播 :在
catch块中,我们使用throw error重新抛出了原始错误。这确保了错误原因不变,且继续向下游传播。如果这里我们return了一个新值,就会将失败“转化”为成功,这通常是错误的。 - 值传递 :在
then块中,我们return value,将原始成功值透明地传递下去。
包装模式的变体:装饰函数 更常见的场景是,我们想装饰的是一个 会返回Promise的函数 ,而不是一个已经创建的Promise实例。
function withRetry(asyncFn, maxRetries = 3) {
return function (...args) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
asyncFn(...args)
.then(resolve) // 成功则直接解决外部Promise
.catch(error => {
attempts++;
if (attempts <= maxRetries) {
console.log(`Attempt ${attempts} failed, retrying...`);
attempt(); // 重试
} else {
reject(error); // 耗尽重试次数,抛出最终错误
}
});
}
attempt();
});
};
}
// 使用:装饰一个fetch函数
const reliableFetch = withRetry(fetch, 2);
reliableFetch(‘/api/unstable’).then(/* ... */);
这种模式将装饰逻辑提升到了函数级别,更加灵活,是创建可复用异步工具函数的基石。
3.2 继承扩展模式:面向对象的装饰
如果你正在使用ES6类,并且你的异步操作被封装在类的方法中,继承是一种结构化的装饰方式。
class BaseService {
async request(url) {
const response = await fetch(url);
return response.json();
}
}
class LoggedService extends BaseService {
async request(url) {
console.time(‘request’);
try {
const result = await super.request(url); // 调用父类原始方法
console.timeEnd(‘request’);
console.log(‘Success for’, url);
return result; // 返回原始结果
} catch (error) {
console.timeEnd(‘request’);
console.error(‘Failed for’, url, error);
throw error; // 重新抛出原始错误
}
}
}
优点 :结构清晰,符合面向对象设计原则,能很好地装饰整个类的方法族。 缺点 :不够灵活,无法动态装饰单个函数或已存在的Promise实例。且 super 调用是静态的,装饰逻辑与原始逻辑耦合在同一个类层次中。
3.3 高阶函数与中间件模式:管道化装饰
这是Node.js后端框架(如Express、Koa)中非常流行的模式,也适用于组织复杂的前端异步逻辑。其核心是创建一个装饰器(中间件)的管道(pipeline),每个装饰器接收一个“上下文”和 next 函数, next 函数代表执行下一个装饰器或最终的核心操作。
// 一个简单的Promise中间件实现
function createAsyncPipeline(...middlewares) {
return function (initialContext) {
let index = -1;
function dispatch(i, context) {
if (i <= index) return Promise.reject(new Error(‘next() called multiple times’));
index = i;
let middleware = middlewares[i];
// 如果所有中间件执行完毕,返回一个已解决的Promise作为终点
if (i === middlewares.length) {
return Promise.resolve(context);
}
try {
// 执行中间件,传入上下文和next函数(即调用下一个中间件)
return Promise.resolve(
middleware(context, () => dispatch(i + 1, context))
);
} catch (err) {
return Promise.reject(err);
}
}
return dispatch(0, initialContext);
};
}
// 定义装饰器(中间件)
const logger = async (ctx, next) => {
console.log(‘Request started:’, ctx.url);
const start = Date.now();
try {
const result = await next(); // 执行后续中间件和核心逻辑
console.log(`Request succeeded in ${Date.now() - start}ms`);
return result;
} catch (error) {
console.log(`Request failed in ${Date.now() - start}ms`);
throw error;
}
};
const timeout = (ms) => async (ctx, next) => {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)
);
// 竞速:核心操作与超时触发器
return Promise.race([next(), timeoutPromise]);
};
// 组合使用
const pipeline = createAsyncPipeline(logger, timeout(5000));
const context = { url: ‘/api/data’ };
pipeline(context)
.then(result => console.log(‘Final result:’, result))
.catch(err => console.error(‘Pipeline error:’, err));
这种模式的强大之处 在于其声明式和可组合性。你可以像搭积木一样,将超时、重试、认证、日志、缓存等装饰器任意组合,应用到不同的异步流程上,而核心业务逻辑保持干净。它严格遵循了“不破坏Promise”的原则,因为每个中间件都必须妥善处理它接收到的 Promise (即 next() 的返回值)并返回一个新的 Promise 。
4. 核心装饰器实现与避坑指南
理论说再多,不如亲手实现几个最常用的装饰器。在这一部分,我们将深入三个最典型装饰器——超时、重试、并发控制的实现细节,并重点分析其中容易踩坑的地方。
4.1 超时装饰器:与时间的赛跑
为异步操作添加超时控制是刚性需求。一个健壮的超时装饰器需要在时间耗尽时果断拒绝,但同时也要妥善处理原始 Promise ,避免资源泄漏。
/**
* 为Promise添加超时功能
* @param {Promise} promise 原始Promise
* @param {number} ms 超时时间(毫秒)
* @param {string|Error} [timeoutError] 自定义超时错误
* @returns {Promise} 带有超时控制的Promise
*/
function withTimeout(promise, ms, timeoutError = `Operation timed out after ${ms}ms`) {
// 创建一个在指定时间后拒绝的Promise
let timeoutId;
const timeoutPromise = new Promise((_, reject) => {
timeoutId = setTimeout(() => {
reject(timeoutError instanceof Error ? timeoutError : new Error(timeoutError));
}, ms);
});
// 使用Promise.race竞速
return Promise.race([promise, timeoutPromise])
.finally(() => {
// 关键清理步骤:无论谁赢,都要清除定时器,防止内存泄漏
clearTimeout(timeoutId);
});
}
避坑指南:
- 内存泄漏 :这是最隐蔽的坑。
setTimeout会持有回调引用,即使我们的timeoutPromise在竞速中输了(即原始promise先完成),定时器仍然存在于事件循环中,直到ms时间后才会被触发并释放。如果不手动clearTimeout,在装饰大量Promise时会造成严重的内存泄漏。finally块确保了在任何情况下都会执行清理。 - 错误信息丢失 :超时错误应该提供清晰的上下文。最好创建一个
Error实例,并包含超时时间等信息,方便调试。直接reject一个字符串不利于错误堆栈的捕获。 - 取消副作用 :
Promise.race只是忽略了较慢的那个Promise的结果,但并没有“取消”它。如果原始promise关联着一个实际的操作(如网络请求),这个操作可能仍在后台进行。真正的“取消”需要原始操作支持(如AbortController)。超时装饰器只解决“等待”层面的超时,而非“操作”层面的中止。
4.2 自动重试装饰器:提升鲁棒性
对于因网络抖动等临时性故障导致失败的操作,自动重试能显著提升成功率。重试逻辑需要考虑重试次数、退避策略和错误过滤。
/**
* 创建自动重试装饰器
* @param {Function} asyncFn 返回Promise的异步函数
* @param {Object} options 配置项
* @param {number} options.retries 最大重试次数(默认3)
* @param {Function} options.shouldRetry 判断错误是否应重试的函数 (error, attempt) => boolean
* @param {Function} options.delay 计算重试延迟的函数 (attempt) => ms
*/
function withRetry(asyncFn, options = {}) {
const { retries = 3, shouldRetry = () => true, delay = (attempt) => 0 } = options;
return function (...args) {
return new Promise((resolve, reject) => {
let attempt = 0;
function execute() {
asyncFn(...args)
.then(resolve)
.catch(error => {
attempt++;
const canRetry = attempt <= retries && shouldRetry(error, attempt);
if (!canRetry) {
// 不再重试,用最终错误拒绝
reject(error);
return;
}
const waitTime = delay(attempt);
console.warn(`Attempt ${attempt} failed. Retrying in ${waitTime}ms...`, error.message);
// 延迟后重试
setTimeout(execute, waitTime);
});
}
execute();
});
};
}
// 使用示例:指数退避策略
const fetchWithRetry = withRetry(fetch, {
retries: 5,
shouldRetry: (error) => {
// 只对网络错误或5xx服务器错误重试
return error.name === ‘TypeError’ || // 网络错误通常是TypeError
(error.status >= 500 && error.status < 600);
},
delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 30000) // 指数退避,上限30秒
});
避坑指南:
- 无脑重试 :并非所有错误都适合重试。像
401 Unauthorized(认证失败)或400 Bad Request(客户端错误),重试多少次都没用,只会增加服务器负担。shouldRetry选项至关重要,必须根据错误类型进行过滤。 - 重试风暴 :如果服务器宕机,所有客户端同时立即重试,会形成“重试风暴”,阻碍服务器恢复。 指数退避 是必须的。它让每次重试的等待时间指数级增加(如1s, 2s, 4s, 8s...),并加上随机抖动(jitter),可以有效打散客户端请求。
- 幂等性 :确保被重试的操作是 幂等 的。即重复执行多次与执行一次的效果相同。
GET、PUT、DELETE通常是幂等的,而POST(创建)往往不是。重试非幂等操作可能导致数据重复等副作用。 - 副作用累积 :每次重试,装饰器内部的
console.warn或日志记录都会执行。在高频操作中,这可能产生大量日志。生产环境中应考虑将日志级别调高或使用更智能的日志策略。
4.3 并发控制装饰器:限制资源消耗
有时我们需要限制同时执行的异步任务数量,例如避免同时发起太多网络请求拖垮浏览器或触发服务器限流。
/**
* 创建一个并发控制器
* @param {number} concurrency 最大并发数
* @returns {Function} 一个函数,接收返回Promise的函数,返回受控的Promise
*/
function createConcurrencyLimiter(concurrency) {
if (concurrency < 1) throw new Error(‘Concurrency must be at least 1’);
let running = 0;
const queue = [];
function runNext() {
// 如果并发数已满或队列为空,则停止
if (running >= concurrency || queue.length === 0) {
return;
}
running++;
const { asyncFn, args, resolve, reject } = queue.shift();
const taskPromise = Promise.resolve(asyncFn(...args));
taskPromise
.then((result) => {
resolve(result);
})
.catch((error) => {
reject(error);
})
.finally(() => {
running--;
// 一个任务完成,尝试执行下一个
process.nextTick(runNext); // 在Node.js中,使用nextTick避免递归爆栈。浏览器可用setTimeout(fn, 0)
});
// 如果还有空位,继续执行下一个(管道式填充)
if (running < concurrency) {
process.nextTick(runNext);
}
}
return function (asyncFn) {
return function (...args) {
return new Promise((resolve, reject) => {
// 将任务加入队列
queue.push({ asyncFn, args, resolve, reject });
// 尝试执行
process.nextTick(runNext);
});
};
};
}
// 使用示例:限制最多3个并发请求
const limit = createConcurrencyLimiter(3);
const limitedFetch = limit(fetch);
// 同时发起10个请求,但最多只有3个同时进行
const promises = Array.from({ length: 10 }, (_, i) =>
limitedFetch(`/api/item/${i}`).then(r => r.json())
);
Promise.all(promises).then(results => console.log(‘All done’, results));
避坑指南:
- 队列管理 :此实现使用了一个简单的FIFO(先进先出)队列。在复杂场景下,你可能需要优先级队列。确保队列操作是同步的,避免竞态条件。
- 递归调用与堆栈溢出 :在
finally中直接调用runNext(),如果任务完成速度极快,可能导致同步递归调用链过长,在JavaScript中可能引发堆栈溢出。使用process.nextTick(Node.js)或setTimeout(fn, 0)(浏览器)将下一次执行放到下一个事件循环,可以避免这个问题。 - 错误处理一致性 :装饰器必须确保原始
asyncFn的拒绝原因被正确地传递给调用者。我们在taskPromise.catch中直接reject(error),保证了这一点。 - 资源释放 :这个装饰器主要管理执行许可。如果任务本身持有资源(如文件句柄、数据库连接),需要在任务函数内部确保资源最终被释放,装饰器无法代劳。
5. 高级组合与性能考量
单个装饰器已经很有用,但真正的威力在于组合。然而,随意组合装饰器可能会引入意想不到的复杂性和性能开销。
5.1 装饰器的组合顺序
装饰器的组合顺序至关重要,不同的顺序可能产生完全不同的效果。
// 假设我们有两个装饰器:超时和重试
const fetchWithTimeoutAndRetry = withRetry(withTimeout(fetch, 2000), { retries: 2 });
const fetchWithRetryAndTimeout = withTimeout(withRetry(fetch, { retries: 2 }), 2000);
-
fetchWithTimeoutAndRetry:先加超时,再加重试。这意味着 每次重试都有独立的2秒超时 。如果一次请求在1.9秒时超时,会触发重试,新的请求又有2秒时间。总耗时可能接近2秒 * (重试次数+1)。 -
fetchWithRetryAndTimeout:先加重试,再加超时。这意味着 整个重试过程(包括所有重试)总共只有2秒时间 。如果第一次请求花了1.5秒失败,第二次请求可能只有0.5秒时间,这很可能不够。
哪种顺序正确?取决于你的业务逻辑。如果你希望每次尝试都有充足但有限的时间,用前者。如果你希望整个操作(含重试)必须在总时间内完成,用后者。你必须根据语义仔细设计顺序。
5.2 避免装饰器地狱与性能损耗
虽然装饰器模式很优雅,但过度嵌套会导致“装饰器地狱”,降低代码可读性,并带来性能损耗。
// 难以阅读的装饰器地狱
const superFetch = withMetrics(
withCache(
withAuth(
withRetry(
withTimeout(fetch, 5000),
{ retries: 3 }
),
getToken
),
{ ttl: 60000 }
),
metricsCollector
);
解决方案:
- 使用管道/中间件模式 :如前所述,中间件模式能以更声明式、线性的方式组合行为。
- 创建组合函数 :编写一个工具函数来组合多个装饰器。
function composeDecorators(...decorators) { return (fn) => decorators.reduceRight((wrapped, decorator) => decorator(wrapped), fn); } const enhance = composeDecorators( fn => withTimeout(fn, 5000), fn => withRetry(fn, { retries: 3 }), fn => withAuth(fn, getToken), ); const superFetch = enhance(fetch); - 性能考量 :每个装饰器都会引入额外的函数调用、
Promise封装和微任务。在性能关键的循环中(例如渲染每一帧时处理大量数据),这可能会成为瓶颈。对策包括:- 按需装饰 :只在必要时应用装饰器。
- 缓存装饰结果 :如果装饰器是纯函数(输出仅取决于输入),可以缓存装饰后的函数。
- 在更底层装饰 :如果可能,在更底层、调用次数更少的地方应用装饰(例如装饰一个会调用多次的模块入口函数,而不是装饰内部每个小函数)。
5.3 调试被装饰的Promise
装饰后的 Promise 在调试时,错误堆栈可能会变得冗长且令人困惑,因为堆栈中会包含装饰器内部的调用路径。
技巧:保持错误堆栈清晰
- 在创建自定义错误时,确保使用
new Error()并保留原始错误的堆栈信息。.catch(error => { const decoratedError = new Error(`Operation failed: ${error.message}`); // 将原始堆栈附加到新错误上,方便追踪 decoratedError.stack = `DecoratedError: ${decoratedError.message}\nCaused by: ${error.stack}`; throw decoratedError; }); - 在Node.js中,可以使用
Error.captureStackTrace来生成更清晰的堆栈。 - 在开发环境中,可以考虑给装饰后的
Promise添加一个自定义的Symbol属性,用于标识其被哪些装饰器处理过,辅助调试。
6. 实战:构建一个健壮的API客户端
让我们综合运用以上知识,构建一个用于生产环境的、功能丰富的API客户端装饰器。它将集成超时、重试、认证、请求/响应拦截、并发控制等常用功能。
// 核心装饰器组合工具
function compose(...decorators) {
return (fn) => decorators.reduceRight((acc, decorator) => decorator(acc), fn);
}
// 1. 基础请求函数(使用fetch)
async function coreRequest(url, options = {}) {
const response = await fetch(url, options);
if (!response.ok) {
const error = new Error(`HTTP ${response.status}: ${response.statusText}`);
error.status = response.status;
error.response = response;
throw error;
}
return response.json(); // 假设默认返回JSON
}
// 2. 各个装饰器工厂
const withBaseUrl = (baseUrl) => (fn) => (url, ...args) =>
fn(new URL(url, baseUrl).toString(), ...args);
const withTimeout = (ms) => (fn) => async (...args) => {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Request timeout after ${ms}ms`)), ms)
);
const requestPromise = fn(...args);
// 使用Promise.race,并在finally中清理
let timeoutId;
const timeoutWrapper = new Promise((_, reject) => {
timeoutId = setTimeout(() => reject(new Error(`Request timeout after ${ms}ms`)), ms);
});
return Promise.race([requestPromise, timeoutWrapper]).finally(() => clearTimeout(timeoutId));
};
const withRetry = ({ maxRetries = 3, baseDelay = 100, shouldRetry = (e) => e.status >= 500 }) => (fn) => {
return async function retryWrapped(...args) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn(...args);
} catch (error) {
lastError = error;
if (attempt === maxRetries || !shouldRetry(error)) break;
const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 100; // 指数退避+抖动
console.warn(`Attempt ${attempt + 1} failed, retrying in ${Math.round(delay)}ms`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError; // 抛出最后一次的错误
};
};
const withAuth = (getToken) => (fn) => async (...args) => {
const token = await getToken(); // getToken可以是同步或异步的
const [url, options = {}] = args;
const headers = new Headers(options.headers);
headers.set(‘Authorization’, `Bearer ${token}`);
return fn(url, { ...options, headers });
};
// 3. 创建增强的API客户端
function createApiClient(config) {
const {
baseUrl,
timeout = 10000,
retryOptions = {},
getToken,
// 可以继续添加缓存、日志等配置
} = config;
// 按顺序组合装饰器:认证 -> 重试 -> 超时 -> 基础URL -> 核心请求
// 注意:执行顺序是从右到左(reduceRight),所以列表后面的先应用
const decorators = [];
if (baseUrl) decorators.push(withBaseUrl(baseUrl));
decorators.push(withTimeout(timeout));
decorators.push(withRetry(retryOptions));
if (getToken) decorators.push(withAuth(getToken));
const enhancedRequest = compose(...decorators)(coreRequest);
// 返回一个具备常用方法的客户端对象
return {
get: (url, options) => enhancedRequest(url, { ...options, method: ‘GET’ }),
post: (url, data, options) => enhancedRequest(url, {
...options,
method: ‘POST’,
body: JSON.stringify(data),
headers: { ‘Content-Type’: ‘application/json’, ...options?.headers }
}),
// 可以继续添加put, delete, patch等方法
request: enhancedRequest, // 原始增强函数
};
}
// 4. 使用示例
const api = createApiClient({
baseUrl: ‘https://api.example.com’,
timeout: 8000,
retryOptions: { maxRetries: 2, shouldRetry: (e) => e.status === 429 || e.status >= 500 }, // 对429(限流)和5xx重试
getToken: async () => localStorage.getItem(‘auth_token’),
});
// 发起一个健壮的GET请求
api.get(‘/users/me’)
.then(user => console.log(‘User:’, user))
.catch(error => console.error(‘Request failed:’, error.message, error.status));
这个实战案例展示了如何将多个独立的、职责单一的装饰器组合成一个强大、可配置、易维护的API客户端。每个装饰器都严格遵守了“不破坏Promise”的原则,通过清晰的错误传播和资源管理,确保了整个异步流程的健壮性。你可以根据项目需要,轻松地添加或移除装饰器(如请求缓存、响应数据转换、全局错误处理等),而不会影响核心请求逻辑。
更多推荐



所有评论(0)