request.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. /**
  2. * uni-app 网络请求封装
  3. * 特性:请求/响应拦截、统一错误处理、超时控制、请求取消、Token 自动携带、环境切换
  4. * 适配端:小程序、H5、App
  5. */
  6. // ======================== 基础配置 ========================
  7. // 环境配置(根据 NODE_ENV 切换 baseURL)
  8. const env = process.env.NODE_ENV || 'development';
  9. const baseURLMap = {
  10. development: 'http://192.168.110.235:1860', // 开发环境
  11. production: '', // 生产环境
  12. test: 'http://192.168.110.235:1860' // 测试环境
  13. };
  14. // 默认基础请求地址
  15. const DEFAULT_BASE_URL = baseURLMap[env];
  16. const DEFAULT_BASE_URL_IMG = 'http://47.107.107.47:9000/zksy-file/';
  17. // 默认请求配置
  18. const DEFAULT_CONFIG = {
  19. baseURL: DEFAULT_BASE_URL,
  20. timeout: 10000, // 全局默认超时时间(10秒)
  21. header: {
  22. 'Content-Type': 'application/json;charset=UTF-8' // 默认请求头
  23. },
  24. showLoading: true, // 是否显示加载中提示(全局默认开启)
  25. loadingText: '加载中...', // 加载提示文字
  26. withToken: true, // 是否自动携带 Token(全局默认开启)
  27. cancelRepeat: true // 是否取消重复请求(全局默认开启)
  28. };
  29. // 存储请求取消令牌(key: 请求唯一标识,value: 取消函数)
  30. const cancelTokenMap = new Map();
  31. // ======================== 工具函数 ========================
  32. /**
  33. * 生成请求唯一标识
  34. * @param {String} method - 请求方法(GET/POST 等)
  35. * @param {String} url - 请求地址
  36. * @param {Object} data - 请求参数
  37. * @returns {String} 请求唯一标识
  38. */
  39. function generateRequestKey(method, url, data) {
  40. return `${method.toUpperCase()}-${url}-${JSON.stringify(data)}`;
  41. }
  42. /**
  43. * 统一提示方法(兼容不同端的提示样式)
  44. * @param {String} msg - 提示文字
  45. * @param {String} type - 提示类型(none/success/error)
  46. */
  47. function showToast(msg, type = 'none') {
  48. uni.showToast({
  49. title: msg,
  50. icon: type,
  51. duration: 2000,
  52. mask: true // 显示蒙层,防止穿透点击
  53. });
  54. }
  55. // ======================== 核心拦截逻辑 ========================
  56. /**
  57. * 请求拦截器:处理请求前的统一逻辑
  58. * @param {Object} header - 原始请求头
  59. * @param {Boolean} withToken - 是否携带 Token
  60. * @returns {Object} 处理后的请求头
  61. */
  62. function requestInterceptor(header, withToken) {
  63. const finalHeader = { ...header };
  64. // 自动携带 Token(从本地缓存读取)
  65. if (withToken) {
  66. const token = uni.getStorageSync('token');
  67. if (token) {
  68. finalHeader.Authorization = `Bearer ${token}`; // 适配 JWT Token 格式
  69. }
  70. }
  71. return finalHeader;
  72. }
  73. /**
  74. * 响应拦截器:处理响应后的统一逻辑
  75. * @param {Object} response - 响应对象
  76. * @returns {Promise} 处理后的结果
  77. */
  78. function responseInterceptor(response) {
  79. const { statusCode, data } = response;
  80. // 1. 处理 HTTP 状态码错误
  81. if (statusCode !== 200) {
  82. let errMsg = '';
  83. switch (statusCode) {
  84. case 401:
  85. errMsg = '登录失效,请重新登录';
  86. // 清除 Token 并跳转到登录页(排除登录页本身)
  87. uni.removeStorageSync('token');
  88. const pages = getCurrentPages();
  89. const currentPage = pages[pages.length - 1];
  90. if (currentPage.route !== 'pages/login/login') {
  91. uni.redirectTo({ url: '/pages/login/login' });
  92. }
  93. break;
  94. case 403:
  95. errMsg = '权限不足,无法访问';
  96. break;
  97. case 404:
  98. errMsg = '请求地址不存在';
  99. break;
  100. case 500:
  101. errMsg = '服务器内部错误,请稍后重试';
  102. break;
  103. default:
  104. errMsg = `请求失败(状态码:${statusCode})`;
  105. }
  106. showToast(errMsg, 'none');
  107. return Promise.reject({
  108. code: statusCode,
  109. msg: errMsg,
  110. raw: response
  111. });
  112. }
  113. // 2. 处理业务状态码(根据后端约定调整)
  114. // 假设后端返回格式:{ code: 0, msg: '成功', data: {} }
  115. // const { code, msg, data: resData } = data;
  116. const { code, msg, token } = data; // 直接解析 token
  117. if (code === 200) {
  118. // 业务请求成功,返回核心数据
  119. return Promise.resolve({ token, ...data }); // 确保返回有值
  120. } else {
  121. // 业务请求失败,提示错误信息
  122. showToast(msg || '操作失败', 'none');
  123. return Promise.reject({
  124. code,
  125. msg: msg || '操作失败',
  126. raw: data
  127. });
  128. }
  129. }
  130. /**
  131. * 网络错误处理
  132. * @param {Object} error - 错误对象
  133. * @returns {Promise} 拒绝的 Promise
  134. */
  135. function networkErrorHandler(error) {
  136. let errMsg = '网络请求失败';
  137. switch (error.errMsg) {
  138. case 'request:fail timeout':
  139. errMsg = '请求超时,请检查网络';
  140. break;
  141. case 'request:fail abort':
  142. errMsg = '请求已取消';
  143. break;
  144. case 'request:fail network error':
  145. errMsg = '网络异常,请检查网络连接';
  146. break;
  147. default:
  148. errMsg = `请求错误:${error.errMsg}`;
  149. }
  150. showToast(errMsg, 'none');
  151. return Promise.reject({
  152. code: -1,
  153. msg: errMsg,
  154. raw: error
  155. });
  156. }
  157. // ======================== 核心请求方法 ========================
  158. /**
  159. * 创建请求实例
  160. * @param {Object} customConfig - 自定义全局配置(覆盖默认配置)
  161. * @returns {Function} 请求方法
  162. */
  163. function createRequest(customConfig = {}) {
  164. // 合并全局配置
  165. const globalConfig = { ...DEFAULT_CONFIG, ...customConfig };
  166. /**
  167. * 核心请求函数
  168. * @param {Object} options - 单个请求配置
  169. * @returns {Promise} 请求结果
  170. */
  171. const request = async (options) => {
  172. const {
  173. url,
  174. method = 'GET',
  175. data = {},
  176. header = {},
  177. showLoading = globalConfig.showLoading,
  178. loadingText = globalConfig.loadingText,
  179. timeout = globalConfig.timeout,
  180. withToken = globalConfig.withToken,
  181. cancelRepeat = globalConfig.cancelRepeat
  182. } = options;
  183. // 1. 验证必填参数
  184. if (!url) {
  185. showToast('请求地址不能为空', 'none');
  186. return Promise.reject({ code: -2, msg: '请求地址不能为空' });
  187. }
  188. // 2. 显示加载中提示
  189. let loadingTimer = null;
  190. if (showLoading) {
  191. // 延迟显示(避免请求过快导致提示闪烁)
  192. loadingTimer = setTimeout(() => {
  193. uni.showLoading({ title: loadingText, mask: true });
  194. }, 200);
  195. }
  196. // 3. 处理重复请求
  197. const requestKey = generateRequestKey(method, url, data);
  198. if (cancelRepeat && cancelTokenMap.has(requestKey)) {
  199. // 取消已存在的重复请求
  200. cancelTokenMap.get(requestKey)('取消重复请求');
  201. cancelTokenMap.delete(requestKey);
  202. }
  203. // 4. 创建取消令牌(用于取消请求)
  204. let cancelRequest;
  205. const cancelToken = new Promise((resolve) => {
  206. cancelRequest = resolve;
  207. cancelTokenMap.set(requestKey, resolve);
  208. });
  209. try {
  210. // 5. 合并请求头(全局头 + 单个请求头 + 拦截器处理)
  211. const finalHeader = requestInterceptor({ ...globalConfig.header, ...header }, withToken);
  212. // 6. 发起请求(Promise 化 uni.request)
  213. const requestPromise = new Promise((resolve, reject) => {
  214. uni.request({
  215. url: globalConfig.baseURL + url, // 拼接完整请求地址
  216. method,
  217. data,
  218. header: finalHeader,
  219. timeout,
  220. success: resolve,
  221. fail: reject
  222. });
  223. });
  224. // 7. 同时监听请求和取消令牌(race: 谁先完成就执行谁)
  225. const result = await Promise.race([requestPromise, cancelToken]);
  226. // 8. 移除当前请求的取消令牌
  227. cancelTokenMap.delete(requestKey);
  228. // 9. 关闭加载中提示
  229. clearTimeout(loadingTimer);
  230. if (showLoading) uni.hideLoading();
  231. // 10. 处理响应结果
  232. return responseInterceptor(result);
  233. } catch (error) {
  234. // 11. 异常处理
  235. cancelTokenMap.delete(requestKey);
  236. clearTimeout(loadingTimer);
  237. if (showLoading) uni.hideLoading();
  238. // 区分是网络错误还是取消请求
  239. if (typeof error === 'string') {
  240. // 取消请求的错误(主动取消)
  241. return Promise.reject({ code: -3, msg: error });
  242. } else {
  243. // 网络错误(超时、断网等)
  244. return networkErrorHandler(error);
  245. }
  246. }
  247. };
  248. // ======================== 快捷请求方法 ========================
  249. /**
  250. * GET 请求快捷方法
  251. * @param {String} url - 请求地址
  252. * @param {Object} data - 请求参数
  253. * @param {Object} options - 自定义配置
  254. * @returns {Promise} 请求结果
  255. */
  256. request.get = (url, data = {}, options = {}) => {
  257. return request({ url, method: 'GET', data, ...options });
  258. };
  259. /**
  260. * POST 请求快捷方法
  261. * @param {String} url - 请求地址
  262. * @param {Object} data - 请求参数
  263. * @param {Object} options - 自定义配置
  264. * @returns {Promise} 请求结果
  265. */
  266. request.post = (url, data = {}, options = {}) => {
  267. return request({ url, method: 'POST', data, ...options });
  268. };
  269. /**
  270. * PUT 请求快捷方法
  271. * @param {String} url - 请求地址
  272. * @param {Object} data - 请求参数
  273. * @param {Object} options - 自定义配置
  274. * @returns {Promise} 请求结果
  275. */
  276. request.put = (url, data = {}, options = {}) => {
  277. return request({ url, method: 'PUT', data, ...options });
  278. };
  279. /**
  280. * DELETE 请求快捷方法
  281. * @param {String} url - 请求地址
  282. * @param {Object} data - 请求参数
  283. * @param {Object} options - 自定义配置
  284. * @returns {Promise} 请求结果
  285. */
  286. request.delete = (url, data = {}, options = {}) => {
  287. return request({ url, method: 'DELETE', data, ...options });
  288. };
  289. // ======================== 辅助方法 ========================
  290. /**
  291. * 取消所有未完成的请求
  292. * @param {String} reason - 取消原因
  293. */
  294. request.cancelAll = (reason = '取消所有请求') => {
  295. cancelTokenMap.forEach((cancel) => cancel(reason));
  296. cancelTokenMap.clear();
  297. };
  298. /**
  299. * 获取当前全局配置
  300. * @returns {Object} 全局配置
  301. */
  302. request.getConfig = () => {
  303. return { ...globalConfig };
  304. };
  305. /**
  306. * 更新全局配置
  307. * @param {Object} newConfig - 新的全局配置
  308. */
  309. request.updateConfig = (newConfig) => {
  310. Object.assign(globalConfig, newConfig);
  311. };
  312. return request;
  313. }
  314. // 创建默认请求实例(全局使用)
  315. const request = createRequest();
  316. export const baseURL = DEFAULT_BASE_URL;
  317. export const imgBaseURL = DEFAULT_BASE_URL_IMG;
  318. export { baseURL as DEFAULT_BASE_URL };
  319. export { imgBaseURL as DEFAULT_BASE_URL_IMG };
  320. // 导出请求实例和创建方法
  321. export default request;
  322. export { createRequest };