request.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. /**
  2. * @Description: request 封装(支持流式请求,修复onComplete兼容性问题)
  3. * @Version: 1.0.4
  4. * @author:
  5. * @Date: 2025-08-25
  6. */
  7. import { requestConfig } from '../app.config.js'
  8. import api from '@/api/index.js'
  9. // import { TextDecoder } from 'text-encoding';
  10. class Request {
  11. constructor() {
  12. this.isRefreshing = false; // 是否正在刷新token的标记
  13. this.subscribers = []; // 缓存等待token刷新的请求
  14. this.tokenKey = 'token';
  15. this.refreshTokenKey = 'refresh_token';
  16. // 绑定方法确保this上下文
  17. this.http = this.http.bind(this);
  18. this.streamHttp = this.streamHttp.bind(this);
  19. this.refreshToken = this.refreshToken.bind(this);
  20. this.buildRequestConfig = this.buildRequestConfig.bind(this);
  21. this.handleError = this.handleError.bind(this);
  22. }
  23. // 获取token
  24. getToken() {
  25. return uni.getStorageSync(this.tokenKey);
  26. }
  27. // 获取刷新token
  28. getRefreshToken() {
  29. return uni.getStorageSync(this.refreshTokenKey);
  30. }
  31. // 保存token
  32. saveToken(token) {
  33. uni.setStorageSync(this.tokenKey, token);
  34. }
  35. // 保存刷新token
  36. saveRefreshToken(refreshToken) {
  37. uni.setStorageSync(this.refreshTokenKey, refreshToken);
  38. }
  39. // 清除token
  40. clearToken() {
  41. uni.removeStorageSync(this.tokenKey);
  42. uni.removeStorageSync(this.refreshTokenKey);
  43. }
  44. // 触发用户信息刷新
  45. triggerUserInfoRefresh() {
  46. uni.$emit("userinfoRefresh", true);
  47. }
  48. // 添加等待token刷新的请求
  49. addSubscriber(callback) {
  50. this.subscribers.push(callback);
  51. }
  52. // 执行等待中的请求
  53. resolveSubscribers() {
  54. this.subscribers.forEach(callback => callback());
  55. this.subscribers = [];
  56. }
  57. // 无感刷新token
  58. async refreshToken() {
  59. if (this.isRefreshing) {
  60. // 如果正在刷新,返回一个Promise等待刷新完成
  61. return new Promise(resolve => {
  62. this.addSubscriber(() => resolve(this.http(...arguments)));
  63. });
  64. }
  65. this.isRefreshing = true;
  66. try {
  67. const refreshToken = this.getRefreshToken();
  68. if (!refreshToken) {
  69. throw new Error('刷新令牌不存在');
  70. }
  71. const res = await api.login({
  72. grant_type: 'refresh',
  73. refresh_token: refreshToken,
  74. client_type: 0,
  75. });
  76. if (res.code === 200) {
  77. this.saveToken(`Bearer ${res.data.access_token}`);
  78. this.saveRefreshToken(res.data.refresh_token);
  79. this.triggerUserInfoRefresh();
  80. this.resolveSubscribers();
  81. return true;
  82. } else {
  83. throw new Error(res.msg || '令牌刷新失败');
  84. }
  85. } catch (error) {
  86. console.error('令牌刷新错误:', error);
  87. this.clearToken();
  88. uni.redirectTo({
  89. url:
  90. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  91. });
  92. return false;
  93. } finally {
  94. // 延迟释放刷新标记,避免并发请求问题
  95. setTimeout(() => {
  96. this.isRefreshing = false;
  97. }, 1000);
  98. }
  99. }
  100. // 处理请求错误
  101. handleError(error, param, isStream = false) {
  102. if (error.code === 501) {
  103. // 令牌过期,尝试刷新
  104. return this.refreshToken().then(refreshed => {
  105. if (refreshed) {
  106. // 刷新成功后重试请求,区分普通和流式请求
  107. return isStream ? this.streamHttp(param) : this.http(param);
  108. }
  109. throw error; // 刷新失败,抛出错误
  110. });
  111. }
  112. throw error; // 其他错误直接抛出
  113. }
  114. // 构建请求配置
  115. buildRequestConfig(param) {
  116. const {
  117. url,
  118. method = 'GET',
  119. data,
  120. params,
  121. header = {},
  122. responseType = 'text',
  123. type,
  124. enableChunked,
  125. timeout
  126. } = param;
  127. // 合并请求头
  128. const headers = {
  129. 'Content-Type': 'application/x-www-form-urlencoded',
  130. ...header
  131. };
  132. console.log(headers);
  133. // 添加授权信息
  134. const token = this.getToken();
  135. // if (token && !headers['Authorization']) {
  136. // headers['Authorization'] = 'Bearer ' + token;
  137. // }
  138. // 流式请求需要特殊处理
  139. if (param.isStream) {
  140. headers['Accept'] = 'text/event-stream';
  141. }
  142. console.log(requestConfig);
  143. // 构建请求URL
  144. const BaseUrl = requestConfig.BaseUrl;
  145. const requestUrl = BaseUrl + url;
  146. return {
  147. url: requestUrl,
  148. data: data || params,
  149. method,
  150. dataType: 'json',
  151. responseType,
  152. header: headers,
  153. enableChunked,
  154. timeout: timeout,
  155. };
  156. }
  157. // 主请求方法(普通请求)
  158. http(param) {
  159. return new Promise((resolve, reject) => {
  160. try {
  161. const requestConfig = this.buildRequestConfig(param);
  162. // 从参数中获取是否显示错误提示的标志
  163. const showToast = param.showToast !== undefined ? param.showToast : true;
  164. let isAborted = false;
  165. // 创建请求任务
  166. const requestTask = uni.request({
  167. ...requestConfig,
  168. success: (res) => {
  169. if (isAborted) return;
  170. // HTTP状态码检查
  171. if (res.statusCode !== 200) {
  172. const error = new Error(
  173. `API错误: ${res.data.error || res.statusCode}`);
  174. error.response = res;
  175. if (showToast) uni.$u.toast(error.message);
  176. reject(error);
  177. return;
  178. }
  179. // 业务逻辑错误处理
  180. if (res.data.code !== 200) {
  181. if (res.data.code === 501) {
  182. // 令牌过期,处理刷新
  183. // this.handleError({ code: 501 }, param)
  184. // .then(refreshedRes => resolve(refreshedRes))
  185. // .catch(err => reject(err));
  186. uni.reLaunch({
  187. url:
  188. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  189. });
  190. localStorage.clear();
  191. } else if (res.data.code === 400 && res.data.msg === '登录凭证不为空') {
  192. const error = new Error(res.data.msg);
  193. error.response = res;
  194. // if (showToast) {
  195. uni.reLaunch({
  196. url:
  197. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  198. });
  199. localStorage.clear();
  200. // }
  201. reject(error);
  202. } else {
  203. const error = new Error(res.data.msg || '请求失败');
  204. if (showToast) uni.$u.toast(error.message);
  205. reject(res);
  206. }
  207. return;
  208. }
  209. // 正常响应
  210. resolve(res.data);
  211. },
  212. fail: (err) => {
  213. if (isAborted) return;
  214. const error = new Error('网络异常, 请检查网络连接');
  215. error.originalError = err;
  216. if (showToast) uni.$u.toast(error.message);
  217. reject(error);
  218. }
  219. });
  220. // 终止请求的方法
  221. const abort = () => {
  222. isAborted = true;
  223. if (requestTask.abort) {
  224. console.log('中断请求成功!');
  225. requestTask.abort();
  226. }
  227. if (typeof param.onAbort === 'function') {
  228. param.onAbort();
  229. }
  230. };
  231. // 暴露终止方法
  232. if (typeof param.onInit === 'function') {
  233. param.onInit(abort);
  234. }
  235. } catch (error) {
  236. // 统一错误处理
  237. console.error('请求处理错误:', error);
  238. error.isRequestError = true;
  239. reject(error);
  240. }
  241. });
  242. }
  243. // 流式请求方法
  244. streamHttp(param) {
  245. return new Promise((resolve, reject) => {
  246. // 标记为流式请求
  247. param.isStream = true;
  248. const requestConfig = this.buildRequestConfig(param);
  249. const showToast = param.showToast !== undefined ? param.showToast : true;
  250. let isAborted = false;
  251. let complete = false;
  252. let lastChunkTime = 0; // 记录最后一次接收数据的时间
  253. // 设置超时检测,无数据传输时判断为结束
  254. const timeoutInterval = 500000; // 5分钟无数据则视为结束
  255. const checkTimeout = () => {
  256. if (complete || isAborted) return;
  257. const now = Date.now();
  258. if (lastChunkTime > 0 && now - lastChunkTime > timeoutInterval) {
  259. // 超时,视为请求完成
  260. finish();
  261. } else {
  262. // 继续检测
  263. setTimeout(checkTimeout, 1000);
  264. }
  265. };
  266. // 创建请求任务
  267. const requestTask = uni.request({
  268. ...requestConfig,
  269. // 流式请求需要设置响应类型为arraybuffer
  270. responseType: 'arraybuffer',
  271. success: (res) => {
  272. if (res.statusCode !== 200) {
  273. const error = new Error(`API错误: ${res.statusCode}`);
  274. error.response = res;
  275. if (showToast) uni.$u.toast(error.message);
  276. reject(error);
  277. return;
  278. }
  279. },
  280. fail: (err) => {
  281. if (isAborted) return;
  282. const error = new Error('网络异常, 请检查网络连接');
  283. error.originalError = err;
  284. if (showToast) uni.$u.toast(error.message);
  285. reject(error);
  286. }
  287. });
  288. // 处理流式数据
  289. // const decoder = new TextDecoder('utf-8');
  290. // 检查请求任务是否支持onChunkReceived方法
  291. if (typeof requestTask.onChunkReceived !== 'function') {
  292. reject(new Error('当前环境不支持流式请求'));
  293. return;
  294. }
  295. // 监听数据块
  296. requestTask.onChunkReceived((response) => {
  297. if (isAborted || complete) return;
  298. // 更新最后接收数据的时间
  299. lastChunkTime = Date.now();
  300. // 处理token过期情况
  301. if (response.statusCode === 401 || (response.data && response.data.code === 501)) {
  302. this.handleError({ code: 501 }, param, true)
  303. .then(() => {
  304. // 刷新成功后会重新发起请求,这里终止当前流
  305. abort();
  306. })
  307. .catch(err => {
  308. if (!complete) {
  309. complete = true;
  310. reject(err);
  311. }
  312. });
  313. return;
  314. }
  315. // 解码二进制数据
  316. const chunk = this.decodeUtf8(response.data)
  317. // const chunk = decoder.decode(response.data, { stream: true });
  318. // 检查是否是结束标记(根据实际后端约定调整)
  319. if (chunk.includes('[DONE]') || chunk.includes('</stream>') || chunk.includes('result')) {
  320. // 调用流式回调处理最后一块数据
  321. if (typeof param.onChunk === 'function') {
  322. param.onChunk(chunk.replace(/\[DONE\]|<\/stream>/g, ''), response);
  323. }
  324. finish();
  325. return;
  326. }
  327. // 调用流式回调处理数据
  328. if (typeof param.onChunk === 'function') {
  329. param.onChunk(chunk, response);
  330. }
  331. });
  332. // 监听请求头接收
  333. if (typeof requestTask.onHeadersReceived === 'function') {
  334. requestTask.onHeadersReceived((response) => {
  335. if (response.statusCode === 200 && typeof param.onOpen === 'function') {
  336. param.onOpen(response);
  337. // 收到响应头后开始超时检测
  338. lastChunkTime = Date.now();
  339. setTimeout(checkTimeout, 1000);
  340. }
  341. });
  342. } else {
  343. // 如果不支持onHeadersReceived,直接开始超时检测
  344. setTimeout(checkTimeout, 1000);
  345. }
  346. // 终止请求的方法
  347. const abort = () => {
  348. isAborted = true;
  349. if (requestTask.abort) {
  350. requestTask.abort();
  351. }
  352. if (typeof param.onAbort === 'function') {
  353. param.onAbort();
  354. }
  355. };
  356. // 完成回调
  357. const finish = () => {
  358. if (complete) return;
  359. complete = true;
  360. if (typeof param.onComplete === 'function') {
  361. param.onComplete();
  362. }
  363. resolve();
  364. };
  365. // 暴露终止方法
  366. if (typeof param.onInit === 'function') {
  367. param.onInit(abort);
  368. }
  369. });
  370. }
  371. // 新增:手动实现 UTF-8 解码(替换 TextDecoder)
  372. decodeUtf8(arrayBuffer) {
  373. const uint8Array = new Uint8Array(arrayBuffer);
  374. let str = '';
  375. let i = 0;
  376. const len = uint8Array.length;
  377. while (i < len) {
  378. const byte = uint8Array[i];
  379. let codePoint;
  380. // 单字节字符(0-127)
  381. if (byte < 0x80) {
  382. codePoint = byte;
  383. i += 1;
  384. }
  385. // 双字节字符(128-2047)
  386. else if (byte >= 0xC0 && byte < 0xE0) {
  387. codePoint = ((byte & 0x1F) << 6) | (uint8Array[i + 1] & 0x3F);
  388. i += 2;
  389. }
  390. // 三字节字符(2048-65535)
  391. else if (byte >= 0xE0 && byte < 0xF0) {
  392. codePoint = ((byte & 0x0F) << 12) | ((uint8Array[i + 1] & 0x3F) << 6) | (uint8Array[i + 2] & 0x3F);
  393. i += 3;
  394. }
  395. // 四字节字符(Unicode 扩展平面)
  396. else if (byte >= 0xF0 && byte < 0xF8) {
  397. codePoint = ((byte & 0x07) << 18) | ((uint8Array[i + 1] & 0x3F) << 12) | ((uint8Array[i + 2] & 0x3F) << 6) | (uint8Array[i + 3] & 0x3F);
  398. i += 4;
  399. }
  400. // 无效字节(跳过)
  401. else {
  402. i += 1;
  403. continue;
  404. }
  405. // 处理四字节字符的代理对
  406. if (codePoint <= 0xFFFF) {
  407. str += String.fromCharCode(codePoint);
  408. } else {
  409. codePoint -= 0x10000;
  410. const high = (codePoint >> 10) & 0x3FF;
  411. const low = codePoint & 0x3FF;
  412. str += String.fromCharCode(0xD800 + high, 0xDC00 + low);
  413. }
  414. }
  415. return str;
  416. }
  417. }
  418. export default new Request();