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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

请求取消,往往使用在当网络较慢,用户在等待当前请求时转而去点了其他请求,且这两个请求是会相互影响时,或者关闭当前标签页,我们会将当前请求取消。例如远程模糊搜索、搜索建议、路由切换。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>好的,我现在需要解决用户的问题:如何处理或避免多个POST /userinfo接口在同一项目中具有相同路径的问题。首先,我得理解用户的具体情况。用户可能在开发一个Node.js后端项目,使用了Express框架,并且在不同的路由模块中定义了相同的路由路径,导致冲突。比如可能在user.js和另一个模块中都定义了POST /userinfo,这样当应用启动时,Express无法区分这两个路由,导致其中一个覆盖或者出现不可预料的行为。 接下来,我要回顾一下Express的路由机制。Express中,路由是按照定义的顺序进行匹配的,一旦找到第一个匹配的路由,就会执行对应的处理函数。因此,如果有两个相同的路由定义,后面的那个会覆盖前面的,或者可能因为中间件顺序问题导致冲突。 用户的引用内容提到了用户路由模块的初始化,以及JWT和Cors的使用,但问题核心在于路由路径重复。根据引用[1],用户已经将路由模块化,比如在user.js中创建了路由对象,并导出,然后在app.js中通过app.use('/api', userRouter)挂载。这意味着所有在user.js中定义的路由都会带有/api前缀。例如,/reguser实际上会成为/api/reguser。 但用户的问题是关于多个POST /userinfo接口冲突。可能的情况是,用户在不同的路由模块中定义了相同的路径,比如在user.js和另一个profile.js中都定义了POST /userinfo,并且这两个模块都被挂载到了同一个基础路径下,比如/api。这样,当两个模块都被挂载到/api时,它们的路由路径就会变成/api/userinfo,导致冲突。 解决方法有几个方向: 1. **检查路由模块的挂载路径**:确保每个路由模块挂载到不同的基础路径下。例如,用户模块挂载到/api/user,个人资料模块挂载到/api/profile,这样即使它们内部有/userinfo的路由,实际路径也会不同。 2. **统一管理路由路径**:在项目中进行路由路径的统一规划,避免重复。例如,使用不同的HTTP方法或路径来区分不同的操作,比如POST /userinfo/create和PUT /userinfo/update。 3. **中间件区分**:通过中间件来根据请求的其他参数(如headers、查询参数等)进行路由,但这种方法不太推荐,因为不符合RESTful设计原则。 4. **代码审查和模块化**:在项目中建立规范,确保每个路由模块负责特定的功能区域,避免不同模块处理相同路径。例如,用户相关的路由集中在user.js,而个人资料相关的在profile.js,且路径设计不同。 具体来说,根据引用[1]中的例子,用户路由模块被挂载到/api路径下。如果在另一个模块中也定义了/userinfo,并且被挂载到同一路径下,就会冲突。所以应该调整挂载的基础路径,或者调整各个模块内部的路由路径。 例如,在app.js中,如果用户路由模块挂载到/api/user,另一个模块挂载到/api/profile,那么各自内部的/userinfo路径就会变成/api/user/userinfo和/api/profile/userinfo,从而避免冲突。 另外,检查是否有重复定义的路由处理程序。可能在同一个路由模块中不小心定义了两个相同的POST /userinfo,这时需要合并或删除其中一个。 总结步骤可能包括: - 检查所有路由模块的挂载点,确保基础路径不重叠。 - 统一规划路由路径,遵循RESTful设计,使用不同的HTTP方法或路径参数区分操作。 - 使用版本控制,如/api/v1/userinfo和/api/v2/userinfo,但这可能不是当前问题的最佳解决方案。 - 代码审查,确保没有重复的路由定义。 同时,根据引用[3]中提到的同一账号只能在一台设备登录的问题,可能需要考虑路由的安全性,但当前问题更侧重于路径冲突,所以暂时不需要处理JWT或会话管理。 最后,用户可能需要具体的代码示例来展示如何正确挂载不同模块以避免路径冲突,比如在app.js中如何分别挂载用户模块和个人资料模块到不同的基础路径。</think>在处理多个接口路径冲突问题时,可以通过以下步骤实现路由路径的规范管理: ### 一、问题分析 当多个路由模块中出现相同路径定义(如两个`POST /userinfo`)时,Express会按照代码加载顺序**仅执行第一个匹配的路由处理函数**,导致接口功能异常或覆盖。例如: ```javascript // 模块A中定义 router.post('/userinfo', handleA) // 模块B中定义 router.post('/userinfo', handleB) ``` ### 二、解决方案 #### 1. 路由分层设计(推荐) 通过**基础路径区分模块**,在挂载路由时添加命名空间: ```javascript // app.js 中分别挂载不同模块 app.use('/api/user', userRouter) // 最终路径: /api/user/userinfo app.use('/api/article', articleRouter) // 最终路径: /api/article/userinfo ``` #### 2. 功能拆分细化 采用RESTful风格规范路径设计: ```javascript // 用户信息获取 router.get('/userinfo', getUserInfo) // 用户信息更新 router.put('/userinfo', updateUserInfo) ``` #### 3. 版本控制机制 为API接口添加版本标识: ```javascript // v1版本接口 app.use('/api/v1/user', userRouterV1) // v2版本接口 app.use('/api/v2/user', userRouterV2) ``` ### 三、错误示例修正 假设存在用户模块和文章模块都需要处理用户信息: ```javascript // ❌ 错误写法(路径冲突) // user.js router.post('/userinfo', userHandler) // article.js router.post('/userinfo', articleHandler) // ✅ 正确写法(通过基础路径隔离) // app.js app.use('/user', userRouter) app.use('/article', articleRouter) // user.js router.post('/info', userHandler) // 完整路径: /user/info // article.js router.post('/userinfo', articleHandler) // 完整路径: /article/userinfo ``` ### 四、路由调试技巧 可通过`router.stack`查看已注册路由: ```javascript console.log(router.stack.map(layer => layer.route.path)) // 输出: [ '/info', '/userinfo' ] ``` ### 五、最佳实践建议 1. 建立`routes/`目录统一存放路由模块 2. 使用Swagger等工具生成API文档 3. 结合中间件进行路由级权限控制[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值