针对常见请求库如 Axios 和 ofetch 的痛点,我开发了 Soon-Fetch —— 一个仅 2.8kB 的现代化组合式请求方案,专注于解决拦截器混乱、过度封装和类型安全等问题。
设计哲学:组合优于继承
graph TD A[原生fetch] --> B[Soon-Fetch] B --> C{核心能力} C --> D[组合式拦截器] C --> E[类型安全] C --> F[取消请求] C --> G[自动重试] D --> H[独立管理] E --> I[TypeScript深度集成]
核心优势对比
特性 | Axios | ofetch | Soon-Fetch |
---|---|---|---|
体积大小 | 13.4kB | 4.2kB | 2.8kB |
拦截器管理 | 全局/实例级 | 有限支持 | 组合式 |
TypeScript支持 | 良好 | 良好 | 极致类型推导 |
取消请求 | CancelToken(弃用) | AbortController | 组合式取消 |
树摇优化(TreeShaking) | 部分支持 | 优秀 | 100%支持 |
学习曲线 | 中等 | 简单 | 极简API |
实战演示:解决拦截器混乱问题
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Soon-Fetch 组合式请求库</title>
<script src="https://cdn.jsdelivr.net/npm/soon-fetch@1.0.0/dist/soon-fetch.min.js"></script>
<style>
:root {
--primary: #4361ee;
--success: #06d6a0;
--warning: #ffd166;
--danger: #ef476f;
--dark: #2b2d42;
--light: #f8f9fa;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
min-height: 100vh;
padding: 2rem;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 3rem;
padding: 2rem;
background: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
}
h1 {
font-size: 3rem;
color: var(--primary);
margin-bottom: 1rem;
background: linear-gradient(90deg, var(--primary), var(--success));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.tagline {
font-size: 1.2rem;
color: var(--dark);
opacity: 0.8;
max-width: 700px;
margin: 0 auto;
}
.size-badge {
display: inline-block;
background: var(--success);
color: white;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-weight: bold;
margin-top: 1rem;
}
.comparison {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}
.card {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.1);
}
.card h2 {
margin-bottom: 1.5rem;
padding-bottom: 1rem;
border-bottom: 2px solid #eee;
}
.problem-card {
border-top: 4px solid var(--danger);
}
.solution-card {
border-top: 4px solid var(--success);
}
.code-block {
background: #1e1e1e;
color: #d4d4d4;
border-radius: 8px;
padding: 1.5rem;
font-family: 'Fira Code', monospace;
font-size: 0.9rem;
overflow-x: auto;
margin: 1.5rem 0;
line-height: 1.5;
}
.keyword {
color: #569cd6;
}
.function {
color: #dcdcaa;
}
.string {
color: #ce9178;
}
.comment {
color: #6a9955;
}
.demo-area {
background: white;
border-radius: 16px;
padding: 2rem;
box-shadow: 0 10px 30px rgba(0,0,0,0.05);
margin-top: 2rem;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
button {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 8px;
background: var(--primary);
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
}
button:hover {
background: #3251d4;
transform: translateY(-2px);
}
button.danger {
background: var(--danger);
}
button.danger:hover {
background: #d2315d;
}
.response-area {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
min-height: 200px;
font-family: 'Fira Code', monospace;
white-space: pre-wrap;
overflow: auto;
max-height: 400px;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-pending {
background: var(--warning);
animation: pulse 1.5s infinite;
}
.status-success {
background: var(--success);
}
.status-error {
background: var(--danger);
}
@keyframes pulse {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
footer {
text-align: center;
margin-top: 3rem;
padding: 2rem;
color: var(--dark);
opacity: 0.7;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Soon-Fetch</h1>
<p class="tagline">一个仅 <span class="size-badge">2.8kB</span> 的组合式请求库,解决传统请求库的拦截器痛点</p>
</header>
<div class="comparison">
<div class="card problem-card">
<h2>传统请求库的痛点</h2>
<ul>
<li>拦截器层层嵌套,难以维护</li>
<li>全局拦截器影响所有请求</li>
<li>取消机制复杂且不一致</li>
<li>类型安全支持不足</li>
<li>过度封装导致体积膨胀</li>
</ul>
<div class="code-block">
<span class="comment">// Axios 拦截器的典型问题</span><br>
<span class="keyword">const</span> instance = axios.<span class="function">create</span>();<br><br>
<span class="comment">// 全局请求拦截器</span><br>
instance.<span class="function">interceptors</span>.request.<span class="function">use</span>(<span class="keyword">function</span> (config) {<br>
<span class="comment">// 添加认证头</span><br>
config.headers.Authorization = <span class="string">`Bearer ${token}`</span>;<br>
<span class="keyword">return</span> config;<br>
});<br><br>
<span class="comment">// 另一个拦截器 - 但顺序很重要!</span><br>
instance.<span class="function">interceptors</span>.request.<span class="function">use</span>(<span class="keyword">function</span> (config) {<br>
<span class="comment">// 添加追踪ID</span><br>
config.headers['X-Trace-Id'] = <span class="function">generateId</span>();<br>
<span class="keyword">return</span> config;<br>
});<br><br>
<span class="comment">// 问题: 所有请求都会应用这些拦截器</span><br>
<span class="comment">// 难以针对特定请求禁用部分逻辑</span>
</div>
</div>
<div class="card solution-card">
<h2>Soon-Fetch 的解决方案</h2>
<ul>
<li>组合式拦截器 - 按需组合</li>
<li>请求级拦截控制</li>
<li>现代化取消机制</li>
<li>一流的TypeScript支持</li>
<li>极致轻量无依赖</li>
</ul>
<div class="code-block">
<span class="keyword">import</span> { createFetch } <span class="keyword">from</span> <span class="string">'soon-fetch'</span>;<br><br>
<span class="comment">// 创建可组合的拦截器单元</span><br>
<span class="keyword">const</span> withAuth = {<br>
onRequest: (req) => {<br>
req.headers.set(<span class="string">'Authorization'</span>, <span class="string">`Bearer ${token}`</span>);<br>
}<br>
};<br><br>
<span class="keyword">const</span> withLogging = {<br>
onRequest: (req) => console.<span class="function">log</span>(<span class="string">'Request:'</span>, req),<br>
onResponse: (res) => console.<span class="function">log</span>(<span class="string">'Response:'</span>, res)<br>
};<br><br>
<span class="comment">// 组合特定请求</span><br>
<span class="keyword">const</span> fetchUser = createFetch(<br>
<span class="string">'/api/user'</span>,<br>
{ interceptors: [withAuth] } <span class="comment">// 仅使用认证拦截器</span><br>
);<br><br>
<span class="comment">// 另一个请求使用不同组合</span><br>
<span class="keyword">const</span> fetchProducts = createFetch(<br>
<span class="string">'/api/products'</span>,<br>
{ interceptors: [withAuth, withLogging] } <span class="comment">// 认证+日志</span><br>
);
</div>
</div>
</div>
<div class="demo-area">
<h2>实时演示:组合式请求</h2>
<div class="controls">
<button id="fetchUser">获取用户数据 (仅认证)</button>
<button id="fetchProducts">获取产品数据 (认证+日志)</button>
<button id="fetchPublic">获取公共数据 (无拦截器)</button>
<button id="cancelRequest" class="danger">取消请求</button>
</div>
<div class="response-area">
<p id="responseStatus">等待请求...</p>
<pre id="responseData"></pre>
</div>
</div>
<div class="card">
<h2>核心功能:现代化取消机制</h2>
<div class="code-block">
<span class="keyword">import</span> { createFetch, withAbort } <span class="keyword">from</span> <span class="string">'soon-fetch'</span>;<br><br>
<span class="comment">// 创建支持取消的请求</span><br>
<span class="keyword">const</span> [fetch, abort] = createFetch(<br>
<span class="string">'/api/large-data'</span>,<br>
{ <br>
interceptors: [withAbort()] <span class="comment">// 添加AbortController支持</span><br>
}<br>
);<br><br>
<span class="comment">// 发起请求</span><br>
<span class="keyword">const</span> dataPromise = <span class="function">fetch</span>();<br><br>
<span class="comment">// 需要时取消请求</span><br>
<span class="function">abort</span>(<span class="string">'用户取消了操作'</span>);<br><br>
<span class="comment">// 使用方式2:超时自动取消</span><br>
<span class="keyword">const</span> [fetchWithTimeout] = createFetch(<br>
<span class="string">'/api/slow'</span>,<br>
{ <br>
interceptors: [withAbort({ timeout: 5000 })] <span class="comment">// 5秒超时</span><br>
}<br>
);
</div>
</div>
<footer>
<p>Soon-Fetch © 2023 - 轻量、组合、强大的现代请求解决方案</p>
<p>GZIP压缩后仅2.8kB | MIT许可证 | 零依赖</p>
</footer>
</div>
<script>
// 模拟实现 Soon-Fetch 的核心功能
class SoonFetch {
constructor(baseUrl = '') {
this.baseUrl = baseUrl;
this.interceptors = {
request: [],
response: []
};
this.controller = null;
}
use(interceptor) {
if (interceptor.onRequest) {
this.interceptors.request.push(interceptor.onRequest);
}
if (interceptor.onResponse) {
this.interceptors.response.push(interceptor.onResponse);
}
return this;
}
async fetch(endpoint, options = {}) {
// 创建新的AbortController
this.controller = new AbortController();
// 合并选项
const fetchOptions = {
...options,
signal: this.controller.signal
};
// 处理请求拦截器
let request = new Request(`${this.baseUrl}${endpoint}`, fetchOptions);
for (const interceptor of this.interceptors.request) {
request = interceptor(request) || request;
}
// 更新状态显示
updateStatus('pending', '请求中...');
try {
// 发起实际请求
let response = await window.fetch(request);
// 处理响应拦截器
for (const interceptor of this.interceptors.response) {
response = interceptor(response) || response;
}
// 处理JSON响应
const data = await response.json();
// 更新状态
updateStatus('success', '请求成功');
displayResponse(data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
updateStatus('error', '请求已取消');
displayResponse({ error: '请求被用户取消' });
} else {
updateStatus('error', `请求失败: ${error.message}`);
displayResponse({ error: error.message });
}
throw error;
}
}
abort(reason = '请求已取消') {
if (this.controller) {
this.controller.abort(reason);
this.controller = null;
}
}
}
// 创建拦截器
const withAuth = {
onRequest: (request) => {
const newHeaders = new Headers(request.headers);
newHeaders.set('Authorization', 'Bearer demo-token-abc123');
return new Request(request, { headers: newHeaders });
}
};
const withLogging = {
onRequest: (request) => {
console.log('请求拦截:', request.url);
return request;
},
onResponse: (response) => {
console.log('响应拦截:', response.status);
return response;
}
};
// 更新UI状态
function updateStatus(status, message) {
const statusEl = document.getElementById('responseStatus');
let statusHtml = '';
if (status === 'pending') {
statusHtml = `<span class="status-indicator status-pending"></span>${message}`;
} else if (status === 'success') {
statusHtml = `<span class="status-indicator status-success"></span>${message}`;
} else {
statusHtml = `<span class="status-indicator status-error"></span>${message}`;
}
statusEl.innerHTML = statusHtml;
}
// 显示响应数据
function displayResponse(data) {
const responseDataEl = document.getElementById('responseData');
responseDataEl.textContent = JSON.stringify(data, null, 2);
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 创建不同配置的请求实例
const userFetcher = new SoonFetch('https://jsonplaceholder.typicode.com');
userFetcher.use(withAuth);
const productFetcher = new SoonFetch('https://jsonplaceholder.typicode.com');
productFetcher.use(withAuth);
productFetcher.use(withLogging);
const publicFetcher = new SoonFetch('https://jsonplaceholder.typicode.com');
// 绑定按钮事件
document.getElementById('fetchUser').addEventListener('click', async () => {
updateStatus('pending', '获取用户数据...');
try {
await userFetcher.fetch('/users/1');
} catch (e) {
// 错误已在内部处理
}
});
document.getElementById('fetchProducts').addEventListener('click', async () => {
updateStatus('pending', '获取产品数据...');
try {
await productFetcher.fetch('/todos/1');
} catch (e) {
// 错误已在内部处理
}
});
document.getElementById('fetchPublic').addEventListener('click', async () => {
updateStatus('pending', '获取公共数据...');
try {
await publicFetcher.fetch('/posts/1');
} catch (e) {
// 错误已在内部处理
}
});
document.getElementById('cancelRequest').addEventListener('click', () => {
userFetcher.abort();
productFetcher.abort();
publicFetcher.abort();
});
});
</script>
</body>
</html>
技术亮点解析
组合式拦截器系统
- 将拦截器拆分为独立单元,可按需组合
- 支持请求级别的拦截器控制
- 避免全局拦截器污染
现代化取消机制
- 基于标准的AbortController实现
- 提供超时自动取消功能
- 支持手动取消和取消原因传递
极致类型安全 “`typescript import { createFetch } from ‘soon-fetch’;
// 定义强类型端点 const fetchUser = createFetch<{
id: number;
name: string;
email: string;
}>(‘/api/user’);
// 使用时获得完整类型提示 const user = await fetchUser(); console.log(user.name); // 正确类型推断
4. **智能重试机制**
```typescript
import { withRetry } from 'soon-fetch/interceptors';
// 创建带重试的请求
const fetchData = createFetch('/api/unstable', {
interceptors: [
withRetry({
retries: 3,
delay: 1000,
retryOn: [502, 503] // 仅在特定状态码重试
})
]
});
何时选择 Soon-Fetch?
- 需要精细控制请求/响应流程的项目
- 追求极致性能和轻量化的应用
- 需要灵活组合不同请求策略的场景
- TypeScript项目需要深度类型支持
- 现代浏览器环境(无需支持IE)
Soon-Fetch 已在 GitHub 开源,欢迎贡献和反馈! 它代表了请求库发展的新方向——更轻量、更组合、更符合现代前端开发需求。
与其继续在复杂的拦截器链中挣扎,不如尝试这种声明式的组合式请求方案。3kB的大小,带来的却是开发体验的巨大提升。