allinssl/frontend/packages/utils/src/browser.ts

662 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* 文件定义:浏览器相关操作
*/
import * as R from "ramda";
/* -------------- 1、浏览器相关操作 -------------- */
/**
* 获取当前页面 URL
*/
export const isHttps = (): boolean => window.location.protocol === "https:";
/**
* 判断是否为开发环境
*/
export const isDev = (): boolean => process.env.NODE_ENV === "development";
/**
* 获取浏览器及操作系统信息
* @returns {{ browser: string; os: string }} 浏览器和操作系统信息
*/
export const getBrowserOSInfo = (): { browser: string; os: string } => {
const ua = navigator.userAgent;
type Rule = [(str: string) => boolean, () => string];
// 浏览器识别规则
const browserRules: Rule[] = [
[
R.allPass([R.test(/Chrome/), R.complement(R.test(/Edg/))]),
R.always("Chrome"),
],
[R.test(/Firefox/), R.always("Firefox")],
[
R.allPass([R.test(/Safari/), R.complement(R.test(/Chrome/))]),
R.always("Safari"),
],
[R.test(/Edg/), R.always("Edge")],
[R.T, R.always("Unknown")],
];
// 操作系统识别规则
const osRules: Rule[] = [
[R.test(/iPhone|iPad|iPod/), R.always("iOS")],
[R.test(/Android/), R.always("Android")],
[R.test(/Win/), R.always("Windows")],
[
R.allPass([R.test(/Mac/), R.complement(R.test(/iPhone|iPad|iPod/))]),
R.always("macOS"),
],
[R.test(/Linux/), R.always("Linux")],
[R.T, R.always("Unknown")],
];
return {
browser: R.cond(browserRules)(ua),
os: R.cond(osRules)(ua),
};
};
/**
* 获取屏幕信息,分辨率、缩放比例
*/
export const getScreenInfo = (): {
resolution: string;
scale: number;
} => {
const resolution = `${window.screen.width}x${window.screen.height}`;
const scale = window.devicePixelRatio;
return { resolution, scale };
};
/**
* 判断是否为移动设备
* @returns {boolean} 是否为移动设备
*/
export const isMobile = (): boolean => {
const ua = navigator.userAgent;
// 移动设备识别规则
const mobileRules = [
/Android/i,
/webOS/i,
/iPhone/i,
/iPad/i,
/iPod/i,
/BlackBerry/i,
/Windows Phone/i,
/Mobile/i,
/Tablet/i,
];
return mobileRules.some((rule) => rule.test(ua));
};
/**
* 判断设备类型
* @returns {'mobile' | 'desktop'} 设备类型
*/
export const getDeviceType = (): "mobile" | "desktop" => {
return isMobile() ? "mobile" : "desktop";
};
/**
* 柯里化版本的设备类型判断
*/
export const isMobileCurried = R.always(isMobile());
export const getDeviceTypeCurried = R.always(getDeviceType());
/* -------------- 2、浏览器缓存相关操作 -------------- */
/**
* 强制刷新页面并清理所有缓存
* 清除 Cache API、localStorage 和 sessionStorage 后刷新
*/
export const forceRefresh = () => {
clearBrowserCache();
// window.location.reload()
};
/**
* 获取 URL 参数
* @param name 参数名
*/
export const getUrlParam = (name: string): string | null => {
const params = new URLSearchParams(window.location.search);
return params.get(name);
};
/**
* 柯里化版本的getUrlParam
*/
export const getUrlParamCurried: {
(name: string): string | null;
} = R.curry(getUrlParam);
/**
* Cookie 操作辅助函数:根据 HTTPS 协议增加前缀
* @param key cookie 键名
*/
export const cookiePrefixKey = (key: string): string =>
R.ifElse(R.always(isHttps()), (k: string) => `https_${k}`, R.identity)(key);
/**
* 设置 Cookie
* @param {string} key 键名
* @param {string} value 值
* @param {number} days 过期天数(可选)
*/
export const setCookie = (key: string, value: string, days?: number): void => {
const prefixedKey = cookiePrefixKey(key);
// 获取过期时间
const getExpires = (days?: number): string => {
if (!days) return "";
const date = new Date();
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
return `; expires=${date.toUTCString()}`;
};
const expires = getExpires(days);
document.cookie = `${prefixedKey}=${encodeURIComponent(
value
)}${expires}; path=/`;
};
/**
* 柯里化版本的setCookie
*/
export const setCookieCurried: {
(key: string, value: string, days?: number): void;
(key: string): (value: string, days?: number) => void;
(key: string, value: string): (days?: number) => void;
} = R.curry(setCookie);
/**
* 获取 Cookie
* @param key 键名
*/
export const getCookie = (
key: string,
isPrefixKey: boolean = true
): string | null => {
const prefixedKey = isPrefixKey ? cookiePrefixKey(key) : key;
const nameEQ = `${prefixedKey}=`;
const cookies = document.cookie.split(";").map((c) => c.trim());
const cookie = cookies.find((c) => c.startsWith(nameEQ));
if (cookie) {
return decodeURIComponent(cookie.substring(nameEQ.length));
}
return null;
};
/**
* 柯里化版本的getCookie
*/
export const getCookieCurried: {
(key: string): string | null;
} = R.curry(getCookie);
/**
* 删除 Cookie
* @param key 键名
*/
export const deleteCookie = (key: string): void => {
// 设置过期时间为负值,即删除 Cookie
setCookie(key, "", -1);
console.log(document.cookie);
};
/**
* 清空 Cookie
*/
export const clearCookie = (): void => {
const cookies = document.cookie.split(";").map((c) => c.trim()); // 获取所有 Cookie
cookies.forEach((c) => {
const [key] = c.split("=");
if (key) {
// 通过设置过期时间为过去来删除cookie
document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
}
});
};
/**
* 设置存储增强(支持自动序列化)
*/
export const setStorageItem = (
key: string,
value: any,
storage: Storage
): void => {
const serializedValue = JSON.stringify(value);
storage.setItem(key, serializedValue);
};
/**
* 柯里化版本的setStorageItem
*/
export const setStorageItemCurried: {
(key: string, value: any, storage: Storage): void;
(key: string): (value: any, storage: Storage) => void;
(key: string, value: any): (storage: Storage) => void;
} = R.curry(setStorageItem);
/**
* 获取存储增强(支持自动反序列化)
*/
export const getStorageItem = (key: string, storage: Storage): any => {
const value = storage.getItem(key);
return value ? JSON.parse(value) : null;
};
/**
* 柯里化版本的getStorageItem
*/
export const getStorageItemCurried: {
(key: string, storage: Storage): any;
(key: string): (storage: Storage) => any;
} = R.curry(getStorageItem);
/**
* 删除存储
* @param key 键名
* @param storage 存储类型(可选)
*/
export const removeStorageItem = (
key: string,
storage: Storage = localStorage
): void => storage.removeItem(key);
/**
* 设置 sessionStorage 数据
* @param key 键名
* @param value 值
*/
export const setSessionItem = (key: string, value: any): void =>
setStorageItem(key, value, sessionStorage);
/**
* 获取 sessionStorage 数据
* @param key 键名
*/
export const getSessionItem = (key: string): any =>
getStorageItem(key, sessionStorage);
/**
* 删除 sessionStorage 数据
* @param key 键名
*/
export const removeSessionItem = (key: string): void =>
sessionStorage.removeItem(key);
/**
* 清空 sessionStorage 中所有数据
*/
export const clearSession = (): void => sessionStorage.clear();
/**
* 设置 localStorage 数据
* @param key 键名
* @param value 值
*/
export const setLocalItem = (key: string, value: any): void =>
setStorageItem(key, value, localStorage);
/**
* 获取 localStorage 数据
* @param key 键名
*/
export const getLocalItem = (key: string): any =>
getStorageItem(key, localStorage);
/**
* 删除 localStorage 数据
* @param key 键名
*/
export const removeLocalItem = (key: string): void =>
localStorage.removeItem(key);
/**
* 清空 localStorage 中所有数据
*/
export const clearLocal = (): void => localStorage.clear();
/**
* 清空浏览器缓存
*/
export const clearBrowserCache = (): void => {
clearSession();
clearLocal();
clearCookie();
};
/**
* 创建过期时间的存储支持sessionStorage和localStorage
* @param key 键名
* @param value 值
* @param time 过期时间可选支持new Date()、时间戳、时间字符串
* @param storage 存储类型(可选)
*/
export const setExpiredStorageItem = (
key: string,
value: any,
time?: Date | number | string,
storage: Storage = localStorage
): void => {
// 如果没有设置过期时间,直接存储值
if (!time) {
setStorageItem(key, value, storage);
return;
}
// 转换过期时间为时间戳
let expires: number;
if (time instanceof Date) {
expires = time.getTime();
} else if (typeof time === "number") {
expires = time;
} else {
expires = new Date(time).getTime();
}
// 存储数据和过期时间
const data = {
value,
expires,
};
setStorageItem(key, data, storage);
};
/**
* 获取过期时间的存储
* @param key 键名
*/
export const getExpiredStorageItem = (
key: string,
storage: Storage = localStorage
): any => {
const data = getStorageItem(key, storage);
if (!data) return null;
// 检查是否过期
if (data.expires && data.expires < Date.now()) {
removeStorageItem(key, storage);
return null;
}
};
/* -------------- 3、IndexedDB 相关操作 -------------- */
/**
* IndexedDB 相关类型定义
* @interface IndexedDBConfig
* @description 数据库配置接口,用于定义数据库的基本结构
* @property {string} dbName - 数据库名称
* @property {number} version - 数据库版本号,用于数据库升级
* @property {Object} stores - 存储对象配置key 为存储对象名称
*/
export interface IndexedDBConfig {
dbName: string;
version: number;
stores: {
[key: string]: {
/** 主键路径,用于唯一标识记录 */
keyPath: string;
/** 索引配置数组,用于优化查询性能 */
indexes?: Array<{
/** 索引名称 */
name: string;
/** 索引的键路径 */
keyPath: string;
/** 索引选项,如是否唯一等 */
options?: IDBIndexParameters;
}>;
};
};
}
/**
* IndexedDB 管理类
* @class IndexedDBManager
* @description 提供 IndexedDB 数据库操作的统一接口,支持异步操作和类型安全
*/
export class IndexedDBManager {
/** 数据库连接实例 */
private db: IDBDatabase | null = null;
/** 数据库配置信息 */
private config: IndexedDBConfig;
/**
* 构造函数
* @param config 数据库配置对象
*/
constructor(config: IndexedDBConfig) {
this.config = config;
}
/**
* 统一的事件处理器
* @description 处理 IDBRequest 的成功和错误事件
* @template T 返回数据类型
* @param request IDBRequest 实例
* @returns Promise<T> 返回处理结果
*/
private handleRequest<T>(request: IDBRequest<T>): Promise<T> {
return new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
/**
* 获取事务和对象仓库
* @description 创建事务并获取对象仓库
* @param storeName 仓库名称
* @param mode 事务模式
* @returns 包含事务和对象仓库的对象
*/
private async getTransactionAndStore(
storeName: string,
mode: IDBTransactionMode = "readonly"
): Promise<{
transaction: IDBTransaction;
store: IDBObjectStore;
}> {
await this.connect();
const transaction = this.db!.transaction(storeName, mode);
const store = transaction.objectStore(storeName);
return { transaction, store };
}
/**
* 初始化数据库连接
* @description 创建或打开数据库连接,如果数据库不存在则自动创建
* @returns {Promise<IDBDatabase>} 返回数据库连接实例
* @throws {Error} 连接失败时抛出错误
*/
async connect(): Promise<IDBDatabase> {
if (this.db) return this.db;
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.config.dbName, this.config.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(request.result);
};
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
Object.entries(this.config.stores).forEach(
([storeName, storeConfig]) => {
if (!db.objectStoreNames.contains(storeName)) {
const store = db.createObjectStore(storeName, {
keyPath: storeConfig.keyPath,
});
storeConfig.indexes?.forEach((index) => {
store.createIndex(index.name, index.keyPath, index.options);
});
}
}
);
};
});
}
/**
* 添加数据
* @description 向指定的对象仓库添加新数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T} data 要添加的数据
* @returns {Promise<IDBValidKey>} 返回新添加数据的主键
*/
async add<T>(storeName: string, data: T): Promise<IDBValidKey> {
const { store } = await this.getTransactionAndStore(storeName, "readwrite");
return this.handleRequest(store.add(data));
}
/**
* 更新数据
* @description 更新指定对象仓库中的数据,如果数据不存在则添加
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T} data 要更新的数据
* @returns {Promise<IDBValidKey>} 返回更新数据的主键
*/
async put<T>(storeName: string, data: T): Promise<IDBValidKey> {
const { store } = await this.getTransactionAndStore(storeName, "readwrite");
return this.handleRequest(store.put(data));
}
/**
* 删除数据
* @description 从指定对象仓库中删除数据
* @param {string} storeName 仓库名称
* @param {IDBValidKey} key 要删除数据的主键
* @returns {Promise<void>} 删除成功时解析
*/
async delete(storeName: string, key: IDBValidKey): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, "readwrite");
return this.handleRequest(store.delete(key));
}
/**
* 通过主键获取数据
* @description 从指定对象仓库中获取指定主键的数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @param {IDBValidKey} key 主键值
* @returns {Promise<T | undefined>} 返回查询到的数据
*/
async get<T>(storeName: string, key: IDBValidKey): Promise<T | undefined> {
const { store } = await this.getTransactionAndStore(storeName);
return this.handleRequest(store.get(key));
}
/**
* 通过索引查询数据
* @description 使用索引从指定对象仓库中查询数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @param {string} indexName 索引名称
* @param {IDBValidKey} key 索引值
* @returns {Promise<T | undefined>} 返回查询到的数据
*/
async getByIndex<T>(
storeName: string,
indexName: string,
key: IDBValidKey
): Promise<T | undefined> {
const { store } = await this.getTransactionAndStore(storeName);
const index = store.index(indexName);
return this.handleRequest(index.get(key));
}
/**
* 获取所有数据
* @description 获取指定对象仓库中的所有数据
* @template T 返回数据类型
* @param {string} storeName 仓库名称
* @returns {Promise<T[]>} 返回所有数据的数组
*/
async getAll<T>(storeName: string): Promise<T[]> {
const { store } = await this.getTransactionAndStore(storeName);
return this.handleRequest(store.getAll());
}
/**
* 使用游标遍历数据
* @description 使用游标遍历对象仓库中的数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {(item: T) => void} callback 处理每条数据的回调函数
* @returns {Promise<void>}
*/
async forEach<T>(
storeName: string,
callback: (item: T) => void
): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName);
return new Promise((resolve, reject) => {
const request = store.openCursor();
request.onerror = () => reject(request.error);
request.onsuccess = () => {
const cursor = request.result;
if (cursor) {
callback(cursor.value);
cursor.continue();
} else {
resolve();
}
};
});
}
/**
* 批量添加数据
* @description 向指定的对象仓库批量添加数据
* @template T 数据类型
* @param {string} storeName 仓库名称
* @param {T[]} items 要添加的数据数组
* @returns {Promise<void>}
*/
async addBatch<T>(storeName: string, items: T[]): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, "readwrite");
return new Promise((resolve, reject) => {
try {
items.forEach((item) => store.add(item));
resolve();
} catch (error) {
reject(error);
}
});
}
/**
* 清空对象仓库
* @description 删除指定对象仓库中的所有数据
* @param {string} storeName 仓库名称
* @returns {Promise<void>}
*/
async clear(storeName: string): Promise<void> {
const { store } = await this.getTransactionAndStore(storeName, "readwrite");
return this.handleRequest(store.clear());
}
/**
* 关闭数据库连接
* @description 安全地关闭数据库连接,释放资源
*/
close(): void {
if (this.db) {
this.db.close();
this.db = null;
}
}
}