http-utils.md 3.8 KB

HTTP 与工具(utils)

用原生 fetch 薄封装,不引 axios。工具层是纯无状态函数,谁都能 import,不持有全局状态。

一、utils 的边界

允许 禁止
纯函数(格式化、校验、URL 拼接、深拷贝) 持有可变全局状态
通用封装(http、storage) import components/ 或 services/
依赖浏览器 API(fetch/localStorage/URL) 依赖 Lit / DOM 渲染

utils 必须可在 Node 或任意上下文 import 而不产生副作用。

二、HTTP 封装(utils/http.js)

// utils/http.js
import { authService } from '../services/auth-service.js';

const BASE = '/api';   // 或从 config 注入

class HttpError extends Error {
  constructor(status, body) { super(`HTTP ${status}`); this.status = status; this.body = body; }
}

async function request(method, path, { body, headers = {}, query } = {}) {
  let url = BASE + path;
  if (query) url += '?' + new URLSearchParams(query).toString();

  const finalHeaders = { 'Content-Type': 'application/json', ...headers };
  const token = authService?.getToken?.();
  if (token) finalHeaders.Authorization = `Bearer ${token}`;

  const res = await fetch(url, {
    method,
    headers: finalHeaders,
    body: body != null ? JSON.stringify(body) : undefined,
    credentials: 'same-origin',
  });

  if (res.status === 401) {
    authService.logout();
    // 触发跳转登录(由路由/根组件响应 signal)
    throw new HttpError(401, null);
  }
  const ct = res.headers.get('content-type') || '';
  const data = ct.includes('application/json') ? await res.json() : await res.text();
  if (!res.ok) throw new HttpError(res.status, data);
  return data;
}

export const http = {
  get: (p, o) => request('GET', p, o),
  post: (p, body, o) => request('POST', p, { ...o, body }),
  put: (p, body, o) => request('PUT', p, { ...o, body }),
  del: (p, o) => request('DELETE', p, o),
};

设计要点:

  • token 自动注入(从 authService 取)。
  • 401 统一处理:登出 + 让 UI 响应 signal 跳登录。
  • 统一错误:抛 HttpError,携带 status / body,调用方可 catch。
  • DTO 解析按 content-type 分流。
  • 循环依赖注意:http 引用 authService,authService 又用 http → 用惰性调用(函数内取)而非模块顶层解构,避免 ESM 循环初始化问题。

三、Storage 封装(utils/storage.js)

export const storage = {
  get(key, fallback = null) {
    try { const v = localStorage.getItem(key); return v == null ? fallback : JSON.parse(v); }
    catch { return fallback; }
  },
  set(key, value) { localStorage.setItem(key, JSON.stringify(value)); },
  remove(key) { localStorage.removeItem(key); },
};
  • 加 try/catch(隐私模式 / 配额满会抛)。
  • JSON 序列化,避免手写类型转换。
  • 敏感数据(token)单独考虑,不要明文长期存(见安全)。

四、格式化(utils/format.js)

export const formatMoney = (n, currency = '¥') =>
  `${currency}${Number(n ?? 0).toFixed(2)}`;

export const formatDate = (d, fmt = 'YYYY-MM-DD') => { /* ... */ };

export const debounce = (fn, ms = 200) => {
  let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
};

纯函数,无副作用,可单测。

五、其他常用工具

工具 用途
utils/router.js navigate()(基于 document.baseURI,尊重 <base href>)
utils/dom.js 少量原生 DOM 辅助(慎用,优先 Lit 指令)
utils/validate.js 表单校验纯函数

六、禁止

  • ❌ 在 utils 里 import { html }。
  • ❌ 在 utils 里持有跨调用的可变状态(那属于 Service)。
  • ❌ 在组件里直接 fetch(一律走 http)。
  • ❌ 手写重复的 URL 拼接 / 时间格式化(抽到 utils)。