浏览器请求指南

630 阅读7分钟

📡 浏览器请求

1.1 一次完整请求的旅程

// 你写的代码
fetch('https://api.example.com/data', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ id: 123 })
});

// 底层发生的事情
const requestJourney = {
  1: '【JS调用】fetch/XHR 发起请求',
  2: '【渲染进程】请求拦截(Service Worker)',
  3: '【IPC通信】渲染进程 → 浏览器进程',
  4: '【浏览器进程】转发 → 网络进程',
  5: '【网络进程】查找缓存(内存/磁盘)',
  6: '【DNS解析】域名 → IP(有缓存则跳过)',
  7: '【TCP连接】三次握手(有连接池则复用)',
  8: '【TLS协商】HTTPS握手(如果是HTTPS)',
  9: '【发送请求】构建HTTP报文 → 发送',
  10: '【服务器处理】等待响应',
  11: '【接收响应】解析HTTP报文',
  12: '【缓存决策】是否缓存响应',
  13: '【IPC通信】网络进程 → 渲染进程',
  14: '【渲染进程】回调JS Promise'
};

1.2 请求的底层数据结构

// HTTP请求报文结构
const httpRequest = {
  // 请求行
  requestLine: {
    method: 'POST',
    path: '/data',
    version: 'HTTP/2'
  },
  
  // 请求头
  headers: {
    host: 'api.example.com',
    'content-type': 'application/json',
    'content-length': 13,
    'user-agent': 'Chrome/120.0.0.0',
    'accept-encoding': 'gzip, deflate, br',
    connection: 'keep-alive'
  },
  
  // 请求体
  body: '{"id":123}'
};

// HTTP响应报文
const httpResponse = {
  // 状态行
  statusLine: {
    version: 'HTTP/2',
    statusCode: 200,
    statusText: 'OK'
  },
  
  // 响应头
  headers: {
    'content-type': 'application/json',
    'content-length': 256,
    'cache-control': 'max-age=3600',
    'content-encoding': 'gzip'
  },
  
  // 响应体
  body: '{"data":"..."}'
};

🔧 请求的实现方式

2.1 XMLHttpRequest(老牌但仍有价值)

class XHRRequest {
  constructor(options = {}) {
    this.xhr = new XMLHttpRequest();
    this.options = options;
  }
  
  send(method, url, data = null) {
    return new Promise((resolve, reject) => {
      // 请求配置
      this.xhr.open(method, url, true); // 异步
      
      // 设置请求头
      if (this.options.headers) {
        Object.entries(this.options.headers).forEach(([key, value]) => {
          this.xhr.setRequestHeader(key, value);
        });
      }
      
      // 设置超时
      if (this.options.timeout) {
        this.xhr.timeout = this.options.timeout;
        this.xhr.ontimeout = () => reject(new Error('请求超时'));
      }
      
      // 监听状态变化
      this.xhr.onreadystatechange = () => {
        if (this.xhr.readyState === 4) {
          if (this.xhr.status >= 200 && this.xhr.status < 300) {
            resolve({
              status: this.xhr.status,
              statusText: this.xhr.statusText,
              data: this.xhr.response,
              headers: this.parseHeaders(this.xhr.getAllResponseHeaders())
            });
          } else {
            reject(new Error(`HTTP ${this.xhr.status}`));
          }
        }
      };
      
      // 错误处理
      this.xhr.onerror = () => reject(new Error('网络错误'));
      this.xhr.onabort = () => reject(new Error('请求取消'));
      
      // 发送请求
      this.xhr.send(data);
    });
  }
  
  // 监听上传进度
  onUploadProgress(callback) {
    this.xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = (event.loaded / event.total) * 100;
        callback(percent, event);
      }
    };
  }
  
  // 监听下载进度
  onDownloadProgress(callback) {
    this.xhr.onprogress = (event) => {
      if (event.lengthComputable) {
        const percent = (event.loaded / event.total) * 100;
        callback(percent, event);
      }
    };
  }
  
  // 取消请求
  abort() {
    this.xhr.abort();
  }
  
  parseHeaders(headerStr) {
    const headers = {};
    headerStr.split('\r\n').forEach(line => {
      const [key, value] = line.split(': ');
      if (key && value) headers[key.toLowerCase()] = value;
    });
    return headers;
  }
}

