@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());             // true

API

| 方法 | 参数 | 说明 | |------|------|------| | 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