Soon-Fetch:顛覆傳統(tǒng)的輕量級(jí)組合式請(qǐng)求庫
針對(duì)常見請(qǐng)求庫如 Axios 和 ofetch 的痛點(diǎn),我開發(fā)了 Soon-Fetch —— 一個(gè)僅 2.8kB 的現(xiàn)代化組合式請(qǐng)求方案,專注于解決攔截器混亂、過度封裝和類型安全等問題。
設(shè)計(jì)哲學(xué):組合優(yōu)于繼承
graph TD A[原生fetch] --> B[Soon-Fetch] B --> C{核心能力} C --> D[組合式攔截器] C --> E[類型安全] C --> F[取消請(qǐng)求] C --> G[自動(dòng)重試] D --> H[獨(dú)立管理] E --> I[TypeScript深度集成]
核心優(yōu)勢對(duì)比
特性 | Axios | ofetch | Soon-Fetch |
---|---|---|---|
體積大小 | 13.4kB | 4.2kB | 2.8kB |
攔截器管理 | 全局/實(shí)例級(jí) | 有限支持 | 組合式 |
TypeScript支持 | 良好 | 良好 | 極致類型推導(dǎo) |
取消請(qǐng)求 | CancelToken(棄用) | AbortController | 組合式取消 |
樹搖優(yōu)化(TreeShaking) | 部分支持 | 優(yōu)秀 | 100%支持 |
學(xué)習(xí)曲線 | 中等 | 簡單 | 極簡API |
實(shí)戰(zhàn)演示:解決攔截器混亂問題
<!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 組合式請(qǐng)求庫</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">一個(gè)僅 <span class="size-badge">2.8kB</span> 的組合式請(qǐng)求庫,解決傳統(tǒng)請(qǐng)求庫的攔截器痛點(diǎn)</p>
</header>
<div class="comparison">
<div class="card problem-card">
<h2>傳統(tǒng)請(qǐng)求庫的痛點(diǎn)</h2>
<ul>
<li>攔截器層層嵌套,難以維護(hù)</li>
<li>全局?jǐn)r截器影響所有請(qǐng)求</li>
<li>取消機(jī)制復(fù)雜且不一致</li>
<li>類型安全支持不足</li>
<li>過度封裝導(dǎo)致體積膨脹</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">// 全局請(qǐng)求攔截器</span><br>
instance.<span class="function">interceptors</span>.request.<span class="function">use</span>(<span class="keyword">function</span> (config) {<br>
<span class="comment">// 添加認(rèn)證頭</span><br>
config.headers.Authorization = <span class="string">`Bearer ${token}`</span>;<br>
<span class="keyword">return</span> config;<br>
});<br><br>
<span class="comment">// 另一個(gè)攔截器 - 但順序很重要!</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">// 問題: 所有請(qǐng)求都會(huì)應(yīng)用這些攔截器</span><br>
<span class="comment">// 難以針對(duì)特定請(qǐng)求禁用部分邏輯</span>
</div>
</div>
<div class="card solution-card">
<h2>Soon-Fetch 的解決方案</h2>
<ul>
<li>組合式攔截器 - 按需組合</li>
<li>請(qǐng)求級(jí)攔截控制</li>
<li>現(xiàn)代化取消機(jī)制</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">// 創(chuàng)建可組合的攔截器單元</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">// 組合特定請(qǐng)求</span><br>
<span class="keyword">const</span> fetchUser = createFetch(<br>
<span class="string">'/api/user'</span>,<br>
{ interceptors: [withAuth] } <span class="comment">// 僅使用認(rèn)證攔截器</span><br>
);<br><br>
<span class="comment">// 另一個(gè)請(qǐng)求使用不同組合</span><br>
<span class="keyword">const</span> fetchProducts = createFetch(<br>
<span class="string">'/api/products'</span>,<br>
{ interceptors: [withAuth, withLogging] } <span class="comment">// 認(rèn)證+日志</span><br>
);
</div>
</div>
</div>
<div class="demo-area">
<h2>實(shí)時(shí)演示:組合式請(qǐng)求</h2>
<div class="controls">
<button id="fetchUser">獲取用戶數(shù)據(jù) (僅認(rèn)證)</button>
<button id="fetchProducts">獲取產(chǎn)品數(shù)據(jù) (認(rèn)證+日志)</button>
<button id="fetchPublic">獲取公共數(shù)據(jù) (無攔截器)</button>
<button id="cancelRequest" class="danger">取消請(qǐng)求</button>
</div>
<div class="response-area">
<p id="responseStatus">等待請(qǐng)求...</p>
<pre id="responseData"></pre>
</div>
</div>
<div class="card">
<h2>核心功能:現(xiàn)代化取消機(jī)制</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">// 創(chuàng)建支持取消的請(qǐng)求</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">// 發(fā)起請(qǐng)求</span><br>
<span class="keyword">const</span> dataPromise = <span class="function">fetch</span>();<br><br>
<span class="comment">// 需要時(shí)取消請(qǐng)求</span><br>
<span class="function">abort</span>(<span class="string">'用戶取消了操作'</span>);<br><br>
<span class="comment">// 使用方式2:超時(shí)自動(dòng)取消</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秒超時(shí)</span><br>
}<br>
);
</div>
</div>
<footer>
<p>Soon-Fetch © 2023 - 輕量、組合、強(qiáng)大的現(xiàn)代請(qǐng)求解決方案</p>
<p>GZIP壓縮后僅2.8kB | MIT許可證 | 零依賴</p>
</footer>
</div>
<script>
// 模擬實(shí)現(xiàn) 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 = {}) {
// 創(chuàng)建新的AbortController
this.controller = new AbortController();
// 合并選項(xiàng)
const fetchOptions = {
...options,
signal: this.controller.signal
};
// 處理請(qǐng)求攔截器
let request = new Request(`${this.baseUrl}${endpoint}`, fetchOptions);
for (const interceptor of this.interceptors.request) {
request = interceptor(request) || request;
}
// 更新狀態(tài)顯示
updateStatus('pending', '請(qǐng)求中...');
try {
// 發(fā)起實(shí)際請(qǐng)求
let response = await window.fetch(request);
// 處理響應(yīng)攔截器
for (const interceptor of this.interceptors.response) {
response = interceptor(response) || response;
}
// 處理JSON響應(yīng)
const data = await response.json();
// 更新狀態(tài)
updateStatus('success', '請(qǐng)求成功');
displayResponse(data);
return data;
} catch (error) {
if (error.name === 'AbortError') {
updateStatus('error', '請(qǐng)求已取消');
displayResponse({ error: '請(qǐng)求被用戶取消' });
} else {
updateStatus('error', `請(qǐng)求失敗: ${error.message}`);
displayResponse({ error: error.message });
}
throw error;
}
}
abort(reason = '請(qǐng)求已取消') {
if (this.controller) {
this.controller.abort(reason);
this.controller = null;
}
}
}
// 創(chuàng)建攔截器
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('請(qǐng)求攔截:', request.url);
return request;
},
onResponse: (response) => {
console.log('響應(yīng)攔截:', response.status);
return response;
}
};
// 更新UI狀態(tài)
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;
}
// 顯示響應(yīng)數(shù)據(jù)
function displayResponse(data) {
const responseDataEl = document.getElementById('responseData');
responseDataEl.textContent = JSON.stringify(data, null, 2);
}
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 創(chuàng)建不同配置的請(qǐng)求實(shí)例
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', '獲取用戶數(shù)據(jù)...');
try {
await userFetcher.fetch('/users/1');
} catch (e) {
// 錯(cuò)誤已在內(nèi)部處理
}
});
document.getElementById('fetchProducts').addEventListener('click', async () => {
updateStatus('pending', '獲取產(chǎn)品數(shù)據(jù)...');
try {
await productFetcher.fetch('/todos/1');
} catch (e) {
// 錯(cuò)誤已在內(nèi)部處理
}
});
document.getElementById('fetchPublic').addEventListener('click', async () => {
updateStatus('pending', '獲取公共數(shù)據(jù)...');
try {
await publicFetcher.fetch('/posts/1');
} catch (e) {
// 錯(cuò)誤已在內(nèi)部處理
}
});
document.getElementById('cancelRequest').addEventListener('click', () => {
userFetcher.abort();
productFetcher.abort();
publicFetcher.abort();
});
});
</script>
</body>
</html>
技術(shù)亮點(diǎn)解析
組合式攔截器系統(tǒng)
- 將攔截器拆分為獨(dú)立單元,可按需組合
- 支持請(qǐng)求級(jí)別的攔截器控制
- 避免全局?jǐn)r截器污染
現(xiàn)代化取消機(jī)制
- 基于標(biāo)準(zhǔn)的AbortController實(shí)現(xiàn)
- 提供超時(shí)自動(dòng)取消功能
- 支持手動(dòng)取消和取消原因傳遞
極致類型安全 “`typescript import { createFetch } from ‘soon-fetch’;
// 定義強(qiáng)類型端點(diǎn) const fetchUser = createFetch<{
id: number;
name: string;
email: string;
}>(‘/api/user’);
// 使用時(shí)獲得完整類型提示 const user = await fetchUser(); console.log(user.name); // 正確類型推斷
4. **智能重試機(jī)制**
```typescript
import { withRetry } from 'soon-fetch/interceptors';
// 創(chuàng)建帶重試的請(qǐng)求
const fetchData = createFetch('/api/unstable', {
interceptors: [
withRetry({
retries: 3,
delay: 1000,
retryOn: [502, 503] // 僅在特定狀態(tài)碼重試
})
]
});
何時(shí)選擇 Soon-Fetch?
- 需要精細(xì)控制請(qǐng)求/響應(yīng)流程的項(xiàng)目
- 追求極致性能和輕量化的應(yīng)用
- 需要靈活組合不同請(qǐng)求策略的場景
- TypeScript項(xiàng)目需要深度類型支持
- 現(xiàn)代瀏覽器環(huán)境(無需支持IE)
Soon-Fetch 已在 GitHub 開源,歡迎貢獻(xiàn)和反饋! 它代表了請(qǐng)求庫發(fā)展的新方向——更輕量、更組合、更符合現(xiàn)代前端開發(fā)需求。
與其繼續(xù)在復(fù)雜的攔截器鏈中掙扎,不如嘗試這種聲明式的組合式請(qǐng)求方案。3kB的大小,帶來的卻是開發(fā)體驗(yàn)的巨大提升。