// 使用
const request = new XHRRequest({ timeout: 5000 });
request.onUploadProgress(percent => console.log(`上传: ${percent}%`));
request.send('POST', '/api/data', JSON.stringify({ id: 123 }))
  .then(res => console.log(res))
  .catch(err => console.error(err));

2.2 Fetch API(现代、Promise-based)

// 最简单的 GET 请求
fetch('https://api.example.com/data')
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error('错误:', error));

// async/await 写法
async function getData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('错误:', error);
  }
}

2.3 Axios(最流行的 HTTP 客户端)

// Axios 的核心功能 [citation:2][citation:4]
const axiosFeatures = {
  // 1. 基于 Promise
  promise: '✅ 完全支持 async/await',
  
  // 2. 拦截器(请求/响应)
  interceptors: '✅ 可在请求前后添加全局逻辑',
  
  // 3. 自动 JSON 转换
  autoJSON: '✅ 自动序列化/反序列化',
  
  // 4. 请求取消
  cancelToken: '✅ 支持取消进行中的请求',
  
  // 5. 并发请求
  concurrent: '✅ axios.all/axios.spread',
  
  // 6. 浏览器兼容性
  compatibility: '✅ 支持 IE11+',
  
  // 7. XSRF 保护
  xsrf: '✅ 内置 CSRF 防护'
};

基本使用:

import axios from 'axios';

// 创建实例(推荐)
const api = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: { 'X-Custom-Header': 'foobar' }
});

// 请求拦截器
api.interceptors.request.use(config => {
  // 添加 token
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});

// 响应拦截器
api.interceptors.response.use(response => {
  // 统一处理响应数据
  return response.data;
}, error => {
  // 统一处理错误
  if (error.response?.status === 401) {
    // 跳转登录
    window.location.href = '/login';
  }
  return Promise.reject(error);
});

// GET 请求
const getUser = async (id) => {
  try {
    const data = await api.get(`/users/${id}`);
    console.log('用户数据:', data);
  } catch (error) {
    console.error('获取用户失败:', error);
  }
};

// POST 请求
const createUser = async (userData) => {
  try {
    const data = await api.post('/users', userData);
    return data;
  } catch (error) {
    throw error;
  }
};

2.4 底层对比:XHR vs Fetch

const comparison = {
  xhr: {
    pros: [
      '上传下载进度',
      '同步请求支持(不推荐)',
      '超时控制原生',
      '中断请求原生'
    ],
    cons: [
      '回调地狱',
      'API设计老旧',
      '需要手动封装Promise',
      '代码冗长'
    ]
  },
  
  fetch: {
    pros: [
      'Promise风格',
      'API简洁',
      'Service Worker友好',
      'Stream支持',
      '更现代化'
    ],
    cons: [
      '默认不携带Cookie',
      '不抛出HTTP错误',
      '不支持进度',
      '超时需要AbortController'
    ]
  }
};

🔄 高级请求控制

3.1 请求取消(AbortController)

1. 取消axios请求

axios提供了CancelTokencanceTokenSource来实现请求的取消。

使用CancelToken可以创建一个用于取消请求的令牌,而CancelTokenSource 可以用于一次性取消多个请求。

import axios from 'axios';

// 创建一个 CancelToken.source 实例
const { token, cancel } = axios.CancelToken.source();

取消单个请求

// 发送请求时,将 cancelToken 传递给 Axios 的 request 方法
axios.request({
    url:'https://api.example.com/data',
    method: 'get',
    cancelToken: token
}).then(response => {}).catch(error => {
    if(axios.isCancel(error)) {
        console.log("请求已取消",error.message);
    }else {
        console.error('请求出错',error);
    }
})
cancel('请求取消原因');

当调用 cancel 函数时,会导致与 token 相关联的请求被取消。在请求被取消时,Axios 会抛出一个 Cancel 错误,我们可以通过axios.isCancel(error) 方法来判断是否是取消请求导致的错误,并进行相应的处理。

一次取消多个请求

// 创建所有请求
const request1 = axios.get('/api/data1', { cancelToken: token });
const request2 = axios.get('/api/data2', { cancelToken: token });
const request3 = axios.get('/api/data3', { cancelToken: token });
// 同时发送多个请求
axios.all([request1,request2,request3])
    .then(axios.spread((response1,response2,response3) => {  
        console.log(response1.data);
        console.log(response2.data);
        console.log(response3.data);
    ))}
    .catch(error => {
        if(axios.isCancel(error)){
            console.log('请求被取消',error.message);
        }else{
            console.log('请求失败',error.message);
        }
    });

