默认入口
ts
import { /* ... */ } from '@tripo3d/utils';默认入口聚合了浏览器侧的通用工具:设备检测、Promise 工具、跳转/下载、文件名/MIME、文件读取、localStorage 包装、事件总线。
设备检测
isMobileDevice
| 函数 | 签名 | 说明 |
|---|---|---|
isMobileDevice | (ua: string | null) => boolean | 识别 UA 是否为移动端浏览器(android / webos / iphone / ipod / blackberry / iemobile / opera mini) |
ts
import { isMobileDevice } from '@tripo3d/utils';
if (isMobileDevice(navigator.userAgent)) {
// 走移动端路径
}为什么要传参
显式传入 UA 而不是内部读 navigator,便于 SSR 场景下从请求 header 注入。空值返回 false,不会抛错。
Promise 工具
sleep
| 函数 | 签名 | 说明 |
|---|---|---|
sleep | (ms: number) => Promise<void> | 延迟 N 毫秒解决 Promise,常用于节流、动画衔接 |
ts
import { sleep } from '@tripo3d/utils';
await sleep(300);
console.log('300ms 后执行');pf(promise factory)
外部解构 resolve / reject 的 Promise 工厂——适合把"等用户操作完成"封成 Promise(如等待弹窗确认、等待 OAuth 回调)。
| 函数 | 签名 | 说明 |
|---|---|---|
pf<T, U = any> | () => { promise: Promise<T>, resolve: (v) => void, reject: (e) => void } | 返回 promise 与外部可调用的 resolve/reject |
ts
import { pf } from '@tripo3d/utils';
function waitForLogin() {
const { promise, resolve } = pf<string>();
// 在某个事件回调里调 resolve('token')
document.addEventListener('login-done', e => resolve(e.detail), { once: true });
return promise;
}
const token = await waitForLogin();跳转 / 下载
jump 和 download 都是基于动态 <a> 元素 + .click() 实现,无需手动管理 DOM。
| 函数 | 签名 | 说明 |
|---|---|---|
jump | (url: string, blank = true) => void | 导航到 URL,默认在新标签页打开。blank=false 替换当前页 |
download | (url: string, name?: string) => void | 触发浏览器下载。name 设置文件名(需同源或服务端配置 Content-Disposition) |
ts
import { download, jump } from '@tripo3d/utils';
jump('https://tripo3d.ai'); // 新标签页打开
jump('/dashboard', false); // 当前页跳转
download('https://example.com/file.zip'); // 下载,文件名由响应头决定
download('https://example.com/file.zip', 'my.zip'); // 下载并重命名(需同源)跨源下载文件名
浏览器只在同源或服务端 Content-Disposition 配合时才会接受 download 属性的文件名。跨源 CDN 上的文件,用户最终拿到的可能是原始文件名。
文件名 / MIME
getSuffix
| 函数 | 签名 | 说明 |
|---|---|---|
getSuffix | (file: File | string) => string | undefined | 提取文件扩展名(小写)。传 File 对象时,扩展名缺失会回退到 MIME 后段 |
ts
import { getSuffix } from '@tripo3d/utils';
getSuffix('https://cdn.example.com/x/a.PNG'); // 'png'
getSuffix(new File([], 'photo.JPEG')); // 'jpeg'
getSuffix(new File([], 'no-extension', { type: 'image/webp' })); // 'webp'(回退到 MIME)readFileAsDataURL
| 函数 | 签名 | 说明 |
|---|---|---|
readFileAsDataURL | (file: File) => Promise<string> | FileReader 包装,把 File 读成 base64 data URL。失败时 reject |
ts
import { readFileAsDataURL } from '@tripo3d/utils';
const dataUrl = await readFileAsDataURL(file); // 'data:image/png;base64,...'
img.src = dataUrl;localStorage 包装
带 JSON 序列化的 localStorage。仅在浏览器环境可用,SSR 时直接调用会抛 localStorage is not defined。
| 函数 | 签名 | 说明 |
|---|---|---|
getStorage<T> | (key: string) => T | null | JSON.parse 失败或 key 不存在返回 null。不会抛错 |
setStorage | (key: string, value: unknown) => void | JSON.stringify 后写入。值过大会触发 QuotaExceededError(未捕获) |
removeStorage | (key: string) => void | 删除 key |
ts
import { getStorage, removeStorage, setStorage } from '@tripo3d/utils';
interface UserPref {
theme: 'light' | 'dark';
lang: string;
}
setStorage('user-pref', { theme: 'dark', lang: 'zh-CN' });
const pref = getStorage<UserPref>('user-pref');
if (pref) console.log(pref.theme);
removeStorage('user-pref');事件总线
通用 pub/sub,无 DOM / 框架依赖,可被 Node、Worker、浏览器统一消费。
| 函数 | 签名 | 说明 |
|---|---|---|
createEventBus<TEventMap> | () => EventBus<TEventMap> | 创建事件总线。泛型可选,传入事件映射后 on / once / off / emit 都受类型约束;不传退化为 string + unknown |
EventBus API
| 方法 | 签名 | 说明 |
|---|---|---|
on | (type, handler) => () => void | 订阅事件,返回解绑函数 |
once | (type, handler) => () => void | 订阅一次性事件,首次触发后自动解绑;返回值可在触发前提前取消 |
off | (type, handler) => void | 手动解绑;通道无订阅者时自动清理键 |
emit | (type, payload) => void | 广播载荷。emit 期间复制 handler 集合迭代,handler 内部调用 off / clear 不影响本次派发 |
has | (type, handler?) => boolean | 省略 handler 时检查通道是否有订阅者;传入 handler 时检查该 handler 是否注册 |
clear | () => void | 清空所有订阅 |
ts
import { createEventBus } from '@tripo3d/utils';
// 无泛型:通道为 string,载荷为 unknown。
const bus = createEventBus();
const off = bus.on('login', (user) => console.log(user));
bus.emit('login', { id: 1 });
off();
// 带泛型:on / emit / off 全部受类型约束。
interface AppEvents {
'cart:add': { sku: string; qty: number };
'cart:clear': void;
}
const app = createEventBus<AppEvents>();
app.on('cart:add', payload => console.log(payload.sku)); // payload 被推断为 { sku, qty }
app.emit('cart:add', { sku: 'A1', qty: 2 });
// once + has。
app.once('cart:clear', () => console.log('cleared once'));
app.has('cart:clear'); // true
app.emit('cart:clear', undefined);
app.has('cart:clear'); // false (once 已自动解绑)once 的解绑语义
once(type, handler) 内部包装了一层 wrapper,所以 off(type, originalHandler) 无法匹配 once 注册的项。用 once 返回的解绑函数取消才是合法路径。has(type, originalHandler) 同理只匹配 on 注册的 handler。
下一步
- Date — dayjs 包装与时区
- Resource — AWS S3 上传
- Playground — 工具函数交互测试 + Tripo 图标集浏览