request.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  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. localStorage.clear();
  187. uni.reLaunch({
  188. url:
  189. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  190. });
  191. localStorage.clear();
  192. } else if (res.data.code === 400 && res.data.msg === '登录凭证不为空') {
  193. const error = new Error(res.data.msg);
  194. error.response = res;
  195. localStorage.clear();
  196. // if (showToast) {
  197. uni.reLaunch({
  198. url:
  199. "/pages/index/index?shopId=" + uni.getStorageSync('shopId'),
  200. });
  201. // }
  202. reject(error);
  203. } else {
  204. const error = new Error(res.data.msg || '请求失败');
  205. if (showToast) uni.$u.toast(error.message);
  206. reject(res);
  207. }
  208. return;
  209. }
  210. // 正常响应
  211. resolve(res.data);
  212. },
  213. fail: (err) => {
  214. if (isAborted) return;
  215. const error = new Error('网络异常, 请检查网络连接');
  216. error.originalError = err;
  217. if (showToast) uni.$u.toast(error.message);
  218. reject(error);
  219. }
  220. });
  221. // 终止请求的方法
  222. const abort = () => {
  223. isAborted = true;
  224. if (requestTask.abort) {
  225. console.log('中断请求成功!');
  226. requestTask.abort();
  227. }
  228. if (typeof param.onAbort === 'function') {
  229. param.onAbort();
  230. }
  231. };
  232. // 暴露终止方法
  233. if (typeof param.onInit === 'function') {
  234. param.onInit(abort);
  235. }
  236. } catch (error) {
  237. // 统一错误处理
  238. console.error('请求处理错误:', error);
  239. error.isRequestError = true;
  240. reject(error);
  241. }
  242. });
  243. }
  244. // 流式请求方法
  245. streamHttp(param) {
  246. return new Promise((resolve, reject) => {
  247. // 标记为流式请求
  248. param.isStream = true;
  249. const requestConfig = this.buildRequestConfig(param);
  250. const showToast = param.showToast !== undefined ? param.showToast : true;
  251. let isAborted = false;
  252. let complete = false;
  253. let lastChunkTime = 0; // 记录最后一次接收数据的时间
  254. // 设置超时检测,无数据传输时判断为结束
  255. const timeoutInterval = 500000; // 5分钟无数据则视为结束
  256. const checkTimeout = () => {
  257. if (complete || isAborted) return;
  258. const now = Date.now();
  259. if (lastChunkTime > 0 && now - lastChunkTime > timeoutInterval) {
  260. // 超时,视为请求完成
  261. finish();
  262. } else {
  263. // 继续检测
  264. setTimeout(checkTimeout, 1000);
  265. }
  266. };
  267. // 创建请求任务
  268. const requestTask = uni.request({
  269. ...requestConfig,
  270. // 流式请求需要设置响应类型为arraybuffer
  271. responseType: 'arraybuffer',
  272. success: (res) => {
  273. if (res.statusCode !== 200) {
  274. const error = new Error(`API错误: ${res.statusCode}`);
  275. error.response = res;
  276. if (showToast) uni.$u.toast(error.message);
  277. reject(error);
  278. return;
  279. }
  280. },
  281. fail: (err) => {
  282. if (isAborted) return;
  283. const error = new Error('网络异常, 请检查网络连接');
  284. error.originalError = err;
  285. if (showToast) uni.$u.toast(error.message);
  286. reject(error);
  287. }
  288. });
  289. // 处理流式数据
  290. // const decoder = new TextDecoder('utf-8');
  291. // 检查请求任务是否支持onChunkReceived方法
  292. if (typeof requestTask.onChunkReceived !== 'function') {
  293. reject(new Error('当前环境不支持流式请求'));
  294. return;
  295. }
  296. // 监听数据块
  297. requestTask.onChunkReceived((response) => {
  298. if (isAborted || complete) return;
  299. // 更新最后接收数据的时间
  300. lastChunkTime = Date.now();
  301. // 处理token过期情况
  302. if (response.statusCode === 401 || (response.data && response.data.code === 501)) {
  303. this.handleError({ code: 501 }, param, true)
  304. .then(() => {
  305. // 刷新成功后会重新发起请求,这里终止当前流
  306. abort();
  307. })
  308. .catch(err => {
  309. if (!complete) {
  310. complete = true;
  311. reject(err);
  312. }
  313. });
  314. return;
  315. }
  316. // 解码二进制数据
  317. const chunk = this.decodeUtf8(response.data)
  318. // const chunk = decoder.decode(response.data, { stream: true });
  319. // 检查是否是结束标记(根据实际后端约定调整)
  320. if (chunk.includes('[DONE]') || chunk.includes('</stream>') || chunk.includes('result')) {
  321. // 调用流式回调处理最后一块数据
  322. if (typeof param.onChunk === 'function') {
  323. param.onChunk(chunk.replace(/\[DONE\]|<\/stream>/g, ''), response);
  324. }
  325. finish();
  326. return;
  327. }
  328. // 调用流式回调处理数据
  329. if (typeof param.onChunk === 'function') {
  330. param.onChunk(chunk, response);
  331. }
  332. });
  333. // 监听请求头接收
  334. if (typeof requestTask.onHeadersReceived === 'function') {
  335. requestTask.onHeadersReceived((response) => {
  336. if (response.statusCode === 200 && typeof param.onOpen === 'function') {
  337. param.onOpen(response);
  338. // 收到响应头后开始超时检测
  339. lastChunkTime = Date.now();
  340. setTimeout(checkTimeout, 1000);
  341. }
  342. });
  343. } else {
  344. // 如果不支持onHeadersReceived,直接开始超时检测
  345. setTimeout(checkTimeout, 1000);
  346. }
  347. // 终止请求的方法
  348. const abort = () => {
  349. isAborted = true;
  350. if (requestTask.abort) {
  351. requestTask.abort();
  352. }
  353. if (typeof param.onAbort === 'function') {
  354. param.onAbort();
  355. }
  356. };
  357. // 完成回调
  358. const finish = () => {
  359. if (complete) return;
  360. complete = true;
  361. if (typeof param.onComplete === 'function') {
  362. param.onComplete();
  363. }
  364. resolve();
  365. };
  366. // 暴露终止方法
  367. if (typeof param.onInit === 'function') {
  368. param.onInit(abort);
  369. }
  370. });
  371. }
  372. // 新增:手动实现 UTF-8 解码(替换 TextDecoder)
  373. decodeUtf8(arrayBuffer) {
  374. const uint8Array = new Uint8Array(arrayBuffer);
  375. let str = '';
  376. let i = 0;
  377. const len = uint8Array.length;
  378. while (i < len) {
  379. const byte = uint8Array[i];
  380. let codePoint;
  381. // 单字节字符(0-127)
  382. if (byte < 0x80) {
  383. codePoint = byte;
  384. i += 1;
  385. }
  386. // 双字节字符(128-2047)
  387. else if (byte >= 0xC0 && byte < 0xE0) {
  388. codePoint = ((byte & 0x1F) << 6) | (uint8Array[i + 1] & 0x3F);
  389. i += 2;
  390. }
  391. // 三字节字符(2048-65535)
  392. else if (byte >= 0xE0 && byte < 0xF0) {
  393. codePoint = ((byte & 0x0F) << 12) | ((uint8Array[i + 1] & 0x3F) << 6) | (uint8Array[i + 2] & 0x3F);
  394. i += 3;
  395. }
  396. // 四字节字符(Unicode 扩展平面)
  397. else if (byte >= 0xF0 && byte < 0xF8) {
  398. codePoint = ((byte & 0x07) << 18) | ((uint8Array[i + 1] & 0x3F) << 12) | ((uint8Array[i + 2] & 0x3F) << 6) | (uint8Array[i + 3] & 0x3F);
  399. i += 4;
  400. }
  401. // 无效字节(跳过)
  402. else {
  403. i += 1;
  404. continue;
  405. }
  406. // 处理四字节字符的代理对
  407. if (codePoint <= 0xFFFF) {
  408. str += String.fromCharCode(codePoint);
  409. } else {
  410. codePoint -= 0x10000;
  411. const high = (codePoint >> 10) & 0x3FF;
  412. const low = codePoint & 0x3FF;
  413. str += String.fromCharCode(0xD800 + high, 0xDC00 + low);
  414. }
  415. }
  416. return str;
  417. }
  418. }
  419. export default new Request();