// 取消所有请求
cancel('用户取消操作');

选择性取消某个请求

const CancelToken = axios.CancelToken;
const source1 = CancelToken.source();
const source2 = CancelToken.source();
 
function clearApi1Fn() {
    // 用于取消第一个接口的请求
    source1.cancel('Operation canceled by the user.');
}
function clearApi2Fn() {
    // 用于取消第二个接口的请求
    source2.cancel('Operation canceled by the user.');
}

2. 取消fetch请求

AbortController可以终止一个或者多个fetch请求。

// 创建一个 AbortController 实例,中文名为中断控制器
const controller = new AbortController();
const signal = controller.signal;

取消单个请求

// 发送请求时,将signal传递给fetch或XMLHttpRequest
fetch('https://api.example.com/data', { signal })
.then(response => {})
.catch(error => {
    if (error.name === 'AbortError'){
        console.log("请求已取消");
    } else {
        console.error( '请求出错',error);
    }
})
// 取消请求
controller.abort();

当调用 controller.abort() 方法时,会导致与 signal 相关联的请求被取消。在请求被取消时,fetch 或 XMLHttpRequest会抛出一个 AbortError 错误,我们可以在 catch 中捕获这个错误并进行相应的处理。

通过使用 AbortController 和 Abortsignal,我们可以在浏览器中比较方便地取消已经发送的请求,避免不必要的网络流量和资源浪费。

一次取消多个请求

// 发送多个请求
Promise.all([
    fetch(url1, { signal }), 
    fetch(url2, { signal }),
    fetch(url1, { signal })
]).then((data) => { 
    const [result1,result2,result3] = data;
}).catch(error => {
    ... ...
})

// 取消请求
controller.abort();

搜索框防抖+取消


// 实际应用:搜索框防抖+取消
class SearchWithCancel {
  constructor() {
    this.request = new CancellableRequest();
    this.currentId = null;
    this.debounceTimer = null;
  }
  
  search(query) {
    // 取消前一个请求
    if (this.currentId) {
      this.request.cancelRequest(this.currentId);
    }
    
    // 防抖
    clearTimeout(this.debounceTimer);
    this.debounceTimer = setTimeout(async () => {
      const id = Symbol('search');
      this.currentId = id;
      
      try {
        const result = await this.request.fetchWithCancel(
          `/api/search?q=${encodeURIComponent(query)}`,
          { id }
        );
        
        if (result) {
          const data = await result.json();
          this.updateUI(data);
        }
      } catch (error) {
        console.error('搜索失败:', error);
      }
    }, 300);
  }
  
  updateUI(data) {
    // 更新界面
  }
}

3.2 并发请求控制

并发请求:在同一时间内发出多个请求

并发请求可以针对不同的资源或者服务,其目的是为了提高文件传输效率或者请求效率。并发是计算机领域中的一个重要概念,它指的是在同一时间内处理多个任务或者请求的能力。这种能力是现代计算机系统设计的核心,它影响着系统的性能、可伸缩性和稳定性。

HTTP 0.9、HTTP 1.0 每个请求都单独建立一个 TCP 连接,请求完成后连接断开; HTTP 1.1 可以持久连接,TCP 建立连接后不会立即关闭,多个请求可以复用同一个TCP 连接,而且多个请求可以并行发送。

浏览器对于并发请求的规则如下所示(同一域名下生效):

  • 相同的 GET 请求的并发数是1,上一个请求结束,才会执行下一个请求,否则置入队列等待发送
  • 不同的 GET/POST 请求的并发数量是6,当发送的请求数量达到6个,并且都没有得到响应时,后面的请求会置入队列等待发送

HTTP 1.1 持久链接和HTTP 2.0 多路复用的区别:

  • HTTP 1.1 持久链接:多个请求共用一个 TCP 链接,但是同时只能发送一个请求
  • HTTP 2.0 多路复用:在一个 TCP 链接中,可以同时发送多个请求
