前言
在 uni-app 项目开发过程中,我们经常会遇到一些影响体验和开发效率的共性问题,比如页面跳转时残留请求导致的数据错乱、重复登录拦截逻辑分散、重复请求浪费资源等。这些问题如果处理不当,会严重影响应用的稳定性和可维护性。
本文将介绍一套针对这些痛点的高可用解决方案,该方案已通过线上项目验证,支持微信小程序 / APP/H5 多端运行,旨在帮助开发者更高效地管理请求和路由,提升项目质量。
一、开发中常见的痛点问题
在 uni-app 项目中,你是否遇到过这些问题?
- 🔥 页面跳转时残留请求导致数据错乱:页面跳转后,前页面未完成的请求仍在继续,可能导致后续数据处理异常。
- 🔐 重复登录拦截逻辑散落在各个页面:每个页面都需要判断登录状态,代码冗余且难以统一维护。
- 🔄 重复请求浪费资源且难以管理:同一接口短时间内被多次调用,增加服务器压力,也可能导致数据冲突。
二、解决方案核心功能亮点
针对上述问题,封装的解决方案具备以下核心功能:
1️⃣ 智能请求管理
- 自动生成唯一请求标识,拦截重复请求(取消前序相同请求),避免资源浪费。
- 路由跳转时自动取消所有 pending 请求,防止内存泄漏和数据错乱。
- 支持按requestId精准取消单个请求,灵活控制请求生命周期。
2️⃣ 路由拦截增强
- 重写navigateTo/switchTab等导航方法,统一拦截登录态,减少重复代码。
- 白名单机制 + 自动跳转登录页 + 回跳原页面,优化登录体验。
- 内置防抖逻辑,禁止 Tab 页重复点击,避免无效操作。
3️⃣ 精细化错误处理
- 401 未登录自动跳转(排除首次启动场景),统一处理登录失效问题。
- 统一 Toast 错误提示,实现业务码逻辑解耦,便于维护。
- 清晰区分请求取消、业务错误、网络异常等不同错误类型,精准处理。
三、适用场景
该解决方案适用于以下场景:
- ✅ 需要严格管理请求生命周期的中大型项目:中大型项目请求量大、页面多,请求生命周期管理尤为重要。
- ✅ 多页面需登录鉴权的电商 / 社交类应用:这类应用对登录状态依赖高,统一的登录拦截能提升开发效率。
- ✅ 追求代码高复用性与可维护性的团队:方案通过封装减少重复代码,提升项目可维护性。
四、核心模块介绍
1. 路由拦截器:一键搞定导航全场景
路由拦截器接管所有导航行为(navigateTo/switchTab等),自动处理以下问题:
- 白名单页面直接放行,非白名单页面无 token 时,自动跳登录页并记住回跳地址,登录后可直接返回原页面。
- 切换路由时自动取消 pending 请求,避免接口 “打架” 导致的数据问题。
- 提供带加载动画的导航方法,统一管理加载状态,无需在各页面重复编写showLoading逻辑。
使用方式简单,在main.js中引入即可生效:
// main.js引入路由拦截器
import './utils/interceptor';
import http from "./request";
/**
* 路由拦截与跳转封装工具
* 基于uni-app导航API二次封装,支持登录校验、权限控制等全局拦截
*/
const RouterInterceptor = {
// 白名单路由(无需登录)
whiteList: ['/pages_b/mine/login'],
/**
* 初始化路由拦截
*/
init() {
// 保存原始导航方法
const originalNavigateTo = uni.navigateTo;
const originalRedirectTo = uni.redirectTo;
const originalReLaunch = uni.reLaunch;
const originalSwitchTab = uni.switchTab;
const originalNavigateBack = uni.navigateBack; // 新增:保存原始navigateBack方法
// 重写导航方法
uni.navigateTo = this.wrapNavigate('navigateTo', originalNavigateTo);
uni.redirectTo = this.wrapNavigate('redirectTo', originalRedirectTo);
uni.reLaunch = this.wrapNavigate('reLaunch', originalReLaunch);
uni.switchTab = this.wrapNavigate('switchTab', originalSwitchTab);
uni.navigateBack = this.wrapNavigate('navigateBack', originalNavigateBack); // 新增:重写navigateBack方法
},
/**
* 导航方法包装器
* @param {string} method - 导航方法名(navigateTo/redirectTo/reLaunch/switchTab/navigateBack)
* @param {Function} original - 原始导航方法
* @returns {Function} 包装后的导航方法
*/
wrapNavigate(method, original) {
// 使用箭头函数确保this指向RouterInterceptor
return async (options) => {
http.cancelRequest();
// navigateBack特殊处理 (无url参数)
if (method === 'navigateBack') {
return original(options);
}
const { url } = options;
const purePath = url.split('?')[0];
// 避免对登录页本身进行拦截检查,防止无限循环
if (purePath === '/pages_b/mine/login') {
return original(options);
}
// 获取当前页面路径
const currentPages = getCurrentPages();
const currentRoute = currentPages.length ? currentPages[currentPages.length - 1].route : '';
// 路由拦截逻辑
if (!this.checkPermission(url)) {
// 未登录跳转登录页
return uni.navigateTo({
url: `/pages_b/mine/login?redirect=${encodeURIComponent(url)}`
});
}
// 处理tabBar页面特殊逻辑
if (method === 'switchTab' && url === `/${currentRoute}`) {
// 防止重复点击tabBar
return;
}
// 执行原始导航方法
return original(options);
};
},
/**
* 权限检查
* @param {string} url - 目标页面路径
* @returns {boolean} 是否有权限访问
*/
checkPermission(url) {
// 提取纯净路径(去除参数)
const purePath = url.split('?')[0];
// 白名单路由直接放行
if (this.whiteList.includes(purePath)) {
return true;
}
// 检查是否登录
const token = uni.getStorageSync('token');
return !!token;
},
/**
* 封装带加载动画的导航
* @param {Object} options - 导航参数
* @param {string} options.url - 目标页面路径
* @param {boolean} [options.showLoading=true] - 是否显示加载动画
* @param {string} [options.loadingText='加载中...'] - 加载动画文本
*/
async navigateWithLoading(options) {
const { url, showLoading = true, loadingText = '加载中...' } = options;
if (showLoading) {
uni.showLoading({
title: loadingText,
mask: true
});
}
try {
await uni.navigateTo({
url
});
} catch (e) {
console.error('导航失败:', e);
} finally {
if (showLoading) {
uni.hideLoading();
}
}
}
};
// 初始化路由拦截
RouterInterceptor.init();
export default RouterInterceptor;
2. 请求封装:网络层稳如老狗
基于 luch-request 强化的请求封装,解决请求层 90% 的问题:
- 请求头自动携带 token,适配 H5 / 小程序 / App 多端 header 差异。
- 重复请求自动拦截,同一接口即使被多次触发,也只会发送一次请求。
- 401/500 等错误全局拦截,登录过期自动跳登录页,无需在每个接口中单独判断。
import Request from '../sdk/luch-request/luch-request/index.js';
const http = new Request();
/**
* 请求任务管理Map,存储请求ID与任务对象的映射
*/
const requestTasks = new Map();
/**
* 请求标识管理Map,存储请求唯一标识与请求ID的映射,用于检测重复请求
*/
const requestKeys = new Map();
/**
* 请求ID计数器
*/
let requestId = 0;
/**
* 生成请求的唯一标识
* @param {Object} config - 请求配置对象
* @param {string} config.url - 请求URL
* @param {string} [config.method='GET'] - 请求方法
* @param {Object} [config.params={}] - URL查询参数
* @param {Object} [config.data={}] - 请求体数据
* @returns {string} 请求唯一标识字符串
*/
function generateRequestKey(config) {
const { url, method = 'GET' } = config;
// 处理参数,确保顺序一致
const params = config.params || {};
const data = config.data || {};
// 将参数转换为有序字符串
const paramsStr = Object.keys(params).sort().map(key => `${key}=${params[key]}`).join('&');
const dataStr = Object.keys(data).sort().map(key => `${key}=${data[key]}`).join('&');
return `${method.toUpperCase()}:${url}?${paramsStr}#${dataStr}`;
}
/**
* 修改全局默认配置
* @param {Function} configCallback - 配置回调函数
* @returns {Object} 修改后的配置对象
*/
http.setConfig((config) => {
/* config 为默认全局配置*/
config.baseURL = import.meta.env.VITE_APP_BASE_API + '/api'; /* 根域名 */
config.header = {
'Content-Type': 'application/json;charset=utf-8',
// #ifdef MP-WEIXIN
'x-platform': 'wechat',
'x-app-id': import.meta.env.VITE_APP_WECHAT_ID,
// #endif
// #ifdef APP-PLUS
'x-platform': 'app',
'x-app-id': import.meta.env.VITE_APP_APP_ID,
// #endif
};
// 添加getTask回调以获取请求任务对象
config.getTask = (task, options) => {
if (options.requestId) {
// 存储任务对象而非配置
requestTasks.set(options.requestId, task);
}
};
return config;
});
/**
* 请求拦截器
* @param {Object} config - 请求配置对象
* @returns {Object} 处理后的请求配置对象
*/
http.interceptors.request.use(
(config) => {
// 生成请求唯一标识
const requestKey = generateRequestKey(config);
// 检查是否存在重复请求
if (requestKeys.has(requestKey)) {
// 取消之前的重复请求
const previousRequestId = requestKeys.get(requestKey);
http.cancelRequest(previousRequestId, '重复请求,已取消');
}
// 生成唯一请求ID
const currentRequestId = requestId++;
config.requestId = currentRequestId;
// 存储请求标识与requestId的映射
requestKeys.set(requestKey, currentRequestId);
const token = uni.getStorageSync('token')
if (token) {
config.header.authorization = 'Bearer ' + token
}
return config;
},
(config) => {
// 可使用async await 做异步操作
return Promise.reject(config);
},
);
/**
* 响应拦截器
* @param {Object} response - 响应对象
* @returns {Promise<any>} 处理后的响应数据
*/
http.interceptors.response.use(
async (response) => {
// 请求完成后移除请求任务和请求标识
if (response.config?.requestId) {
console.log('请求完成:', response.config.requestId);
requestTasks.delete(response.config.requestId);
// 生成请求标识并从映射中删除
const requestKey = generateRequestKey(response.config);
if (requestKeys.get(requestKey) === response.config.requestId) {
requestKeys.delete(requestKey);
}
}
const res = response.data
// 业务成功处理
if (res.code === 0) return res.data
uni.showToast({
title: res.message || '请求失败',
icon: 'none',
mask: true
})
return Promise.reject(new Error(res.message || 'Error'))
},
async (error) => {
// 请求失败后移除请求任务和请求标识
if (error.config?.requestId) {
requestTasks.delete(error.config.requestId);
// 生成请求标识并从映射中删除
const requestKey = generateRequestKey(error.config);
if (requestKeys.get(requestKey) === error.config.requestId) {
requestKeys.delete(requestKey);
}
}
// 处理取消请求的错误
if (error && error.errMsg === "request:fail abort") {
console.log('请求已取消:', error.message || error.errMsg);
return Promise.reject(new Error('请求已取消'));
}
const isFirstLaunch = uni.getStorageSync('isFirstLaunch');
if (!isFirstLaunch && error.statusCode === 401) {
uni.navigateTo({
url: '/pages_b/mine/login'
})
}
uni.showToast({
title: error?.data?.message || '请求失败',
icon: 'none'
})
return Promise.reject(error);
},
);
/**
* 取消请求的方法
* @param {number} [requestId] - 可选,请求ID,如果提供则取消特定请求,否则取消所有请求
* @param {string} [message='切换路由,已取消'] - 可选,取消请求的原因
*/
http.cancelRequest = function (requestId, message = '切换路由,已取消') {
if (requestId !== undefined) {
// 取消特定请求
const task = requestTasks.get(requestId);
if (task) {
// 使用任务对象的abort方法取消请求
task.abort();
requestTasks.delete(requestId);
console.log(`取消请求: ${requestId}, 原因: ${message}`);
}
} else {
// 取消所有请求
requestTasks.forEach((task, id) => {
task.abort();
console.log(`取消请求: ${id}, 原因: ${message}`);
});
requestTasks.clear();
requestKeys.clear(); // 清空请求标识映射
}
};
export default http;
五、使用示例
路由跳转示例
// 直接使用重写后的导航方法,自动触发拦截逻辑
uni.navigateTo({ url: '/pages/index/detail' });
请求调用示例
// 引入封装的请求工具
import http from '../utils/request';
// 定义接口调用方法
export const fetchData = (params) => {
return http.request({
url: '/api/data/list',
method: 'GET',
params
});
};
总结
本文介绍的 uni-app 请求与路由管理解决方案,通过智能请求管理、路由拦截增强和精细化错误处理,有效解决了开发中常见的请求混乱、登录拦截分散、重复请求等痛点问题。
该方案不仅提升了应用的稳定性和用户体验,还通过代码封装减少了重复开发工作,提高了团队的开发效率。方案支持多端运行且已通过线上验证,适合在中大型 uni-app 项目中推广使用。