@i.un/libs
v1.0.11
Published
一个实用的ts函数库
Readme
@i.un/libs
一个实用的 TypeScript 函数库,提供跨平台的工具函数和实用程序。
📦 安装
npm install @i.un/libs
# 或
yarn add @i.un/libs
# 或
pnpm add @i.un/libs🚀 快速开始
import { debounce, singleFlight, createElement, cloneDeep } from '@i.un/libs';
// 防抖函数
const debouncedFn = debounce(() => {
console.log('防抖执行');
}, 300);
// 单飞:并发调用只请求一次,所有调用方共享结果
const fetchUser = singleFlight((id: string) => fetch(`/api/users/${id}`).then(r => r.json()), {
key: (id) => id,
});
// 创建DOM元素
const button = createElement({
tag: 'button',
children: '点击我',
attrs: { id: 'myButton' },
styles: { color: 'red' }
});
// 深度克隆
const cloned = cloneDeep({ name: 'test', items: [1, 2, 3] });📚 功能模块
🔧 通用工具 (Common)
防抖和延迟
debounce(func, wait)- 防抖函数delayExecution(callback, delay, ...args)- 延迟执行函数,返回 Promise
并发控制
singleFlight(func, options?)- 单飞:并发调用共享同一次执行singleExecutionWrapper(func, minTime?)- 进行中则丢弃,立即返回RealUndefinedcoalesce(func, options?)- 合并尾部:执行期间的触发合并成一次补跑Mutex- 互斥锁,任务串行执行KeyedMutex- 按 key 分组的互斥锁serialize(func, options?)- 把函数包成自动排队的版本
对象操作
cloneDeep(obj)- 深度克隆对象isEqual(a, b)- 深度相等比较createDefaultObject(defaultValue)- 创建带默认值的代理对象isCloneable(obj)- 检查对象是否可克隆isPrimitive(value)- 检查值是否为原始类型
缓存
cacheWrapper(func, setting)- 为异步函数添加缓存功能generateStableUniqueKey(args)- 生成稳定的唯一键
存储
MemoryStore- 内存存储,与WebStore/ChromeStore同一套接口
其他
getGlobal()- 获取当前环境的全局对象RealUndefined- 表示「真正的未定义」的 Symbol 哨兵值
🌐 浏览器工具 (Browser)
DOM 操作
createElement(options)- 创建DOM元素createSvg(params)- 创建SVG元素createSvgFromUrl(url)- 从URL加载SVG
滚动控制
scrollToBottom(dom)- 滚动到底部scrollToBottomIfNeeded(dom)- 智能滚动到底部
拖拽
Draggable- 让元素可拖拽
存储
WebStore-localStorage/sessionStorage封装
🔌 Chrome 扩展工具 (Chrome)
存储
ChromeStore-chrome.storage.local/chrome.storage.sync封装
消息通信
createMessageBus(config?)- 类型安全的消息总线,统一 background / content script / tab 之间的通信isErrorResponse(res)- 判断消息响应是否为错误
Cookie 与扩展信息
getCookie(url, name, onlyValue?)- 读取单个 cookiegetCookies(url, stringified?)- 读取指定 URL 下的全部 cookiegetExtensionInfo()- 获取扩展的 locale、ID、版本
身份验证
launchWebAuthFlow(params)- 发起 OAuth 授权流程decodeJWT(token)- 解析 JWT 的 payload
📖 详细文档
并发控制
四种原语按「并发调用发生时怎么办」区分,选错会引入很隐蔽的 bug:
| 原语 | 并发时 | 后来者拿得到结果 | 典型场景 |
| --- | --- | --- | --- |
| singleFlight | 复用进行中的那次 | 是,同一个 Promise | 同一份数据被多处同时请求 |
| singleExecutionWrapper | 丢弃,立即返回 RealUndefined | 否 | 重复执行无意义,且要立刻脱身 |
| coalesce | 合并成一次补跑 | 否(Promise<void>) | 触发携带新状态,丢了就丢了 |
| Mutex / serialize | 排队串行 | 是 | 读-改-写共享资源 |
四者都只管「同一时刻」的并发,不做结果缓存。跨次调用要不要重复执行是缓存层的职责,请配合 cacheWrapper。
singleFlight —— 共享同一次执行
import { singleFlight } from '@i.un/libs';
const fetchProfile = singleFlight(
(id: string) => fetch(`/api/profile/${id}`).then(r => r.json()),
{ key: (id) => id },
);
// 同一个 id 并发调用只发一个请求,两个调用方拿到同一份结果
const [a, b] = await Promise.all([fetchProfile('x'), fetchProfile('x')]);
fetchProfile.isRunning('x'); // 对应槽位是否有调用在进行中
fetchProfile.size; // 进行中的槽位数量注意:一次失败会传给所有调用方;首次调用卡住时,后来者会跟着挂。高频事件驱动的场景下,需要「立刻脱身」的请用 singleExecutionWrapper。
singleExecutionWrapper —— 进行中则丢弃
import { singleExecutionWrapper, RealUndefined } from '@i.un/libs';
const submit = singleExecutionWrapper(async (form: FormData) => {
await fetch('/api/submit', { method: 'POST', body: form });
}, 800); // minTime:即使任务提前完成,也至少锁定 800ms,防连点
const result = await submit(form);
if (result === RealUndefined) {
// 上一次还没结束,本次被丢弃
}coalesce —— 合并尾部
适用于「触发意味着状态变了,而进行中那轮读到的是旧状态」。丢弃会永久丢掉新状态,共享结果同样丢,只有补跑一次才能收敛到最新状态。
import { coalesce } from '@i.un/libs';
const syncState = coalesce(async () => {
const latest = await readState();
await push(latest);
}, {
onError: (err) => console.error(err), // 返回的 Promise 永不 reject,失败走这里
});
// 首次立即执行;执行期间的多次触发合并成结束后的一次补跑
store.subscribe(() => syncState());和 debounce 正好相反:debounce 延迟首次响应,coalesce 首次立即执行、把合并放在尾部。两者可以叠加。
Mutex —— 互斥锁
读-改-写共享资源时,读和写各是一次异步调用,中间可以被别的调用插入,后写的会覆盖先写的。把整段放进同一把锁里就不会交错:
import { Mutex } from '@i.un/libs';
const storageLock = new Mutex();
const take = (id: string) => storageLock.run(async () => {
const map = await readMap();
const item = map[id];
delete map[id];
await writeMap(map);
return item;
});
const drop = (id: string) => storageLock.run(async () => {
const map = await readMap();
delete map[id];
await writeMap(map);
});
// take 与 drop 并发时也不会交错多个函数操作同一份资源时必须共用一个 Mutex 实例,各自包一层 serialize 是挡不住交错的。
只需要串行化单个函数时用 serialize;key 是动态无界的(用户 ID、tab ID 等)用 KeyedMutex,它会在队列排空后自动回收 key。
import { serialize, KeyedMutex } from '@i.un/libs';
const writeProfile = serialize(async (id: string, patch: object) => { /* ... */ }, {
key: (id) => id, // 相同 id 串行,不同 id 并行
});
const locks = new KeyedMutex();
await locks.run(userId, async () => { /* ... */ });⚠️ 被包装的函数不能通过包装后的入口调用自己:
singleFlight会拿到自己那个尚未完成的 Promise 永远等下去,Mutex会排在自己后面死锁。需要递归时把真正的逻辑抽成内部函数,只在最外层入口做包装。
防抖函数
import { debounce } from '@i.un/libs';
const debouncedSearch = debounce((query: string) => {
console.log('搜索:', query);
}, 300);
// 快速连续调用只会执行最后一次
debouncedSearch('a');
debouncedSearch('ab');
debouncedSearch('abc'); // 只有这次会执行存储
MemoryStore / WebStore / ChromeStore 是同一套接口的三种后端,都是按 storageKey 单例、带类型的键值存储:
import { ChromeStore, WebStore, MemoryStore } from '@i.un/libs';
type UserData = { email: string; loginAt: number };
// Chrome 扩展:chrome.storage.local / sync,接口全部异步
const userStore = ChromeStore.local<UserData>('user_data');
await userStore.set('email', '[email protected]');
const email = await userStore.get('email');
// 网页:localStorage / sessionStorage,接口同步
const prefs = WebStore.local<{ theme: 'light' | 'dark' }>('prefs');
prefs.set('theme', 'dark');
// 内存:不落盘,接口同步
const cache = MemoryStore.create<{ token: string }>('cache');
// 三者都支持
userStore.onChanged((changes) => { /* ... */ });
await userStore.setMultiple({ email: '[email protected]', loginAt: Date.now() });
await userStore.remove('email');
await userStore.clear();消息总线
在 background、content script、popup 之间做类型安全的通信。先定义协议表,send / on 的 payload 和 response 类型会自动推导:
import { createMessageBus } from '@i.un/libs';
type AppMessages = {
'user/get': { payload: void; response: { id: string; name: string } };
'tab/highlight': { payload: { selector: string }; response: boolean };
};
const bus = createMessageBus<AppMessages>();
// background 里注册处理器(返回取消函数)
const off = bus.on('user/get', async (_payload, sender) => {
return { id: '1', name: 'nova' };
});
// content script 里发送到 background
const user = await bus.send('user/get');
// 发送到指定 tab
const ok = await bus.sendToTab(tabId, 'tab/highlight', { selector: '.card' });
// 限定到某个 frame(如只问顶层 frame),字段与 chrome.tabs.sendMessage 的 options 一致
const alive = await bus.sendToTab({ tabId, frameId: 0 }, 'user/get');Cookie 读取
只能在扩展后台(service worker / background)或有 cookies 权限的扩展页面里调用:
import { getCookie, getCookies } from '@i.un/libs';
const token = await getCookie('https://example.com', 'session', true); // 只要值
const cookie = await getCookie('https://example.com', 'session'); // 完整 Cookie 对象
const header = await getCookies('https://example.com', true); // "a=1; b=2" 形式
const list = await getCookies('https://example.com'); // Cookie 数组DOM 元素创建
import { createElement } from '@i.un/libs';
// 创建简单元素
const div = createElement({
tag: 'div',
children: 'Hello World',
styles: { color: 'blue', fontSize: '16px' }
});
// 创建复杂嵌套元素
const card = createElement({
tag: 'div',
attrs: { class: 'card' },
children: [
{
tag: 'h2',
children: '标题',
styles: { margin: 0 }
},
{
tag: 'p',
children: '内容描述',
styles: { color: '#666' }
}
]
});SVG 创建
import { createSvg, createSvgFromUrl } from '@i.un/libs';
// 从字符串创建SVG
const svgString = `
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="red" />
</svg>
`;
const svgElement = createSvg(svgString);
// 从VNode创建SVG
const vnode = {
type: 'svg',
props: { width: '100', height: '100' },
children: [{
type: 'circle',
props: { cx: '50', cy: '50', r: '40', fill: 'red' },
children: null
}]
};
const svgFromVNode = createSvg(vnode);
// 从URL加载SVG
const svgFromUrl = await createSvgFromUrl('https://example.com/icon.svg');缓存包装器
import { cacheWrapper } from '@i.un/libs';
// 包装异步函数添加缓存
const fetchUserData = cacheWrapper(async (userId: string) => {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}, {
timeout: 10000,
timeoutResult: { error: '请求超时' },
identifier: 'userData'
});
// 第一次调用会执行请求
const user1 = await fetchUserData('123');
// 第二次调用会返回缓存结果
const user2 = await fetchUserData('123'); // 从缓存返回深度克隆
import { cloneDeep } from '@i.un/libs';
const original = {
name: 'test',
items: [1, 2, { nested: true }],
date: new Date()
};
const cloned = cloneDeep(original);
cloned.items.push(4); // 不会影响原对象滚动控制
import { scrollToBottom, scrollToBottomIfNeeded } from '@i.un/libs';
const chatContainer = document.getElementById('chat')!;
// 强制滚动到底部
scrollToBottom(chatContainer);
// 智能滚动(只在用户未手动滚动时自动滚动)
scrollToBottomIfNeeded(chatContainer);🧪 测试
npm test📝 开发
# 安装依赖
npm install
# 开发模式
npm run dev
# 构建
npm run build
# 生成文档
npm run doc📄 许可证
ISC
👨💻 作者
zhyswan