function sendRequest(requestList, limits, callback) {
    // 任务队列
    const promises = [];
    // 当前的并发池
    const pool = new Set([]);
    
    const runTask = async () => {
        // for await of 循环执行并发池
        for (let requestItem of requestList) {
            if (pool.size >= limits) {
                await Promise.race(pool).catch((err) => err);
            }
            const promise = requestItem();
            const cb = () => {
                pool.delete(promise);
            };
            promise.then(cb, cb);
            pool.add(promise);
            promises.push(promise);
        }
        // 通过allSettled获取异步任务的执行结果
        Promise.allSettled(promises).then(callback,callback);
    };
    runTask();
}
  • 减少变量记录当前并发执行的请求数量
  • 利用Set数据结构避免重复触发同一个请求
  • 通过Promise.race将请求池数量降到限制以下
  • 通过.then中的回调函数完成任务的清除
  • 通过Promise.allSettled获取所有的异步结果

3.3 请求队列与优先级

// 优先级请求队列
class PriorityRequestQueue {
  constructor(maxConcurrent = 5) {
    this.maxConcurrent = maxConcurrent;
    this.active = new Set();
    this.queues = {
      high: [],    // 高优先级
      normal: [],  // 普通优先级
      low: []      // 低优先级
    };
  }
  
  // 添加请求(支持优先级)
  add(requestFn, priority = 'normal') {
    return new Promise((resolve, reject) => {
      this.queues[priority].push({
        requestFn,
        resolve,
        reject
      });
      
      this.schedule();
    });
  }
  
  // 调度
  schedule() {
    if (this.active.size >= this.maxConcurrent) return;
    
    // 按优先级取任务
    const nextTask = this.getNextTask();
    if (!nextTask) return;
    
    this.execute(nextTask);
  }
  
  getNextTask() {
    // 高优先级优先
    if (this.queues.high.length > 0) {
      return this.queues.high.shift();
    }
    
    // 普通优先级
    if (this.queues.normal.length > 0) {
      return this.queues.normal.shift();
    }
    
    // 低优先级
    if (this.queues.low.length > 0) {
      return this.queues.low.shift();
    }
    
    return null;
  }
  
  async execute(task) {
    const id = Symbol('task');
    this.active.add(id);
    
    try {
      const result = await task.requestFn();
      task.resolve(result);
    } catch (error) {
      task.reject(error);
    } finally {
      this.active.delete(id);
      this.schedule();
    }
  }
  
  // 提升优先级
  promote(taskId) {
    // 实现在不同队列间移动任务
  }
}

// 使用
const queue = new PriorityRequestQueue();

// 高优先级(用户正在查看的内容)
queue.add(() => fetch('/api/current-page'), 'high');

// 普通优先级(列表数据)
queue.add(() => fetch('/api/list'), 'normal');

// 低优先级(预加载)
queue.add(() => fetch('/api/next-page'), 'low');

3.4 请求重试

/**
 * 最简单的重试请求
 * @param {Function} requestFn 请求函数(返回Promise)
 * @param {number} retries 重试次数
 * @returns {Promise}
 */
async function retryRequest(requestFn, retries = 3) {
  try {
    // 尝试执行请求
    const result = await requestFn();
    return result; // 成功则返回结果
  } catch (error) {
    // 如果还有重试次数
    if (retries > 0) {
      console.log(`请求失败,还剩 ${retries} 次重试`);
      // 递归调用,重试次数减1
      return retryRequest(requestFn, retries - 1);
    }
    // 重试次数用完,抛出错误
    throw error;
  }
}

// 使用示例
async function demo() {
  // 模拟一个可能失败的请求
  let attempt = 0;
  const fetchData = async () => {
    attempt++;
    console.log(`第 ${attempt} 次尝试`);
    
    // 模拟随机失败
    if (Math.random() < 0.7) {
      throw new Error('网络错误');
    }
    return '数据获取成功';
  };
  
  try {
    const result = await retryRequest(fetchData, 3);
    console.log('成功:', result);
  } catch (error) {
    console.log('最终失败:', error.message);
  }
}

demo();

🚨 常见坑点与解决方案

4.1 请求缓存问题

// 坑1:GET请求缓存
// ❌ 第二次请求可能命中缓存,拿不到最新数据
fetch('/api/user/123');

// ✅ 解决方案1:加时间戳
fetch('/api/user/123?t=' + Date.now());

// ✅ 解决方案2:设置请求头
fetch('/api/user/123', {
  headers: {
    'Cache-Control': 'no-cache',
    'Pragma': 'no-cache'
  }
});

// ✅ 解决方案3:使用POST(但不符合RESTful)

// 坑2:浏览器预检请求缓存
// OPTIONS请求每次都会发,增加开销
// ✅ 设置Access-Control-Max-Age
Access-Control-Max-Age: 86400 // 缓存24小时

