update
This commit is contained in:
223
t_global_Fine_temp4/src/api/http.ts
Normal file
223
t_global_Fine_temp4/src/api/http.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
// http.js
|
||||
import axios from "axios";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// ============ 配置 ============
|
||||
const BASE_URL = import.meta.env.VITE_BASE_URL === "/"
|
||||
? "/"
|
||||
: import.meta.env.VITE_BASE_URL.startsWith('localhost:')
|
||||
? `http://${import.meta.env.VITE_BASE_URL}`
|
||||
: `https://${import.meta.env.VITE_BASE_URL}`;
|
||||
|
||||
const DB_CONFIG = {
|
||||
name: "TokenDB",
|
||||
version: 2,
|
||||
store: "tokens",
|
||||
key: "userToken",
|
||||
} as const;
|
||||
|
||||
const STORAGE_KEY = "token";
|
||||
|
||||
// ============ IndexedDB 操作 ============
|
||||
class TokenDB {
|
||||
private static async open(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (db.objectStoreNames.contains(DB_CONFIG.store)) {
|
||||
db.deleteObjectStore(DB_CONFIG.store);
|
||||
}
|
||||
db.createObjectStore(DB_CONFIG.store, { keyPath: "key" });
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
static async get(): Promise<string | null> {
|
||||
try {
|
||||
const db = await this.open();
|
||||
return new Promise((resolve) => {
|
||||
const tx = db.transaction(DB_CONFIG.store, "readonly");
|
||||
const request = tx.objectStore(DB_CONFIG.store).get(DB_CONFIG.key);
|
||||
request.onsuccess = () => resolve(request.result?.value || null);
|
||||
request.onerror = () => resolve(null);
|
||||
tx.oncomplete = () => db.close();
|
||||
tx.onabort = () => db.close();
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static async set(token: string): Promise<void> {
|
||||
try {
|
||||
const db = await this.open();
|
||||
return new Promise((resolve) => {
|
||||
const tx = db.transaction(DB_CONFIG.store, "readwrite");
|
||||
tx.objectStore(DB_CONFIG.store).put({ key: DB_CONFIG.key, value: token });
|
||||
tx.oncomplete = () => { db.close(); resolve(); };
|
||||
tx.onerror = () => { db.close(); resolve(); };
|
||||
});
|
||||
} catch {
|
||||
// 静默失败,有其他存储兜底
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Token 管理器 ============
|
||||
class TokenManager {
|
||||
private static cache: string | null = null;
|
||||
private static pending: Promise<string> | null = null;
|
||||
|
||||
// UUID v4 格式校验,防止脏数据
|
||||
private static isValidToken(token: string | null): token is string {
|
||||
return !!token && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(token);
|
||||
}
|
||||
|
||||
// 安全地操作 Storage
|
||||
private static safeGet(storage: Storage): string | null {
|
||||
try {
|
||||
return storage.getItem(STORAGE_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static safeSet(storage: Storage, token: string): void {
|
||||
try {
|
||||
storage.setItem(STORAGE_KEY, token);
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
// Cookie 操作(同步,iOS 上比 localStorage 更早可用)
|
||||
private static getFromCookie(): string | null {
|
||||
try {
|
||||
const match = document.cookie.match(new RegExp(`(?:^|; )${STORAGE_KEY}=([^;]*)`));
|
||||
return match ? decodeURIComponent(match[1]) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static saveToCookie(token: string): void {
|
||||
try {
|
||||
// 有效期 400 天(Safari 上限),SameSite=Lax 兼容 WebView
|
||||
document.cookie = `${STORAGE_KEY}=${encodeURIComponent(token)};path=/;max-age=34560000;SameSite=Lax`;
|
||||
} catch {
|
||||
// 静默失败
|
||||
}
|
||||
}
|
||||
|
||||
// 同步到所有存储(后台执行,不阻塞)
|
||||
private static syncToAllStorages(token: string): void {
|
||||
this.safeSet(sessionStorage, token);
|
||||
this.safeSet(localStorage, token);
|
||||
this.saveToCookie(token);
|
||||
TokenDB.set(token).catch(() => { });
|
||||
}
|
||||
|
||||
// 从同步存储快速获取(cookie 优先,iOS 上最可靠的同步读取)
|
||||
private static getFromSyncStorage(): string | null {
|
||||
const token = this.getFromCookie() || this.safeGet(sessionStorage) || this.safeGet(localStorage);
|
||||
return this.isValidToken(token) ? token : null;
|
||||
}
|
||||
|
||||
// 延迟后重试读取同步存储(iOS 冷启动时存储可能未就绪)
|
||||
private static waitAndRetrySync(ms: number): Promise<string | null> {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(() => resolve(this.getFromSyncStorage()), ms);
|
||||
});
|
||||
}
|
||||
|
||||
// 主入口:获取或创建 Token
|
||||
static async getToken(): Promise<string> {
|
||||
// 1. 内存缓存(最快)
|
||||
if (this.cache) return this.cache;
|
||||
|
||||
// 2. 等待进行中的创建(并发安全)
|
||||
if (this.pending) return this.pending;
|
||||
|
||||
// 3. 同步存储快速路径
|
||||
const syncToken = this.getFromSyncStorage();
|
||||
if (syncToken) {
|
||||
this.cache = syncToken;
|
||||
this.syncToAllStorages(syncToken);
|
||||
return syncToken;
|
||||
}
|
||||
|
||||
// 4. 异步获取或创建(带锁)
|
||||
this.pending = this.createToken();
|
||||
return this.pending;
|
||||
}
|
||||
|
||||
private static async createToken(): Promise<string> {
|
||||
try {
|
||||
// 再次检查缓存
|
||||
if (this.cache) return this.cache;
|
||||
|
||||
// 尝试从 IndexedDB 恢复
|
||||
const dbToken = await TokenDB.get();
|
||||
if (dbToken && this.isValidToken(dbToken)) {
|
||||
this.cache = dbToken;
|
||||
this.syncToAllStorages(dbToken);
|
||||
return dbToken;
|
||||
}
|
||||
|
||||
// iOS 冷启动兜底:等待一小段时间后重试同步存储
|
||||
// (localStorage/cookie 数据可能存在,但初始化瞬间还未就绪)
|
||||
for (const delay of [50, 100, 150]) {
|
||||
const retryToken = await this.waitAndRetrySync(delay);
|
||||
if (retryToken) {
|
||||
this.cache = retryToken;
|
||||
this.syncToAllStorages(retryToken);
|
||||
return retryToken;
|
||||
}
|
||||
}
|
||||
|
||||
// 所有恢复手段用尽,生成新 Token
|
||||
const newToken = uuidv4();
|
||||
this.cache = newToken;
|
||||
this.syncToAllStorages(newToken);
|
||||
return newToken;
|
||||
} finally {
|
||||
this.pending = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Axios 实例 ============
|
||||
const http = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
timeout: 15000,
|
||||
});
|
||||
|
||||
// 请求拦截器
|
||||
http.interceptors.request.use(
|
||||
async (config) => {
|
||||
const token = await TokenManager.getToken();
|
||||
config.headers["Token"] = token;
|
||||
config.headers["X-Token"] = token;
|
||||
config.params = { ...config.params, token };
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// 响应拦截器
|
||||
http.interceptors.response.use(
|
||||
(response) => response.data,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
console.error("Error:", error.response.status, error.response.data);
|
||||
} else {
|
||||
console.error("Error:", error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default http;
|
||||
Reference in New Issue
Block a user