http.js 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. /**
  2. * utils/http.js —— 唯一的网络出口(只有 services 调它;视图层不直接 fetch)。
  3. * 只带公开的 App ID;任何凭据都不进前端。
  4. */
  5. export const request = (...args) => fetch(...args);
  6. export class HttpError extends Error {
  7. constructor(status, body, url) {
  8. super(`HTTP ${status}${body ? ` · ${body}` : ''}`);
  9. this.name = 'HttpError';
  10. this.status = status;
  11. this.url = url;
  12. }
  13. }
  14. export function createHttp({ base, appId, fetchImpl = (...a) => fetch(...a) }) {
  15. const headers = () => ({ 'X-Parse-Application-Id': appId, Accept: 'application/json' });
  16. async function call(path, { method = 'GET', body, timeoutMs = 20000, retries = 1 } = {}) {
  17. const url = base + path;
  18. let lastErr;
  19. for (let i = 0; i <= retries; i++) {
  20. const ctrl = new AbortController();
  21. const t = setTimeout(() => ctrl.abort(), timeoutMs);
  22. try {
  23. const res = await fetchImpl(url, {
  24. method,
  25. headers: { ...headers(), ...(body ? { 'Content-Type': 'application/json' } : {}) },
  26. body: body ? JSON.stringify(body) : undefined,
  27. cache: 'no-store',
  28. signal: ctrl.signal,
  29. });
  30. const text = await res.text();
  31. let json = null;
  32. try { json = text ? JSON.parse(text) : {}; } catch { json = null; }
  33. if (!res.ok) throw new HttpError(res.status, json?.error || text.slice(0, 200), path);
  34. if (json == null) throw new HttpError(res.status, '返回的不是 JSON', path);
  35. return json;
  36. } catch (err) {
  37. lastErr = err;
  38. // 4xx 不重试(重试也没用);网络错误 / 5xx 重试一次
  39. if (err instanceof HttpError && err.status < 500) break;
  40. if (i < retries) await new Promise((r) => setTimeout(r, 400 * (i + 1)));
  41. } finally {
  42. clearTimeout(t);
  43. }
  44. }
  45. throw lastErr;
  46. }
  47. return {
  48. get: (path, opts) => call(path, opts),
  49. post: (path, body, opts) => call(path, { ...opts, method: 'POST', body }),
  50. put: (path, body, opts) => call(path, { ...opts, method: 'PUT', body }),
  51. };
  52. }
  53. /**
  54. * 分页拉全表:每页 limit 条(Parse 单次上限 1000),直到不足一页。
  55. * 翻了 50 页仍满页 → 标记 truncated,UI 明确提示「数据被截断」,不悄悄少显示。
  56. */
  57. export async function fetchAll(http, className, { limit = 1000, order = 'createdAt' } = {}) {
  58. const rows = [];
  59. let skip = 0;
  60. for (let guard = 0; guard < 50; guard++) {
  61. const json = await http.get(`/classes/${encodeURIComponent(className)}?limit=${limit}&skip=${skip}&order=${order}`);
  62. const batch = Array.isArray(json.results) ? json.results : [];
  63. rows.push(...batch);
  64. if (batch.length < limit) return { rows, truncated: false };
  65. skip += limit;
  66. }
  67. return { rows, truncated: true };
  68. }