4.2 Cookie 携带问题

// 坑:fetch默认不携带Cookie
fetch('/api/data'); // 不会带Cookie

// ✅ 解决方案
fetch('/api/data', {
  credentials: 'include' // 携带Cookie(跨域也带)
  // credentials: 'same-origin' // 同源才带
});

// 坑:跨域Cookie需要额外配置
// 服务器必须设置
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://example.com // 不能是*

4.3 超时控制

// 坑:fetch原生不支持超时
// ❌ 这样不行
fetch('/api/slow', { timeout: 5000 });

// ✅ 解决方案1:Promise.race
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  return Promise.race([
    fetch(url, options),
    new Promise((_, reject) => 
      setTimeout(() => reject(new Error('请求超时')), timeout)
    )
  ]);
}

// ✅ 解决方案2:AbortController + setTimeout
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  return fetch(url, {
    ...options,
    signal: controller.signal
  }).finally(() => clearTimeout(timeoutId));
}

4.4 内存泄漏

// 坑:大量请求不释放
class LeakyCache {
  constructor() {
    this.cache = new Map(); // 无限增长
  }
  
  fetch(url) {
    if (this.cache.has(url)) {
      return this.cache.get(url);
    }
    
    const promise = fetch(url).then(r => r.json());
    this.cache.set(url, promise);
    return promise;
  }
}

// ✅ 解决方案:限制缓存大小
class BoundedCache {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
  }
  
  fetch(url) {
    if (this.cache.has(url)) {
      // 更新LRU
      const value = this.cache.get(url);
      this.cache.delete(url);
      this.cache.set(url, value);
      return value;
    }
    
    const promise = fetch(url).then(r => r.json());
    
    // 超过限制时删除最早的
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(url, promise);
    return promise;
  }
}

4.5 请求顺序问题

// 坑:请求返回顺序不确定
async function loadUser() {
  const user = await fetch('/api/user/123');
  const posts = await fetch('/api/posts'); // 等待用户完成
  
  // 这样太慢
}

// ✅ 解决方案:并行请求
async function loadUserParallel() {
  const [user, posts] = await Promise.all([
    fetch('/api/user/123'),
    fetch('/api/posts')
  ]);
}

// 坑:竞态条件
let requestId = 0;

async function search(query) {
  const id = ++requestId;
  const result = await fetch(`/api/search?q=${query}`);
  
  // 如果后发的请求先返回,会覆盖正确结果
  if (id === requestId) {
    updateUI(result);
  }
}

// ✅ 解决方案:取消过期请求
class SearchManager {
  constructor() {
    this.currentController = null;
  }
  
  async search(query) {
    // 取消前一个
    if (this.currentController) {
      this.currentController.abort();
    }
    
    this.currentController = new AbortController();
    
    try {
      const response = await fetch(`/api/search?q=${query}`, {
        signal: this.currentController.signal
      });
      
      const data = await response.json();
      updateUI(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('搜索失败:', error);
      }
    }
  }
}

🎯 难点

Q1:浏览器最多可以同时发起多少个请求?

const browserLimits = {
  // 同域名并发限制
  'HTTP/1.1': 6, // Chrome最多6个
  
  // HTTP/2 多路复用,无此限制
  'HTTP/2': '无限制(单连接)',
  
  // 总连接数限制
  '总连接': 'Chrome最大256个'
};

// 突破限制的方法
const solutions = [
  '1. 使用HTTP/2',
  '2. 域名分片(多个子域名)',
  '3. 连接池复用'
];

Q2:Preflight请求是什么?什么时候触发?

// 触发条件(满足任一)
const preflightTriggers = {
  // 1. 非简单方法
  methods: ['PUT', 'DELETE', 'PATCH', 'CONNECT', 'OPTIONS', 'TRACE'],
  
  // 2. 特殊请求头
  headers: [
    'Content-Type': ['application/json', 'multipart/form-data'],
    'Custom-Header': '任意自定义头'
  ],
  
  // 3. 带凭证的请求
  credentials: 'include'
};

// 避免不必要的preflight
const optimization = {
  // 使用简单请求
  method: 'GET/POST',
  'Content-Type': 'application/x-www-form-urlencoded',
  
  // 或设置预检缓存
  'Access-Control-Max-Age': 86400
};

Q3:如何实现请求的防抖和节流?

