重复请求,前面的覆盖后面的结果(最佳实践)

本文详细解析了axios中请求取消的实现方式,包括通过CancelToken.source()和构造函数CancelToken来取消请求,并展示了如何在实际场景中应用,如处理多个并发请求。此外,还介绍了封装方法以管理和取消多个请求,确保请求管理的有序性。

请求取消,往往使用在当网络较慢,用户在等待当前请求时转而去点了其他请求,且这两个请求是会相互影响时,或者关闭当前标签页,我们会将当前请求取消。例如远程模糊搜索、搜索建议、路由切换。axios提供了请求取消的接口,在github上官方给出了两种实现请求取消的用法

简单处理
  • 方法一
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {

  //  主动取消的请求会抛出错误,需要主动处理
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

解析

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();
我们取第一个例子来解析每一步,首先第一步,将axios中的CancelToken赋值给一个变量CancelToken,我们看看axios中的CancelToken

function CancelToken(executor) {
  // 检测传入的是否为函数
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  // 使用promise异步
  // 在后面调用方法时就会触发promise的resolve
  // 在xhr.js中调用abort方法取消请求
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // 已经发起过取消请求的情况下,会直接return
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}
/**
 * 当请求已经被取消时,抛出Cancel对象
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * 返回一个包含新的CancelToken的对象,以及一个用来取消请求的方法
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  // 这里的token实际上就是新的CancelToken对象
  // cancel是用来取消请求的方法
  return {
    token: token,
    cancel: cancel
  };
};


这里还不太清楚内部的结构没关系,我们先往下走,这里先知道我们创建了一个CancelToken即可。
看看下一行代码

const source = CancelToken.source();
这里调用了CancelToken的source方法,将返回的值赋值给source,从代码中的source方法

/**
 * 返回一个包含新的CancelToken的对象,以及一个用来取消请求的方法
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  // 这里的token实际上就是新的CancelToken对象
  // cancel是用来取消请求的方法
  return {
    token: token,
    cancel: cancel
  };
};

我们可以明确看到,变量source是一个包含新的CancelToken和取消对应token的方法的对象。将executor作为new CancelToken的参数,当我们执行这里的方法c,即返回对象中的cancel时,就会触发下面一系列操作

if (typeof executor !== 'function') {
  throw new TypeError('executor must be a function.');
}

进入CancelToken后,首先判断传入的是不是一个函数,不是的话会抛出错误,这里是一个函数,继续往下走,新建了一个Promise赋值给这个CancelToken对象的promise属性,将resolve赋值给resolvePromise使得我们可以在外部控制promise的状态

var resolvePromise;
// 使用promise异步
// 在后面调用方法时就会触发promise的resolve
// 在xhr.js中调用abort方法取消请求
this.promise = new Promise(function promiseExecutor(resolve) {
  resolvePromise = resolve;
});

在这里,只要我们调用了resolvePromise方法,就等于将this.promise的状态变为resolve,接下来将this赋值给token,将一个cancel方法作为参数传给刚刚传给new CancelToken的参数的方法

var token = this;
executor(function cancel(message) {
  // 已经发起过取消请求的情况下,会直接return
  // 在下一步我们就将请求时会出项错误的信息赋值给token.reason,说明已经发起过这个请求了
  if (token.reason) {
    return;
  }

  token.reason = new Cancel(message);
  resolvePromise(token.reason);
});

当token.reason存在时,即已经执行过这个取消请求的操作了,如果执行过,直接return,如果没执行过,我们将Cancel对象作为token.reason的值,然后将其作为参数执行resolvePromise方法,将promise变为resolve的状态。

Cancel对象实际上就是在请求被取消时会抛出来的错误信息,看看源代码

'use strict';
/**
 * A `Cancel` is an object that is thrown when an operation is canceled.
 *
 * @class
 * @param {string=} message The message.
 */
// 当请求已经被取消的时候再取消会抛出这个Cancel对象作为错误信息
function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

知道这些后,接下来通过一个请求,我们来将请求时取消请求的具体流程弄清楚

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

在这段代码中,我们使用axios发起了一个get请求,然后将source.token即上面我们调用source方法创建的对象中新建的那个CancelToken传入当前请求的cancelToken属性中,这里我们看看xhr.js中的相关内容

if (config.cancelToken) {
  // 对取消请求的操作
  // 当promise变为resolve时,执行then里的方法
  config.cancelToken.promise.then(function onCanceled(cancel) {
    // 如果请求已经没了,即请求已经结束
    // 那么直接返回
    if (!request) {
      return;
    }
    // 取消请求
    request.abort();
    reject(cancel);
    // 清除请求
    request = null;
  });
}

  • 方法二
通过axios.CancelToken构造函数生成取消函数
const CancelToken = axios.CancelToken;
let cancel;
​
axios.get('/user/12345', {
 cancelToken: new CancelToken(function executor(c) {
 // 执行函数接收取消函数作为参数
 cancel = c;
  })
});
​
// cancel the request
cancel();
封装,可以同时处理多个请求
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
const pending = new Map()
/**
 * 添加请求
 * @param {Object} config 
 */
const addPending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  config.cancelToken = config.cancelToken || new axios.CancelToken(cancel => {
    if (!pending.has(url)) { // 如果 pending 中不存在当前请求,则添加进去
      pending.set(url, cancel)
    }
  })
}
/**
 * 移除请求
 * @param {Object} config 
 */
