@wisdomgarden/torque-icons
v0.1.6
Published
Torque Design icons — the Wisdom Garden icon set — one lazily loaded ES module per icon, plus a React <Icon> that resolves them by name.
Keywords
Readme
@wisdomgarden/torque-icons
Torque Design 的图标集 —— Wisdom Garden 设计系统的一部分(另两个是
@wisdomgarden/torque 组件库与 @wisdomgarden/torque-styles 设计 token)。每个图标是一个独立的、按需加载的 ES 模块,外加一个按名字解析图标的 React <Icon>。
为什么是「一图标一模块」
图标名是运行时字符串(<Icon name={someVariable} />),打包器没法静态分析,所以 tree-shaking 帮不上忙。这个包换一条路:把 598 个图标各自编译成独立模块,再由包内的 loader 用一个前缀固定、只有名字是变量的动态 import 去取:
import(`./generated/${name}.js`);消费方的打包器见到这个形状,会对 dist/generated/ 建一个 context module,把 598 个模块各自切成一个 chunk。页面渲染哪几个图标,就只下载哪几个。
实测(Next 16 + Turbopack):598 个图标 → 598 个 chunk;一个渲染 4 个图标的页面只下载 4 个,单个图标 gzip 中位数约 0.6 KB。
对比之下,把全部图标合成一个 sprite 或一份 CSS 都是约 380 KB,不管用几个都得下完。这个形态还让"往包里加图标"对已有消费方的运行时成本是 0。
安装
pnpm add @wisdomgarden/torque-icons用法
零配置 —— 装完直接用:
import { Icon } from '@wisdomgarden/torque-icons';
<Icon name="search" />
<Icon name={`file/${upload.type}`} /> {/* 运行时名字照常工作 */}
<Icon name="common-entry/my-notes" useOriginalColor width={44} height={44} /><Icon> 内部用 React.lazy + Suspense 解析图标:组件在渲染期就拿到、实际模块按需加载。
所以图标参与服务端渲染 —— React 会在流式阶段把图标补进 HTML,不用等 hydration,
而且这条路是框架无关的(next/dynamic 本质就是这个加上 Next 特有的预加载)。
只有当图标要来自别的地方(自建 CDN、宿主自己的图标体系)时才需要注册:
import { setIconLoader } from '@wisdomgarden/torque-icons';
setIconLoader(name => fetchMyIcon(name));必须在第一个 <Icon> 渲染前调用 —— 解析结果按名字缓存。
useOriginalColor
默认会把图标里所有非 none 的 fill / stroke 改写成 currentColor,于是图标跟随父级文字颜色(暗色主题、hover、a11y 主题全部自动跟上)。
自带配色的图标必须传 useOriginalColor —— 比如「色块底 + 白色图形」这类(common-entry/*)、品牌 logo(activity/fill/google-meeting)、带渐变的图标。不传的话所有形状会被统一成一个颜色,糊成色块。
这个改写发生在渲染期而不是构建期,因为 useOriginalColor 是调用点的 prop,不是图标的固有属性 —— 这套图标里有 17 个既按原色用、又按 currentColor 用。构建期出两套变体的话模块数会翻倍,只为把一个布尔值挪出渲染路径,不划算。
改写逻辑由全部 598 个模块共享一份(color-override.tsx),所以每个图标自己的 chunk 里只有它的图形。
新增图标
- 把 svg 放进
src/svgs/(子目录即名字前缀,src/svgs/file/document.svg→"file/document") pnpm build
构建会重新生成组件模块,并把 IconName 联合类型写进 src/icon-names.ts。
API
| 导出 | 说明 |
| ---------------------------- | ----------------------------------------------- |
| Icon | 图标组件 |
| setIconLoader(loader) | 启动时注册一次 |
| getIconLoader() | 读取当前 loader |
| loadIcon(/loader 子入口) | 包内置的按需加载器 |
| iconNames | 全部图标名数组 |
| IconName | 图标名联合类型(仅用于补全,运行时接受任意字符串) |
setIconLoader 是模块级状态,所以本包必须单实例 —— 被 @wisdomgarden/torque 声明为 peerDependency 正是为此。
构建产物
| 路径 | 内容 |
| ------------------------ | ------------------------------- |
| dist/generated/**.js | 598 个图标模块,一图标一文件 |
| dist/color-override.js | 共享的颜色改写逻辑 |
| dist/loader.js | 那行动态 import |
| dist/index.js | Icon / setIconLoader / 类型 |
生成产物不带 sourcemap:598 个图标模块与 icon-names 是从 src/generated/ 编译来的,
而那个目录本身就是构建产物(gitignore),map 指向任何检出都不存在的文件 —— 连本地调试
都没价值。去掉后 tarball 从 856 KB 降到 414 KB;5 个手写模块的 map 保留,合计约 8 KB。
dist/loader.js 里的动态 import 带 @vite-ignore:本包自己的 Vite 构建不能碰它。Vite 默认会交给 @rollup/plugin-dynamic-import-vars,那个插件在构建期就把变量 import 展开成静态 map —— 在这里会得到空 map(图标是各自独立的 entry),运行时必然抛 Unknown variable dynamic import;而且它只支持一层路径,file/document 这种两级名字本来也过不去。这行必须原样送到消费方,由消费方的打包器处理。
