仅限浏览器环境的工具集:文件下载策略、存储封装、环境检测、表单元素类型与取值、props 合并、新窗口打开与 HTML class 切换
Web 子模块通过独立的子路径 @skyroc/utils/web 导出,与主入口分离,避免在非浏览器环境(Node.js、SSR、React Native)引入 BOM API。
主入口的 TS 项目 lib 只有 ESNext,没有 DOM——这类代码在那里根本编译不过,这条边界是编译期强制的。
import { downloadFileFromUrl, createStorage, openWindow, toggleHtmlClass } from '@skyroc/utils/web';| 模块 | 导出 | 说明 |
|---|---|---|
download | downloadFileFromUrl 等 7 个函数 | 多策略文件下载(本页) |
window | openWindow | 安全地在新窗口打开 URL(本页) |
class | toggleHtmlClass | 切换 <html> 元素上的 class(本页) |
storage | createStorage、createLocalforage | 类型安全的存储封装 |
env | isWindow、isMacOs、isWindowsOs、isPC | 环境与设备检测 |
form | FieldElement、CustomElement | 表单元素类型,仅类型导出(本页) |
input | isCheckBoxInput、isRadioInput、isFileInput、getEventValue | 表单元素判定与取值(本页) |
compose-props | mergeProps、withClassName | React props 合并(本页) |
下载场景的难点在于:不同来源(URL / Base64 / Blob)、不同平台(iOS / 桌面 / CORS 限制)需要不同的处理策略。download 模块将这些复杂性封装在内部,对外提供清晰的函数边界。
文件来源是什么?
├─ 普通 URL(http/https) → downloadFileFromUrl
├─ 图片 URL(需转 base64) → downloadFileFromImageUrl
├─ Base64 / DataURL → downloadFileFromBase64
├─ Blob 对象 → downloadFileFromBlob
├─ BlobPart(ArrayBuffer 等)→ downloadFileFromBlobPart
└─ 自定义 href → triggerDownload(底层)downloadFileFromUrl(options) — 异步通过 URL 下载文件,内置跨平台兼容逻辑:
openWindow(a[download] 在 iOS 上不可靠)fetch → blob → a[download](最稳定,可自定义文件名)openWindow文件名解析优先级:Content-Disposition 响应头 → 参数 fileName → URL 路径中的文件名。
import { downloadFileFromUrl } from '@skyroc/utils/web';
// 最简用法
await downloadFileFromUrl({ source: 'https://example.com/report.pdf' });
// 指定文件名
await downloadFileFromUrl({
source: 'https://example.com/export?id=123',
fileName: '月度报表.xlsx'
});
// 在当前 tab 打开(适合预览)
await downloadFileFromUrl({
source: 'https://example.com/preview.pdf',
target: '_self'
});interface DownloadOptions {
source: string; // 文件 URL
fileName?: string; // 自定义文件名(可选)
target?: string; // 回退打开窗口的 target,默认 '_blank'
}downloadFileFromBase64(options)通过 Base64 / DataURL 下载文件,同步。
import { downloadFileFromBase64 } from '@skyroc/utils/web';
downloadFileFromBase64({
source: 'data:application/pdf;base64,JVBERi0x...',
fileName: 'document.pdf'
});
// 图片(省略 mimeType 前缀也可以,但推荐带完整 DataURL)
downloadFileFromBase64({
source: 'data:image/png;base64,iVBORw0KGgo...',
fileName: 'screenshot.png'
});downloadFileFromImageUrl(options) — 异步通过图片 URL 下载图片,内部用 Canvas 将图片转为 Base64 后下载。
跨域限制: 图片服务端必须返回
Access-Control-Allow-Origin响应头,否则 Canvas 会被"污染"而抛错。
import { downloadFileFromImageUrl } from '@skyroc/utils/web';
await downloadFileFromImageUrl({
source: 'https://cdn.example.com/avatar.png',
fileName: 'my-avatar.png'
});downloadFileFromBlob(options)通过 Blob 对象下载,同步。适合服务端返回的二进制响应。
import { downloadFileFromBlob } from '@skyroc/utils/web';
// 配合 axios(responseType: 'blob')
const response = await axios.get('/api/export', { responseType: 'blob' });
downloadFileFromBlob({
source: response.data, // Blob
fileName: '数据导出.xlsx'
});downloadFileFromBlobPart(options)通过 BlobPart(string | ArrayBuffer | Uint8Array 等)下载,同步。适合需要自行构造文件内容的场景。
import { downloadFileFromBlobPart } from '@skyroc/utils/web';
const csvContent = 'name,age\nAlice,30\nBob,25';
downloadFileFromBlobPart({
source: csvContent,
fileName: 'users.csv'
});urlToBase64(url, mimeType?) — 异步将图片 URL 转为 Base64 DataURL(底层实现)。跨域图片需要服务端允许 CORS。
import { urlToBase64 } from '@skyroc/utils/web';
const base64 = await urlToBase64('https://cdn.example.com/image.png');
// 'data:image/png;base64,...'triggerDownload(href, fileName, revokeDelay?) — 底层通用下载触发函数,通过动态创建 <a> 标签并模拟点击实现下载。是其他 download* 函数的内部实现。
一般不需要直接调用,除非需要传入已有的 blob URL 或 data URL:
import { triggerDownload } from '@skyroc/utils/web';
const blobUrl = URL.createObjectURL(myBlob);
triggerDownload(blobUrl, 'file.zip');
// blob URL 会在 150ms 后自动 revokeObjectURLimport { openWindow } from '@skyroc/utils/web';在新窗口(或指定 target)打开一个 URL,默认启用 noopener,noreferrer 安全策略防止 opener 劫持。
// 新 tab 打开(默认)
openWindow('https://docs.example.com');
// 当前 tab
openWindow('/settings', { target: '_self' });
// 关闭安全策略(需要 opener 引用时)
openWindow('https://external.com', { secure: false });interface OpenWindowOptions {
target?: '_blank' | '_parent' | '_self' | '_top' | string; // 默认 '_blank'
secure?: boolean; // 默认 true,开启 noopener + noreferrer
}import { toggleHtmlClass } from '@skyroc/utils/web';切换 document.documentElement(即 <html> 标签)上的 class,常用于主题切换(暗色模式)。
const darkMode = toggleHtmlClass('dark');
darkMode.add(); // <html class="dark">
darkMode.remove(); // <html class="">在 React 中配合主题状态使用:
import { useEffect } from 'react';
import { toggleHtmlClass } from '@skyroc/utils/web';
const dark = toggleHtmlClass('dark');
const ThemeWatcher = ({ isDark }: { isDark: boolean }) => {
useEffect(() => {
isDark ? dark.add() : dark.remove();
}, [isDark]);
return null;
};import type { CustomElement, FieldElement } from '@skyroc/utils/web';FieldElement 是表单收集器能接受的元素:
type FieldElement<T = unknown> = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | CustomElement<T>;CustomElement<T = unknown> 描述第三方 / 自研控件至少要暴露的结构(value、type、checked、files、options、focus),T 是该控件额外暴露的字段。泛型默认值是 unknown 而不是 any——交叉类型里出现 any 会把整个类型塌成 any,那样 FieldElement 就完全失去约束力了。
这两个类型依赖 DOM lib,所以住在 ./web 而不是 ./type。React Native 侧不要从这里
import——@skyroc/form 的根出口刻意不再转发 FieldElement,就是为了不把 DOM 类型带进无 DOM lib 的环境。
import { isCheckBoxInput, isRadioInput, isFileInput, getEventValue } from '@skyroc/utils/web';getEventValue(valuePropName?, ...args)从受控组件的 onChange 参数里取出真实值。受控组件的第一个参数既可能是原生 / 合成事件,也可能是裸值(antd 的 Select、DatePicker 就直接给值),这个函数把两种形态统一掉:
getEventValue('value', event); // <input> → event.target.value
getEventValue('value', checkboxEvt); // <input type="checkbox"> → event.target.checked
getEventValue('value', 'hello'); // 裸值 → 'hello' 原样返回
getEventValue('checked', event); // 自定义 valuePropName取值顺序:不是事件形状 → 原样返回;没有 target → 原样返回;是 checkbox → 取 target.checked;其余取 target[valuePropName]。
| 函数 | 判定 |
|---|---|
isCheckBoxInput(el) | el.type === 'checkbox' |
isRadioInput(el) | el.type === 'radio' |
isFileInput(el) | el.type === 'file' |
三者都是类型守卫,命中后把 FieldElement 收窄为 HTMLInputElement。
import { mergeProps, withClassName } from '@skyroc/utils/web';mergeProps(slotProps, childProps)用于 asChild / Slot 模式:把外层容器的 props 合到使用者自己写的子元素上。合并规则按 prop 名分三档:
| prop | 规则 |
|---|---|
on[A-Z]* 事件处理器 | 两边都有时都调用,先子后父,返回子的返回值;只有一边时用那一边 |
style | 浅合并,子元素的同名字段胜出 |
className | 用空格拼接,两个都保留 |
| 其他 | 子元素的值胜出 |
const props = mergeProps(
{ onClick: trackClick, className: 'btn', style: { color: 'red' } },
{ onClick: submit, className: 'primary', style: { fontSize: 12 } }
);
// onClick → 先 submit 再 trackClick
// className → 'btn primary'
// style → { color: 'red', fontSize: 12 }withClassName(element, ...className)给一个 React 元素追加 class,内部走 cn,因此 Tailwind 冲突类会被正确覆盖:
withClassName(<Icon className="size-4" />, 'text-red-500');
// <Icon className="text-red-500 size-4" />注意参数顺序:元素自身的 className 排在后面,冲突时元素自己的类胜出。
Last updated on