const removePending = (config) => {
  const url = [
    config.method,
    config.url,
    qs.stringify(config.params),
    qs.stringify(config.data)
  ].join('&')
  if (pending.has(url)) { // 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
    const cancel = pending.get(url)
    cancel(url)
    pending.delete(url)
  }
}
/**
 * 清空 pending 中的请求(在路由跳转时调用)
 */
export const clearPending = () => {
  for (const [url, cancel] of pending) {
    cancel(url)
  }
  pending.clear()
}


// 主要的方法已经写好了,只需要添加到axios拦截器中就可以了。
axios.interceptors.request.use(config => {
  removePending(options) // 在请求开始前,对之前的请求做检查取消操作
  addPending(options) // 将当前请求添加到 pending 中
  // other code before request
  return config
}, error => {
  return Promise.reject(error)
})
​
axios.interceptors.response.use(response => {
  removePending(response) // 在请求结束后,移除本次请求
  return response
}, error => {
  if (axios.isCancel(error)) {
    console.log('repeated request: ' + error.message)
  } else {
    // handle error code
  }
  return Promise.reject(error)
})
// 将clearPending()方法添加到vue路由钩子函数中
router.beforeEach((to, from, next) => {
  clearPending()
  // ...
  next()
})

注:qs是一个专门用来转换对象和字符串参数的库,最初是由 TJ 创建并维护的,也是axios推荐使用的参数序列化库。这里我们的目的只是单纯的将参数对象转换为字符串方便拼接。