// 请求防抖(合并连续请求)
function debounceRequest(fn, delay = 300) {
  let timer = null;
  let controller = null;
  
  return function(...args) {
    // 取消之前的请求
    if (controller) {
      controller.abort();
    }
    
    // 清除定时器
    clearTimeout(timer);
    
    // 创建新的控制器
    controller = new AbortController();
    
    return new Promise((resolve, reject) => {
      timer = setTimeout(async () => {
        try {
          const result = await fn(...args, { signal: controller.signal });
          resolve(result);
        } catch (error) {
          reject(error);
        }
      }, delay);
    });
  };
}

// 请求节流(限制频率)
function throttleRequest(fn, limit = 1000) {
  let lastCall = 0;
  let pendingPromise = null;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastCall >= limit) {
      // 可以发送新请求
      lastCall = now;
      pendingPromise = fn(...args);
      return pendingPromise;
    } else {
      // 返回上次的pending请求
      return pendingPromise;
    }
  };
}

Q4:大文件上传如何实现?

class LargeFileUploader {
  constructor(file, options = {}) {
    this.file = file;
    this.chunkSize = options.chunkSize || 1024 * 1024; // 1MB
    this.threads = options.threads || 3; // 并发数
    this.retries = options.retries || 3;
    
    this.chunks = Math.ceil(file.size / this.chunkSize);
    this.progress = new Array(this.chunks).fill(0);
    this.abortController = new AbortController();
  }
  
  async upload() {
    const pool = new RequestPool(this.threads);
    
    // 生成所有分片任务
    for (let i = 0; i < this.chunks; i++) {
      const start = i * this.chunkSize;
      const end = Math.min(start + this.chunkSize, file.size);
      const chunk = file.slice(start, end);
      
      pool.add(i, async ({ signal }) => {
        const formData = new FormData();
        formData.append('chunk', chunk);
        formData.append('index', i);
        formData.append('total', this.chunks);
        formData.append('filename', file.name);
        
        // 上传分片
        for (let attempt = 1; attempt <= this.retries; attempt++) {
          try {
            const response = await fetch('/api/upload', {
              method: 'POST',
              body: formData,
              signal
            });
            
            this.progress[i] = 100;
            return response;
          } catch (error) {
            if (attempt === this.retries) throw error;
            await new Promise(r => setTimeout(r, 1000 * attempt));
          }
        }
      });
    }
    
    // 监听进度
    pool.onProgress = (p) => {
      const total = this.progress.reduce((a, b) => a + b, 0) / this.chunks;
      this.onProgress?.(total);
    };
    
    // 等待所有完成
    await pool.waitForAll();
    
    // 通知合并
    await fetch('/api/upload/merge', {
      method: 'POST',
      body: JSON.stringify({
        filename: file.name,
        chunks: this.chunks
      })
    });
  }
  
  abort() {
    this.abortController.abort();
  }
}

Q5:如何监控请求性能?

class RequestMonitor {
  constructor() {
    this.metrics = {
      total: 0,
      success: 0,
      failed: 0,
      totalTime: 0,
      slowRequests: []
    };
    
    this.initObserver();
  }
  
  initObserver() {
    // 监控资源加载
    new PerformanceObserver((list) => {
      list.getEntries().forEach(entry => {
        if (entry.initiatorType === 'fetch' || entry.initiatorType === 'xmlhttprequest') {
          this.analyzeRequest(entry);
        }
      });
    }).observe({ entryTypes: ['resource'] });
  }
  
  analyzeRequest(entry) {
    const duration = entry.duration;
    const url = entry.name;
    
    this.metrics.total++;
    this.metrics.totalTime += duration;
    
    // 慢请求检测
    if (duration > 1000) {
      this.metrics.slowRequests.push({
        url,
        duration,
        timestamp: Date.now(),
        timing: {
          dns: entry.domainLookupEnd - entry.domainLookupStart,
          tcp: entry.connectEnd - entry.connectStart,
          ttfb: entry.responseStart - entry.requestStart,
          download: entry.responseEnd - entry.responseStart
        }
      });
    }
    
    // 上报
    this.report();
  }
  
  report() {
    if (this.metrics.total % 10 === 0) {
      console.log('请求统计:', {
        平均耗时: (this.metrics.totalTime / this.metrics.total).toFixed(2) + 'ms',
        慢请求数: this.metrics.slowRequests.length
      });
    }
  }
}