原子组件与图标系统
Mizuki 的原子组件层(src/components/atoms/)是整个 UI 体系的最低层积木,其中图标系统(Icon System)是覆盖面最广、实现最复杂的一环:它横跨 Astro 构建时渲染、Svelte 客户端运行时渲染与 Iconify CDN 三条链路,并以统一的封装屏蔽了三条链路的 API 差异。
Purpose and Scope
本页覆盖以下内容:
- 原子组件目录结构:
src/components/atoms/下所有原子组件(Badge、Button、Chip、custom-scrollbar、filter-tabs、Icon、Image)的组织方式与"实现 +types.ts+index.ts"三件套约定。 - 图标系统端到端机制:
- 原子级
Icon.astro(带 loading 状态与 fallback 的 Web Component 封装器) LocalIcon.svelte(基于本地@iconify-json/*包的零 CDN 方案)IconLoader单例(src/utils/icon-loader.ts,带超时与重试的 Iconify 脚本加载器)- 三种标准化图标使用方式及其决策规则(源自
docs/rule/07-icon-usage-specification.md)
- 原子级
- 失败模式、超时与降级策略:MutationObserver 探测、5 秒兜底超时、3 次重试等。
以下内容有意留给兄弟页面:
- 分子级/组织级组合组件(
components.molecules/components.organisms,它们消费本页的原子组件) - 主题与样式令牌体系(Tailwind 主题配置本身)
- 图标加载插件的 Vite/Astro 集成细节(
src/plugins/astro-icon-include.mjs、IconifyLoader.astro仅在此引用其角色)
Overview
原子组件一览
src/components/atoms/ 目录下的每个原子组件都遵循同一目录约定:实现文件 + types.ts + index.ts:
| 组件 | 实现文件 | 框架 | 说明 |
|---|---|---|---|
| Badge | Badge/Badge.svelte | Svelte 5 | 徽标 |
| Button | Button/Button.astro | Astro | 按钮原子 |
| Chip | Chip/Chip.svelte | Svelte 5 | 芯片标签 |
| CustomScrollbar | custom-scrollbar/CustomScrollbar.astro | Astro | 自定义滚动条 |
| FilterTabs | filter-tabs/FilterTabs.astro | Astro | 过滤标签栏 |
| Icon | Icon/Icon.astro + Icon/LocalIcon.svelte | Astro + Svelte | 图标(本页重点) |
| Image | Image/Image.astro | Astro | 图片原子 |
框架选择的意图很明确:静态、无需交互的原子走 Astro(零 JS 输出),需要响应式状态的原子走 Svelte 5($props / $state / $effect runes)。Icon 是唯一同时存在两种实现的原子——这正是三条渲染链路并存的直接体现。
图标系统的三条链路
项目基于 Iconify 生态,规范文档明确了三种标准化使用方式,并禁止在业务代码中直接使用原生 <iconify-icon> 标签:
| # | 使用方式 | 属性名 | 导入来源 | 适用文件 | 运行时机 |
|---|---|---|---|---|---|
| ① | <Icon name="..."> | name | astro-icon/components | .astro 静态组件/页面 | 构建时 (SSR) |
| ② | <Icon icon="..."> | icon | @iconify/svelte | .svelte 客户端组件 | 客户端运行时 (CSR) |
| ③ | <Icon icon="..."> | icon | 自定义 @components/misc/Icon.astro | .astro(需 loading 状态) | 构建时 + 客户端 |
name 与 icon 的属性名差异不是笔误,而是两个库的 API 设计差异:astro-icon 模仿 Astro Image 组件的命名约定(构建时内联 SVG),而 iconify-icon / @iconify/svelte 遵循 Iconify 全生态统一规范(浏览器端按需加载图标数据)。
Architecture
架构分层意图解读:
misc/Icon.astro作为对外包装器:业务代码面向@components/misc/Icon.astro,而它内部转发到原子层atoms/Icon/Icon.astro。这样"原子实现"与"对外契约"解耦——原子组件可以重构而不破坏业务导入路径。Icon.astro是 Web Component 的薄壳:它并不自己画 SVG,而是输出<iconify-icon>自定义元素,并把全部精力放在加载状态管理(loading 指示、成功淡入、超时告警)上。LocalIcon.svelte走完全独立的本地链路:通过动态import本地@iconify-json/*包的icons.json,直接拼出 SVG 字符串,彻底绕开 CDN——为离线/无外部网络依赖场景提供能力。IconLoader是全局单例:负责把 Iconify 的 CDN 脚本以"超时 + 重试"的方式注入页面,并通过观察者模式广播就绪事件。
核心实现一:Icon.astro —— 带 loading 状态的封装器
Props 契约
1---
2import type { IconProps } from "./types";
3
4const {
5 icon,
6 class: className = "",
7 style = "",
8 size = "md",
9 color,
10 fallback = "●",
11 loading = "lazy",
12} = Astro.props as IconProps;
13---Source: Icon.astro
解构默认值即组件契约的核心:size 缺省为 "md",fallback 缺省为实心圆 "●",loading 缺省 "lazy"。这是"加载失败也有可感知占位"这一设计意图的落点。
尺寸系统:语义化 size → Tailwind text-*
1const sizeClasses = {
2 xs: "text-xs",
3 sm: "text-sm",
4 md: "text-base",
5 lg: "text-lg",
6 xl: "text-xl",
7 "2xl": "text-2xl",
8};
9
10const sizeClass = sizeClasses[size] || sizeClasses.md;
11const colorStyle = color ? `color: ${color};` : "";
12const combinedStyle = `${colorStyle}${style}`;
13const combinedClass = `${sizeClass} ${className}`.trim();
14
15const iconId = `icon-${Math.random().toString(36).substring(2, 9)}`;Source: Icon.astro
设计意图有三层:
- 用
text-*而非width/height控制尺寸:Iconify SVG 使用1em相对尺寸,font-size会自动缩放图标,且能继承父级文本色。sizeClasses[size] || sizeClasses.md的兜底表达式保证了非法 size 值不会击穿布局。 - 颜色走内联
color::SVG 默认currentColor填充,所以只需设color即可染色,无需侵入图标内部。 - 随机
iconId:同一页面会渲染大量 Icon 实例,Math.random().toString(36).substring(2, 9)生成的 7 位随机 ID 让内联<script>能用data-icon-container属性选择器精确定位到"自己这个"实例,避免实例间状态串扰。
渲染结构:双 span + 透明度过渡
1<span
2 class={`inline-flex items-center justify-center ${combinedClass}`}
3 style={combinedStyle}
4 data-icon-container={iconId}
5>
6 <span class="icon-loading animate-pulse opacity-50" data-loading-indicator>
7 {fallback}
8 </span>
9
10 <iconify-icon
11 icon={icon}
12 class="icon-content opacity-0 transition-opacity duration-200"
13 data-icon-element
14 loading={loading}></iconify-icon>
15</span>Source: Icon.astro
外层容器 min-width/height: 1em 防止图标加载期间布局塌陷(CLS)。内层两个子节点互斥显示:icon-loading 初始带 animate-pulse opacity-50 呼吸动画显示 fallback 字符;iconify-icon 初始 opacity-0,加载完成后通过 transition-opacity duration-200 淡入。
客户端加载检测:三重探测机制
1function checkIconLoaded() {
2 const hasContent =
3 iconElement.shadowRoot &&
4 iconElement.shadowRoot.children.length > 0;
5
6 if (hasContent) {
7 showIcon();
8 return true;
9 }
10 return false;
11}
12
13showLoading();
14
15iconElement.addEventListener("load", () => {
16 showIcon();
17});
18
19iconElement.addEventListener("error", () => {
20 console.warn(`Failed to load icon: ${icon}`);
21});
22
23if (window.MutationObserver) {
24 const observer = new MutationObserver(() => {
25 if (checkIconLoaded()) {
26 observer.disconnect();
27 }
28 });
29
30 observer.observe(iconElement, {
31 childList: true,
32 subtree: true,
33 attributes: true,
34 });
35
36 setTimeout(() => {
37 observer.disconnect();
38 if (!checkIconLoaded()) {
39 console.warn(`Icon load timeout: ${icon}`);
40 }
41 }, 5000);
42}
43
44setTimeout(() => {
45 checkIconLoaded();
46}, 100);Source: Icon.astro
这是整个图标系统中最精细的一段状态机,核心难点在于:iconify-icon Web Component 的内容渲染在 shadowRoot 内,且 load 事件在缓存命中时不一定可靠触发。因此实现采用了三重冗余探测:
load事件监听——正常网络路径的首选信号;MutationObserver轮询 shadowRoot——监听childList/subtree/attributes,一旦shadowRoot.children.length > 0立即判定成功并disconnect(),覆盖事件未触发的边界情况;- 5 秒兜底超时 + 100ms 即时检查——超时断开 observer 并仅打
console.warn(不抛错、不清除 fallback),保证页面不会被单个图标卡死。
该脚本通过 <script is:inline define:vars={{ iconId, icon }}> 注入,define:vars 让服务端生成的随机 ID 与图标名进入客户端闭包——这正是 Astro 中"每实例独立内联脚本"的标准做法。
核心实现二:LocalIcon.svelte —— 本地零 CDN 链路
1<script lang="ts">
2 /**
3 * Local Icon component for Svelte
4 * Uses icons from @iconify-json packages installed locally - no CDN required
5 */
6 interface Props {
7 icon: string;
8 class?: string;
9 }
10
11 const { icon, class: className = "" }: Props = $props();
12
13 const [collection, name] = $derived(
14 icon.includes(":") ? icon.split(":") : ["mdi", icon],
15 );
16
17 const iconSetMap: Record<string, string> = {
18 "material-symbols": "@iconify-json/material-symbols",
19 "material-symbols-outlined": "@iconify-json/material-symbols",
20 mdi: "@iconify-json/mdi",
21 "fa7-solid": "@iconify-json/fa7-solid",
22 "fa7-regular": "@iconify-json/fa7-regular",
23 "fa7-brands": "@iconify-json/fa7-brands",
24 "simple-icons": "@iconify-json/simple-icons",
25 };
26
27 const packageName = $derived(iconSetMap[collection]);
28 let svgContent = $state("");Source: LocalIcon.svelte
关键设计:
- 图标名解析:
icon.includes(":") ? icon.split(":") : ["mdi", icon]——支持完整写法"mdi:home",也支持省略集合名的简写(缺省落到mdi集合)。 iconSetMap白名单:把 Iconify 集合名映射到本地 npm 包名。material-symbols-outlined复用@iconify-json/material-symbols(outlined 是该包内的变体)。未登记的集合packageName为undefined,后续$effect直接短路返回——这本身是一种防呆。
动态加载与 SVG 拼装
1 $effect(() => {
2 const currentIcon = icon;
3 const currentPackageName = packageName;
4 const currentName = name;
5 const currentClassName = className;
6
7 if (!currentPackageName) {
8 return;
9 }
10
11 async function loadIcon() {
12 try {
13 const iconsData = await import(
14 /* @vite-ignore */ `${currentPackageName}/icons.json`
15 );
16 const icons = iconsData.icons || {};
17 const iconData = icons[currentName];
18
19 if (iconData) {
20 const viewBox = iconData.viewBox || "0 0 24 24";
21 const body = iconData.body;
22
23 if (body) {
24 svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" class="${currentClassName}">${body}</svg>`;
25 }
26 }
27 } catch (e) {
28 console.warn(
29 `Failed to load icon ${currentIcon} from ${currentPackageName}:`,
30 e,
31 );
32 }
33 }
34
35 loadIcon();
36 });
37</script>
38
39{#if svgContent}
40 {@html svgContent}
41{:else}
42 <span class={className}>●</span>
43{/if}Source: LocalIcon.svelte
逐行意图:
$effect内先把所有响应式值快照到局部常量(currentIcon等),是 Svelte 5 中避免依赖追踪范围外泄漏的惯用写法——动态import()的模板字符串如果直接引用响应式变量,会扩大依赖收集范围。/* @vite-ignore */禁止 Vite 对该动态导入做静态分析/预打包,让包名完全由运行时变量决定。viewBox || "0 0 24 24":icons.json中部分图标不带 viewBox,24×24 是 Material Design / mdi 的默认坐标系。- 渲染端
{#if svgContent}与<span>●</span>分支构成了与Icon.astro一致的"成功/占位"语义——整个图标系统的降级哲学是永不抛错、永远有占位。 - 失败路径仅
console.warn,组件停留在●占位态。
核心实现三:IconLoader 单例 —— 带重试的 CDN 脚本加载器
src/utils/icon-loader.ts 是一个典型的单例 + Promise 去重 + 重试组合,负责把 Iconify 官方 Web Component 脚本可靠地注入页面:
1interface IconifyLoadOptions {
2 timeout?: number;
3 retryCount?: number;
4 retryDelay?: number;
5}
6
7class IconLoader {
8 private static instance: IconLoader;
9 private isLoaded = false;
10 private isLoading = false;
11 private loadPromise: Promise<void> | null = null;
12 private observers = new Set<() => void>();
13
14 private constructor() {}
15
16 static getInstance(): IconLoader {
17 if (!IconLoader.instance) {
18 IconLoader.instance = new IconLoader();
19 }
20 return IconLoader.instance;
21 }
22
23 /**
24 * 加载Iconify图标库
25 */
26 async loadIconify(options: IconifyLoadOptions = {}): Promise<void> {
27 const { timeout = 10000, retryCount = 3, retryDelay = 1000 } = options;
28
29 // 如果已经加载完成,直接返回
30 if (this.isLoaded) {
31 return Promise.resolve();
32 }
33
34 // 如果正在加载,返回现有的Promise
35 if (this.isLoading && this.loadPromise) {
36 return this.loadPromise;
37 }
38
39 this.isLoading = true;
40 this.loadPromise = this.loadWithRetry(timeout, retryCount, retryDelay);
41
42 try {
43 await this.loadPromise;
44 this.isLoaded = true;
45 this.notifyObservers();
46 } catch (error) {
47 console.error("Failed to load Iconify after all retries:", error);
48 throw error;
49 } finally {
50 this.isLoading = false;
51 }
52 }Source: icon-loader.ts
并发语义是这个类最重要的设计点,loadIconify 的三段分支构成一个幂等状态机:
- 已完成(
isLoaded)→ 立即 resolve,零开销; - 进行中(
isLoading && loadPromise)→ 返回同一个 Promise 引用,而非发起第二次网络请求。这是"Promise 去重"模式:即使页面上几十个图标同时触发加载,网络层也只会有一次脚本注入; - 未开始 → 置位
isLoading,进入loadWithRetry。
finally { this.isLoading = false; } 保证失败后状态复位,下一次调用可以重新尝试;而成功时 isLoaded = true + notifyObservers() 让所有等待方一次性收到广播。
重试与超时算法
1 private async loadWithRetry(
2 timeout: number,
3 retryCount: number,
4 retryDelay: number,
5 ): Promise<void> {
6 for (let attempt = 1; attempt <= retryCount; attempt++) {
7 try {
8 await this.loadScript(timeout);
9 return;
10 } catch (error) {
11 console.warn(`Iconify load attempt ${attempt} failed:`, error);
12
13 if (attempt === retryCount) {
14 throw new Error(
15 `Failed to load Iconify after ${retryCount} attempts`,
16 );
17 }
18
19 // 等待后重试
20 await new Promise((resolve) => setTimeout(resolve, retryDelay));
21 }
22 }
23 }
24
25 private loadScript(timeout: number): Promise<void> {
26 return new Promise((resolve, reject) => {
27 // 检查是否已经存在脚本
28 const existingScript = document.querySelector(
29 'script[src*="iconify-icon"]',
30 );
31 if (existingScript) {
32 // 检查Iconify是否已经可用
33 if (this.isIconifyReady()) {
34 resolve();
35 return;
36 }
37 }
38
39 const script = document.createElement("script");
40 script.src =
41 "https://code.iconify.design/iconify-icon/3-latest/iconify-icon.min.js";
42 script.async = true;
43 script.defer = true;
44
45 const timeoutId = setTimeout(() => {
46 script.remove();
47 reject(new Error("Iconify script load timeout"));Source: icon-loader.ts
- 默认参数:
timeout = 10000(单次脚本 10 秒)、retryCount = 3(最多 3 次)、retryDelay = 1000(间隔 1 秒)——最坏情况约 33 秒后放弃。 - 幂等注入:
document.querySelector('script[src*="iconify-icon"]')先探测 DOM 中是否已有脚本,配合isIconifyReady()检查全局对象,避免 SPA 场景下重复插入<script>。 - 固定延迟而非指数退避:
retryDelay为常量等待,实现简单,符合"CDN 偶发抖动"的故障模型。
核心流程:一次图标渲染的完整生命周期
流程要点:
- 构建时(SSR):
Icon.astro输出确定性的 HTML 结构与内联脚本,data-*属性 + 随机iconId构成客户端定位锚点。 - 客户端:三重探测(事件 / MutationObserver / 定时器)中任意一个先命中即触发
showIcon();showIcon()与showLoading()是纯互斥的一对状态翻转函数(display+opacity-*类切换)。 - 降级终点统一:无论走哪条失败路径,UI 都停在 fallback
●,且只产生console.warn——图标缺失被视为降级而非错误。
使用示例
方式①:.astro 静态图标(构建时内联)
1---
2import { Icon } from "astro-icon/components";
3---
4
5<Icon name="material-symbols:arrow-back" class="text-base" />
6<Icon name={dynamicIconName} class="text-xl" />Source: 07-icon-usage-specification.md
方式②:.svelte 响应式图标
1<script lang="ts">
2 import Icon from "@iconify/svelte";
3</script>
4
5<Icon icon="material-symbols:pause" class="text-xl" />
6<Icon icon={dynamicIcon} class="text-lg" />Source: 07-icon-usage-specification.md
方式③:带 loading/fallback 的自定义 Icon
1---
2import Icon from "../../misc/Icon.astro";
3---
4
5<Icon icon="mdi:react" size="lg" color="#61DAFB" fallback="⚛" />Source: 07-icon-usage-specification.md
决策规则
规范文档给出了明确的分支决策(原文为 ASCII 流程图,此处归纳):
- 文件是
.svelte→ 一律使用方式②(@iconify/svelte,icon属性); - 文件是
.astro且需要 loading/fallback 状态 → 使用方式③(@components/misc/Icon.astro); - 文件是
.astro且为常规静态图标 → 使用方式①(astro-icon/components,name属性)。
反模式(规范明令禁止):在业务代码中直接写原生 <iconify-icon icon="..." width="16" />;混用 name/icon 属性(@iconify/svelte 不识别 name)。
Configuration Options
Icon.astro Props(方式③)
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
icon | string | —(必填) | Iconify 图标全名,如 "mdi:react" |
class | string | "" | 追加到容器的额外类名(与尺寸类合并) |
style | string | "" | 追加到容器的内联样式(与 color 样式拼接) |
size | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "md" | 语义化尺寸,映射为 Tailwind text-*;非法值兜底为 md |
color | string | — | CSS 颜色值,编译为 color: ...(SVG 以 currentColor 填充) |
fallback | string | "●" | 加载期间/失败时显示的占位字符 |
loading | "lazy" | "eager" | "lazy" | 透传给 <iconify-icon> 的加载策略 |
LocalIcon.svelte Props
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
icon | string | —(必填) | 图标名;含 : 时按 集合:名称 解析,否则默认 mdi 集合 |
class | string | "" | 直接写入生成的 <svg> 标签的 class |
IconLoader.loadIconify(options) 选项
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
timeout | number | 10000 | 单次脚本注入的超时毫秒数,超时移除 script 并 reject |
retryCount | number | 3 | 最大尝试次数,耗尽后抛 Failed to load Iconify after N attempts |
retryDelay | number | 1000 | 每次重试之间的固定等待毫秒数 |
尺寸对照速查(原生属性 → Tailwind 类)
| 原生写法 | 替代 CSS 类 | 场景 |
|---|---|---|
width="10" height="10" | w-2.5 h-2.5 text-[0.625rem] | 最小图标(节点等) |
width="14" height="14" | w-3.5 h-3.5 text-xs | 元信息图标(日期、位置) |
width="16" height="16" | w-4 h-4 text-base | 行内小图标(标签、按钮内) |
width="20" height="20" | text-lg | 卡片头部图标 |
width="48" height="48" | text-5xl | 占位大图标 |
width="64" height="64" | text-6xl | 空状态占位图标 |
规范推荐优先使用 Tailwind
text-*/w-* h-*类控制大小,而非原生width/height。
API Reference
Icon.astro(Astro 组件,方式③实现体)
位置:src/components/atoms/Icon/Icon.astro
Props:见上一节表格;类型契约定义于 src/components/atoms/Icon/types.ts(IconProps)。
输出:外层 <span data-icon-container> + 内层 loading <span> + <iconify-icon>,附每实例内联脚本。
checkIconLoaded(): boolean— 内联函数:检查shadowRoot.children.length > 0,命中则切换到已加载态并返回true。showIcon(): void— 隐藏 loading 指示器,icon-content从opacity-0切到opacity-100。showLoading(): void— 反向切换,回到 loading 态。
LocalIcon.svelte(Svelte 5 组件)
位置:src/components/atoms/Icon/LocalIcon.svelte
导出 Props:icon: string、class?: string。
渲染输出:成功时 {@html svgContent} 内联 SVG;未就绪/失败时 <span class={className}>●</span>。
副作用:$effect 中动态 import("@iconify-json/<集合>/icons.json"),仅在 iconSetMap 含该集合时执行。
IconLoader(src/utils/icon-loader.ts)
IconLoader.getInstance(): IconLoader— 私有构造函数 + 静态实例的懒汉式单例入口。loadIconify(options?: IconifyLoadOptions): Promise<void>— 幂等加载 Iconify 脚本;已完成立即 resolve,进行中复用同一 Promise;成功后置isLoaded并notifyObservers();重试耗尽后 throwError("Failed to load Iconify after N attempts")。loadWithRetry(timeout, retryCount, retryDelay): Promise<void>(private)— 顺序重试循环,末次失败抛聚合错误。loadScript(timeout): Promise<void>(private)— DOM 注入https://code.iconify.design/iconify-icon/3-latest/iconify-icon.min.js,先探测既有 script 与全局就绪状态;超时移除节点并 reject。
Failure Modes, Edge Cases & Concurrency
| 场景 | 机制 | 结果 |
|---|---|---|
| CDN 图标加载失败 | <iconify-icon> error 事件 | console.warn("Failed to load icon: ..."),保持 ● 占位,不抛异常 |
CDN 响应但 load 事件未触发 | MutationObserver 监听 shadowRoot childList | 观察到内容即切换已加载态并 disconnect() |
| 加载超过 5 秒 | 5s setTimeout 兜底 | 断开 observer,仅 console.warn("Icon load timeout: ...") |
非法 size 值 | sizeClasses[size] || sizeClasses.md | 兜底为 md,不击穿布局 |
LocalIcon 收到未登记集合 | packageName 为 undefined,$effect 提前 return | 组件停在 ● 占位 |
LocalIcon 动态 import 失败 | catch (e) → console.warn | 停在 ● 占位 |
| 多个图标并发触发脚本加载 | loadIconify 返回同一 Promise 引用 | 网络层仅一次脚本注入(Promise 去重) |
loadIconify 首次失败 | finally { isLoading = false } | 状态复位,下次调用可重试 |
| 同页多 Icon 实例 | 随机 iconId + data-icon-container 选择器 | 各实例脚本只操作自己的 DOM,互不串扰 |
| 图标加载期间布局 | 容器 min-width/height: 1em | 占位与最终图标等基线尺寸,避免 CLS |
并发与一致性要点:Icon.astro 每实例独立脚本意味着 N 个图标就是 N 个 MutationObserver——代码通过"命中即 disconnect() + 5 秒绝对上限"约束了观察器的生命周期,避免泄漏。IconLoader 的 observers: Set<() => void> 支持多个等待方订阅同一就绪事件,配合单例语义保证全局只有一个加载权威源。
Performance & Operational Notes
- 构建时 vs 运行时的取舍:方式①(astro-icon)在构建期内联 SVG、零网络请求,是性能最优解,因此规范将其设为
.astro的默认选择;方式③仅在确需 loading/fallback 时使用——这是"按需付出运行时成本"的明确设计取向。 LocalIcon.svelte的零 CDN 特性:@vite-ignore动态 import 让图标数据随本地包分发,适合对第三方 CDN 不可达敏感的部署环境;代价是icons.json体积由打包器按需切分承担。- CDN 版本钉扎方式:脚本 URL 使用
3-latest(主版本 latest),获得补丁级自动更新同时锁定大版本 API 兼容。 - 可观测性:所有失败路径均走
console.warn(除loadIconify最终失败用console.error并 throw),便于在浏览器控制台直接定位是哪一个图标名出错——icon变量被闭包捕获进告警消息。
Extension Points
- 新增图标集合(本地链路):在
LocalIcon.svelte的iconSetMap中登记"集合名": "@iconify-json/集合名",并安装对应 npm 包即可,无需改动其他代码。 - 新增尺寸档位:扩展
Icon.astro的sizeClasses映射表(如"3xl": "text-3xl")。 - 更换/自托管 CDN:
icon-loader.ts中脚本 URL 为单一注入点;配合src/constants/icon.ts(图标相关常量)可集中调整。 - 新增原子组件:遵循目录三件套约定——实现文件 +
types.ts(Props 契约)+index.ts(桶导出),框架选择按"是否需要响应式状态"决定 Astro 或 Svelte。
Tests
本页未在仓库中定位到针对图标原子的独立测试文件(源码探索预算内未发现 *.test.* / *.spec.* 匹配项)。规范文档 docs/rule/07-icon-usage-specification.md 承担了"用法契约测试"的角色:其中列出已修复的历史错误(属性名混用 name/icon、直接使用原生 <iconify-icon> 标签),可作为回归检查清单。
Related Links
- 图标使用规范(Icon Usage Specification) — 三种使用方式、决策流程与反模式的权威定义
- Icon.astro — 方式③原子实现
- LocalIcon.svelte — 本地零 CDN 图标组件
- icon-loader.ts — IconLoader 单例加载器
- misc/Icon.astro — 对外统一包装器
- IconifyLoader.astro — CDN 脚本注入组件
- astro-icon-include.mjs — astro-icon 构建集成插件
- constants/icon.ts — 图标常量