Soon-Fetch:顛覆傳統(tǒng)的輕量級(jí)組合式請(qǐng)求庫(kù)

針對(duì)常見請(qǐng)求庫(kù)如 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)勢(shì)對(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í)曲線 中等 簡(jiǎn)單 極簡(jiǎn)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)求庫(kù)</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)求庫(kù),解決傳統(tǒng)請(qǐng)求庫(kù)的攔截器痛點(diǎn)</p>
    </header>
    
    <div class="comparison">
      <div class="card problem-card">
        <h2>傳統(tǒng)請(qǐng)求庫(kù)的痛點(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>
          &nbsp;&nbsp;<span class="comment">// 添加認(rèn)證頭</span><br>
          &nbsp;&nbsp;config.headers.Authorization = <span class="string">`Bearer ${token}`</span>;<br>
          &nbsp;&nbsp;<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>
          &nbsp;&nbsp;<span class="comment">// 添加追蹤ID</span><br>
          &nbsp;&nbsp;config.headers['X-Trace-Id'] = <span class="function">generateId</span>();<br>
          &nbsp;&nbsp;<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>
          &nbsp;&nbsp;onRequest: (req) => {<br>
          &nbsp;&nbsp;&nbsp;&nbsp;req.headers.set(<span class="string">'Authorization'</span>, <span class="string">`Bearer ${token}`</span>);<br>
          &nbsp;&nbsp;}<br>
          };<br><br>
          
          <span class="keyword">const</span> withLogging = {<br>
          &nbsp;&nbsp;onRequest: (req) => console.<span class="function">log</span>(<span class="string">'Request:'</span>, req),<br>
          &nbsp;&nbsp;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>
          &nbsp;&nbsp;<span class="string">'/api/user'</span>,<br>
          &nbsp;&nbsp;{ 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>
          &nbsp;&nbsp;<span class="string">'/api/products'</span>,<br>
          &nbsp;&nbsp;{ 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>
        &nbsp;&nbsp;<span class="string">'/api/large-data'</span>,<br>
        &nbsp;&nbsp;{ <br>
        &nbsp;&nbsp;&nbsp;&nbsp;interceptors: [withAbort()] <span class="comment">// 添加AbortController支持</span><br>
        &nbsp;&nbsp;}<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>
        &nbsp;&nbsp;<span class="string">'/api/slow'</span>,<br>
        &nbsp;&nbsp;{ <br>
        &nbsp;&nbsp;&nbsp;&nbsp;interceptors: [withAbort({ timeout: 5000 })] <span class="comment">// 5秒超時(shí)</span><br>
        &nbsp;&nbsp;}<br>
        );
      </div>
    </div>
    
    <footer>
      <p>Soon-Fetch &copy; 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)解析

  1. 組合式攔截器系統(tǒng)

    • 將攔截器拆分為獨(dú)立單元,可按需組合
    • 支持請(qǐng)求級(jí)別的攔截器控制
    • 避免全局?jǐn)r截器污染
  2. 現(xiàn)代化取消機(jī)制

    • 基于標(biāo)準(zhǔn)的AbortController實(shí)現(xiàn)
    • 提供超時(shí)自動(dòng)取消功能
    • 支持手動(dòng)取消和取消原因傳遞
  3. 極致類型安全 “`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)求策略的場(chǎng)景
  • TypeScript項(xiàng)目需要深度類型支持
  • 現(xiàn)代瀏覽器環(huán)境(無需支持IE)

Soon-Fetch 已在 GitHub 開源,歡迎貢獻(xiàn)和反饋! 它代表了請(qǐng)求庫(kù)發(fā)展的新方向——更輕量、更組合、更符合現(xiàn)代前端開發(fā)需求。

與其繼續(xù)在復(fù)雜的攔截器鏈中掙扎,不如嘗試這種聲明式的組合式請(qǐng)求方案。3kB的大小,帶來的卻是開發(fā)體驗(yàn)的巨大提升。