<think>我们有两个需求: 1.请求拦截器中设置默认请求头(全局设置) 2. 在单个请求覆盖或添加自定义请求头(局部设置) 根据引用[2]的内容,我们知道axios提供了请求拦截器(request interceptor)和响应拦截器(response interceptor)。我们可以通过请求拦截器来设置默认请求头。 步骤: 1. 创建axios实例(可选,但推荐,避免污染全局配置) 2. 使用请求拦截器设置默认请求头 3. 在单个请求中,通过配置对象覆盖或添加请求头 注意:在单个请求中设置的headers会与拦截器中设置的headers合并,如果同名,则单个请求中的配置会覆盖拦截器中的配置。 具体实现: 一、创建axios实例并设置请求拦截器(设置默认请求头) 二、在组件中使用该实例发起请求,并在请求配置中传入自定义headers 示例代码: 1. 创建axios实例并设置拦截器(如:在src/utils/request.js中) ```javascript import axios from 'axios'; // 创建axios实例 const instance = axios.create({ baseURL: 'https://api.example.com', // 设置基础URL }); // 请求拦截器 instance.interceptors.request.use( (config) => { // 在发送请求之前做些什么 // 设置默认请求头 config.headers = { ...config.headers, // 保留已有的headers 'Authorization': 'Bearer default_token', // 默认的认证头 'Content-Type': 'application/json', // 默认内容类型 }; return config; }, (error) => { // 对请求错误做些什么 return Promise.reject(error); } ); export default instance; ``` 2. 在React组件中发起请求,并添加自定义请求头(覆盖默认头或新增头) ```javascript import React, { useEffect } from 'react'; import axiosInstance from './utils/request'; // 导入上面创建的实例 function MyComponent() { useEffect(() => { // 发起GET请求,并添加自定义请求头 axiosInstance.get('/user', { headers: { 'Authorization': 'Bearer custom_token', // 覆盖默认的Authorization头 'X-Custom-Header': 'foobar' // 新增自定义头 } }) .then(response => { console.log(response); }) .catch(error => { console.error(error); }); // 发起POST请求,同样可以添加自定义头 axiosInstance.post('/submit', { data: 'some data' }, { headers: { 'X-Requested-With': 'XMLHttpRequest' } }); }, []); return <div>Check the console for request details.</div>; } export default MyComponent; ``` 说明: - 在拦截器中,我们使用展开运算符`...config.headers`来保留调用时传入的headers,然后添加默认头。注意,如果传入的headers中有同名的字段,后面的会覆盖前面的。但是,由于我们是在拦截器里先保留传入的headers,然后再添加默认头,所以如果传入的headers中有同名字段,那么默认头就会覆盖传入的?这显然不是我们想要的。 修正:实际上,拦截器中的config对象已经包含了我们在请求调用时传入的配置(包括headers)。所以,我们应该先设置默认头,然后用传入的headers覆盖默认头?这样也不行,因为传入的headers可能只有部分字段。 正确做法:我们应该是将默认头和传入头合并,且传入头的优先级更高。所以我们可以这样做: 在拦截器中: ```javascript config.headers = { ...{ 'Authorization': 'Bearer default_token', 'Content-Type': 'application/json' }, ...config.headers // 传入的headers覆盖默认头 }; ``` 但是,注意:如果传入的headers中某个字段为undefined,那么它也会覆盖默认头,导致该字段不存在。所以更健壮的做法是使用深合并,但通常headers是浅层对象,所以这样合并已经足够。 另外,在axios中,其实默认头已经可以通过`defaults`设置,但这里我们使用拦截器来实现,以便在请求前动态设置(比如从store中获取token)。 另一种做法:在创建实例时设置默认头: ```javascript const instance = axios.create({ baseURL: 'https://api.example.com', headers: { 'Authorization': 'Bearer default_token', 'Content-Type': 'application/json' } }); ``` 然后在拦截器中,我们可能还需要动态更新(比如token过期后刷新),所以拦截器里设置更灵活。 因此,我们可以在拦截器中这样写(动态获取token): ```javascript instance.interceptors.request.use((config) => { // 假设我们从redux store或者localStorage中获取token const token = localStorage.getItem('token') || 'default_token'; // 合并headers config.headers = { ...config.headers, 'Authorization': `Bearer ${token}`, // 注意:如果调用时已经传入了Content-Type,那么这里就不覆盖了 // 所以如果调用时需要不同的Content-Type(如multipart/form-data),可以在调用时传入 }; // 如果已经存在Content-Type,我们就不设置默认的,否则设置 if (!config.headers['Content-Type']) { config.headers['Content-Type'] = 'application/json'; } return config; }); ``` 但是,这样写可能有点复杂。更简单的方式是:我们只设置那些肯定需要的头,并且允许调用时覆盖。 总结: 1. 在拦截器中设置默认请求头,注意合并策略(传入的headers优先级更高)。 2. 在单个请求中,通过配置对象的headers属性添加或覆盖请求头。 根据引用[1]和引用[2],我们了解到拦截器的使用,以及如何通过axios实例进行配置。 另外,引用[3]提到了取消请求,但本问题不涉及,所以暂不讨论。 因此,按照上述示例即可实现需求。 相关问题: 1. 如何在 Axios 拦截器中实现全局请求头管理? 2. React 中如何安全地存储用于请求头的认证令牌? 3. 使用 Axios 时如何处理跨域请求(CORS)问题? 4. 相比 `fetch API`,Axios 在 React 中有哪些核心优势?</think>在 React 中使用 Axios 时,可以通过请求拦截器设置默认请求头,并在单个请求覆盖或添加自定义请求头。以下是具体实现方案: ### 方案实现 #### 1. 创建 Axios 实例并设置请求拦截器 ```javascript // src/utils/axios.js import axios from 'axios'; // 创建 Axios 实例 const instance = axios.create({ baseURL: 'https://api.example.com', }); // 请求拦截器 - 设置默认请求头 instance.interceptors.request.use( (config) => { // 设置默认请求头 config.headers = { ...config.headers, // 保留已有 headers 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}`, 'X-Request-Source': 'web-app' }; return config; }, (error) => { return Promise.reject(error); } ); export default instance; ``` #### 2. 在组件中使用时覆盖/添加请求头 ```javascript import React, { useEffect } from 'react'; import axiosInstance from './utils/axios'; function UserProfile() { useEffect(() => { // 单个请求覆盖默认头 axiosInstance.get('/user', { headers: { 'Authorization': 'Bearer custom_token', // 覆盖默认 Authorization 'Cache-Control': 'no-cache' // 添加新头 } }); // POST 请求示例 axiosInstance.post('/submit', { data: 'test' }, { headers: { 'Content-Type': 'multipart/form-data' // 覆盖默认 Content-Type } }); }, []); return <div>用户信息加载中...</div>; } ``` ### 关键机制说明 1. **优先级规则** 单个请求的 `headers` 配置会**覆盖**拦截器中的同名头: ```javascript // 拦截器设置: Authorization: Bearer default_token // 请求设置: Authorization: Bearer custom_token // 实际发送: Authorization: Bearer custom_token ``` 2. **合并策略** 使用展开运算符 `...` 确保: - 保留拦截器设置的默认头 - 保留请求中设置的自定义头 - 同名头以请求配置为准 3. **动态值处理** 拦截器支持动态值生成: ```javascript // 动态获取 token instance.interceptors.request.use(config => { const token = localStorage.getItem('newToken'); config.headers.Authorization = `Bearer ${token}`; return config; }); ``` ### 对比方案 | **场景** | 请求拦截器 | 单个请求配置 | |------------------|-------------------------------------|----------------------------------| | **认证令牌** | ✅ 统一管理 | ⚠️ 需每个请求单独设置 | | **内容类型** | ✅ 设置默认类型 | ✅ 可针对特定请求覆盖 | | **自定义头** | ⚠️ 所有请求都会携带 | ✅ 按需添加 | | **动态值更新** | ✅ 拦截器内自动更新 | ❌ 需手动维护 | ### 最佳实践建议 1. **敏感信息处理** 认证令牌应存储在 `HttpOnly Cookie` 或 `Secure LocalStorage` 中,拦截器自动附加[^1] 2. **内容类型覆盖** 当上传文件时,在特定请求覆盖为 `multipart/form-data`: ```javascript axiosInstance.post('/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); ``` 3. **取消重复请求** 结合 AbortController 避免重复请求: ```javascript const controller = new AbortController(); axiosInstance.get('/data', { signal: controller.signal, headers: { 'X-Request-ID': uuid() } }); // 需要时取消 controller.abort(); ``` > 提示:对于需要完全自定义头的特殊请求,可考虑直接使用 `axios` 而非实例,但会失去拦截器功能[^2]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值