@tker/shared
v1.0.4
Published
一个 Vue3/TypeScript 项目常用的工具函数库,提供缓存管理、日期处理、HTTP 请求和通用工具函数。
Readme
@tker/shared
一个 Vue3/TypeScript 项目常用的工具函数库,提供缓存管理、日期处理、HTTP 请求和通用工具函数。
特性
- 模块化设计:按功能模块划分,按需导入
- TypeScript 支持:完整的类型定义
- 零副作用:所有模块均为纯函数,无副作用
- 轻量级:不依赖特定 UI 框架
安装
pnpm add @tker/shared模块结构
src/
├── cache/ # 缓存管理
│ └── storage-manager.ts
├── date/ # 日期处理
│ └── index.ts
├── request/ # HTTP 请求客户端
│ ├── client.ts
│ ├── interceptors.ts
│ └── modules/
├── utils/ # 通用工具函数
│ ├── array.ts # 数组工具
│ ├── string.ts # 字符串工具
│ ├── object.ts # 对象工具
│ ├── tree.ts # 树结构工具
│ ├── download.ts # 文件下载
│ ├── scroll.ts # 滚动工具
│ ├── sleep.ts # 睡眠函数
│ ├── window.ts # 窗口工具
│ ├── dom.ts # DOM 工具
│ ├── screen.ts # 全屏工具
│ ├── inference.ts # 类型判断
│ └── merge.ts # 对象合并缓存管理 (@tker/shared/cache)
提供带 TTL(存活时间)和前缀支持的本地存储管理。
基础用法
import { StorageManager } from "@tker/shared/cache";
// 创建存储管理器
const storage = new StorageManager({
prefix: "my-app", // 可选:键名前缀
storageType: "localStorage", // 可选:localStorage 或 sessionStorage
});
// 设置值(带 TTL)
storage.setItem("user", { name: "John", age: 30 }, 60_000); // 60秒后过期
// 设置值(无过期时间)
storage.setItem("config", { theme: "dark" });
// 获取值
const user = storage.getItem<{ name: string; age: number }>("user");
// 获取值(带默认值)
const theme = storage.getItem<string>("theme", "light");
// 删除值
storage.removeItem("user");
// 清除所有带前缀的存储项
storage.clear();
// 清除所有过期的存储项
storage.clearExpiredItems();API
| 方法 | 参数 | 说明 |
|------|------|------|
| setItem | (key, value, ttl?) | 存储值,可选 TTL(毫秒) |
| getItem | (key, defaultValue?) | 获取值,已过期返回默认值 |
| removeItem | (key) | 删除指定项 |
| clear | () | 清除所有带前缀的项 |
| clearExpiredItems | () | 清除所有过期项 |
日期处理 (@tker/shared/date)
基于 dayjs 的日期格式化工具。
基础用法
import { formatDate, formatDateTime, isDayjsObject } from "@tker/shared/date";
// 格式化日期
formatDate("2024-01-15"); // "2024-01-15"
formatDate(1705286400000); // "2024-01-15"
formatDate("2024-01-15", "YYYY/MM/DD"); // "2024/01/15"
// 格式化日期时间
formatDateTime("2024-01-15"); // "2024-01-15 00:00:00"
// 判断是否为 dayjs 对象
isDayjsObject(dayjs()); // trueAPI
| 方法 | 参数 | 说明 |
|------|------|------|
| formatDate | (time, format?) | 格式化日期,默认 YYYY-MM-DD |
| formatDateTime | (time) | 格式化日期时间 YYYY-MM-DD HH:mm:ss |
| isDayjsObject | (value) | 判断是否为 dayjs 对象 |
HTTP 请求 (@tker/shared/request)
基于 Axios 的 HTTP 请求客户端,支持拦截器、文件上传下载、Token 刷新。
创建客户端
import { RequestClient } from "@tker/shared/request";
// 创建请求客户端
const client = new RequestClient({
baseURL: "https://api.example.com", // 基础 URL
timeout: 10000, // 超时时间(毫秒),默认 10_000
headers: { // 默认请求头
"Content-Type": "application/json;charset=utf-8",
},
responseReturn: "data", // 返回格式:raw | body | data
paramsSerializer: "brackets", // 参数序列化方式(见下文)
});
// responseReturn 说明:
// - "raw": 返回完整 AxiosResponse(包含 status, headers, data 等)
// - "body": 返回响应体 response.data
// - "data": 返回业务数据 response.data.data(需配合 defaultResponseInterceptor)拦截器
请求拦截器(添加 Token)
// 添加请求拦截器,在每个请求前自动添加 Token
client.addRequestInterceptor({
fulfilled: (config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
rejected: (error) => Promise.reject(error),
});默认响应拦截器(处理业务响应格式)
适用于后端返回 { code, data, message } 格式的响应:
import { defaultResponseInterceptor } from "@tker/shared/request";
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: "code", // 业务状态码字段名
dataField: "data", // 业务数据字段名(或函数)
successCode: 0, // 成功状态码(或判断函数)
})
);
// 后端响应格式示例:
// { code: 0, data: { id: 1, name: "John" }, message: "success" }
// 使用 dataField 函数自定义数据提取:
client.addResponseInterceptor(
defaultResponseInterceptor({
codeField: "code",
dataField: (response) => response.result, // 从 response.result 提取数据
successCode: (code) => code >= 0 && code < 100, // 使用函数判断成功
})
);Token 刷新拦截器(处理 401 自动刷新)
当 Token 过期返回 401 时,自动刷新 Token 并重试请求:
import { authenticateResponseInterceptor } from "@tker/shared/request";
client.addResponseInterceptor(
authenticateResponseInterceptor({
client, // RequestClient 实例
enableRefreshToken: true, // 是否启用 Token 刷新
formatToken: (token) => `Bearer ${token}`, // Token 格式化
doRefreshToken: async () => {
// 调用刷新 Token 接口
const res = await client.post("/refresh-token");
const newToken = res.data.token;
localStorage.setItem("token", newToken);
return newToken;
},
doReAuthenticate: async () => {
// Token 刷新失败,跳转登录页
localStorage.removeItem("token");
router.push("/login");
},
})
);错误消息拦截器(统一错误提示)
统一处理网络错误、HTTP 状态码错误,调用消息提示函数:
import { errorMessageResponseInterceptor } from "@tker/shared/request";
client.addResponseInterceptor(
errorMessageResponseInterceptor((message, error) => {
// 调用 UI 框架的消息提示组件
showToast(message); // 或 message.error(message)
})
);
// 自动处理的错误类型:
// - Network Error → "网络异常,请检查您的网络连接后重试。"
// - Timeout → "请求超时,请稍后再试。"
// - 400 → "请求错误。请检查您的输入并重试。"
// - 401 → "登录认证过期,请重新登录后继续。"
// - 403 → "禁止访问,您没有权限访问此资源。"
// - 404 → "未找到,请求的资源不存在。"参数序列化
支持多种数组参数序列化方式,适用于 GET 请求传递数组参数:
// 创建客户端时指定全局序列化方式
const client = new RequestClient({
paramsSerializer: "brackets",
});
// 或在单个请求中指定
await client.get("/users", {
params: { ids: [1, 2, 3] },
paramsSerializer: "brackets",
});
// 各序列化方式对比(params: { ids: [1, 2, 3] }):
// | 方式 | 结果 | 适用场景 |
// |-------------|------------------------------|-------------------|
// | "brackets" | ids[]=1&ids[]=2&ids[]=3 | PHP、Rails |
// | "comma" | ids=1,2,3 | 简洁传递 |
// | "indices" | ids[0]=1&ids[1]=2&ids[2]=3 | 需要索引的场景 |
// | "repeat" | ids=1&ids=2&ids=3 | 传统方式 |文件上传下载
文件上传
// 上传单个文件
await client.upload("/upload", {
file: fileObject, // File 或 Blob 对象
name: "avatar.png", // 可选:文件名
});
// 上传文件并附带其他字段
await client.upload("/upload", {
file: fileObject,
userId: 123,
type: "avatar",
});
// 监听上传进度(需自行实现)
// 注:当前版本不支持 onProgress,可通过自定义 Axios 配置实现
await client.upload("/upload", { file }, {
onUploadProgress: (progressEvent) => {
const percent = Math.round(
(progressEvent.loaded * 100) / progressEvent.total
);
console.log(`上传进度: ${percent}%`);
},
});文件下载
// 下载文件(返回 Blob)
const blob = await client.download("/download/file.pdf");
// 手动触发浏览器下载
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "document.pdf";
a.click();
URL.revokeObjectURL(url);
// 使用 downloadFileFromBlob 工具函数(推荐)
import { downloadFileFromBlob } from "@tker/shared/utils";
const blob = await client.download("/download/file.pdf");
downloadFileFromBlob({ source: blob, fileName: "document.pdf" });
// 获取完整响应(包含 headers)
const response = await client.download("/download/file.pdf", {
responseReturn: "raw",
});
const filename = response.headers["content-disposition"];请求方法
// GET 请求
const data = await client.get("/users");
const user = await client.get("/users/1");
const list = await client.get("/users", { params: { page: 1, size: 10 } });
// POST 请求
const result = await client.post("/users", { name: "John", age: 30 });
const created = await client.post("/users", data, {
headers: { "X-Custom-Header": "value" },
});
// PUT 请求
await client.put("/users/1", { name: "John Updated" });
// DELETE 请求
await client.delete("/users/1");
await client.delete("/users", { params: { ids: [1, 2, 3] } });
// 泛型指定返回类型
interface User {
id: number;
name: string;
}
const user = await client.get<User>("/users/1");
const users = await client.get<User[]>("/users");API 参考
| 方法 | 参数 | 返回类型 | 说明 |
|------|------|----------|------|
| get | (url, config?) | Promise<T> | GET 请求 |
| post | (url, data?, config?) | Promise<T> | POST 请求 |
| put | (url, data?, config?) | Promise<T> | PUT 请求 |
| delete | (url, config?) | Promise<T> | DELETE 请求 |
| upload | (url, data, config?) | Promise<T> | 文件上传,data 需包含 file 字段 |
| download | (url, config?) | Promise<T> | 文件下载,默认返回 Blob |
| addRequestInterceptor | (config) | - | 添加请求拦截器 |
| addResponseInterceptor | (config) | - | 添加响应拦截器 |
配置选项 (RequestClientOptions)
| 选项 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| baseURL | string | - | 基础 URL |
| timeout | number | 10000 | 超时时间(毫秒) |
| headers | object | { "Content-Type": "application/json" } | 默认请求头 |
| responseReturn | "raw" \| "body" \| "data" | "raw" | 响应返回格式 |
| paramsSerializer | "brackets" \| "comma" \| "indices" \| "repeat" | - | 参数序列化方式 |
工具函数 (@tker/shared/utils)
数组工具
import {
listToMapForSingle,
listToMap,
toArray,
arrayFlat,
removeDuplicates
} from "@tker/shared/utils";
// 数组转对象(单值)
const map = listToMapForSingle([{ id: 1, name: "A" }], "id");
// { 1: { id: 1, name: "A" } }
// 数组转对象(多值)
const groupMap = listToMap([{ pid: 1, name: "A" }, { pid: 1, name: "B" }], "pid");
// { 1: [{ pid: 1, name: "A" }, { pid: 1, name: "B" }] }
// 转为数组
toArray("hello"); // ["hello"]
toArray([1, 2]); // [1, 2]
// 扁平化二维数组
arrayFlat([[1, 2], [3, 4]]); // [1, 2, 3, 4]
// 根据 key 去重
removeDuplicates([{ id: 1 }, { id: 1 }, { id: 2 }], "id");
// [{ id: 1 }, { id: 2 }]字符串工具
import {
capitalizeFirstLetter,
toLowerCaseFirstLetter,
kebabToCamelCase,
uuid
} from "@tker/shared/utils";
capitalizeFirstLetter("hello"); // "Hello"
toLowerCaseFirstLetter("Hello"); // "hello"
kebabToCamelCase("my-component"); // "myComponent"
uuid(); // "xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx" (32位)对象工具
import {
deepMerge,
omit,
pick,
deepClone,
filterUndefinedKeys,
jsonParse,
bindMethods
} from "@tker/shared/utils";
// 深度合并
deepMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 } });
// { a: 1, b: { c: 2, d: 3 } }
// 排除属性
omit({ a: 1, b: 2, c: 3 }, ["b", "c"]);
// { a: 1 }
// 选取属性
pick({ a: 1, b: 2, c: 3 }, ["a", "b"]);
// { a: 1, b: 2 }
// 深拷贝
deepClone({ a: 1, b: { c: 2 } });
// 过滤 undefined 键
filterUndefinedKeys({ a: 1, b: undefined });
// { a: 1 }
// JSON 解析(带默认值)
jsonParse('{"a":1}', {}); // { a: 1 }
jsonParse("invalid", {}); // {}
// 绑定方法到实例
class MyClass {
method() { return this; }
}
bindMethods(new MyClass());树结构工具
import { makeTree, makeFlatTree } from "@tker/shared/utils";
// 构建树结构
const items = [
{ id: 1, pid: 0, name: "Root" },
{ id: 2, pid: 1, name: "Child" },
];
makeTree(items, "id", "pid");
// [{ id: 1, pid: 0, name: "Root", children: [{ id: 2, ... }] }]
// 构建打平的树结构(带层级信息)
makeFlatTree(items, "id", "pid");
// [{ id: 1, level: 1, is_leaf: false }, { id: 2, level: 2, is_leaf: true }]类型判断
import {
isPlainObject, isArray, isString, isNumber,
isFunction, isPromise, isObject, isNil,
isEmpty, isHttpUrl, isWindow, isUndefined, isNull
} from "@tker/shared/utils";
isPlainObject({}); // true
isArray([1, 2]); // true
isString("hello"); // true
isNumber(123); // true
isFunction(() => {}); // true
isPromise(Promise.resolve()); // true
isNil(null); // true
isNil(undefined); // true
isEmpty({}); // true
isEmpty([]); // true
isHttpUrl("https://example.com"); // true文件下载
import {
downloadFileFromUrl,
downloadFileFromBase64,
downloadFileFromBlob,
downloadFileFromImageUrl,
urlToBase64
} from "@tker/shared/utils";
// 从 URL 下载
await downloadFileFromUrl({ source: "https://example.com/file.pdf", fileName: "doc.pdf" });
// 从 Base64 下载
downloadFileFromBase64({ source: "data:text/plain;base64,...", fileName: "text.txt" });
// 从 Blob 下载
downloadFileFromBlob({ source: new Blob(["content"]), fileName: "file.txt" });
// 从图片 URL 下载
await downloadFileFromImageUrl({ source: "https://example.com/image.png", fileName: "img.png" });
// URL 转 Base64
const base64 = await urlToBase64("https://example.com/image.png");窗口工具
import { openWindow, openRouteInNewWindow } from "@tker/shared/utils";
// 打开新窗口
openWindow("https://example.com", { target: "_blank", noopener: true });
// 在新窗口打开路由
openRouteInNewWindow("/users/1");全屏工具
import { fullscreen, unfullscreen } from "@tker/shared/utils";
// 进入全屏
fullscreen(document.getElementById("video"));
// 退出全屏
unfullscreen();DOM 工具
import { htmlElementClass } from "@tker/shared/utils";
// 添加/移除 CSS 类
htmlElementClass(true, "dark-mode", document.body); // 添加
htmlElementClass(false, "dark-mode", document.body); // 移除滚动工具
import { isScrollable, isElementInViewport } from "@tker/shared/utils";
// 判断是否可滚动
isScrollable(element); // true/false
// 判断元素是否在可视区域
isElementInViewport(activeElement, scrollParent);睡眠函数
import { sleep } from "@tker/shared/utils";
await sleep(1000); // 等待 1 秒对象合并
import { merge, createMerge, mergeWithArrayOverride } from "@tker/shared/utils";
// 默认合并(数组会合并)
merge({ arr: [1] }, { arr: [2] }); // { arr: [1, 2] }
// 创建自定义合并器
const customMerge = createMerge((obj, key, val) => {
// 自定义逻辑
});
// 数组覆盖合并
mergeWithArrayOverride({ arr: [1] }, { arr: [2] }); // { arr: [2] }许可证
MIT License