图标是现代 Web 界面中不可或缺的部分。它们简化了导航、阐明了功能并增强了视觉吸引力。然而,高效地实现图标涉及诸如可扩展性、动态加载以及服务端渲染(SSR)兼容性等挑战。
为了应对这些挑战,我们开发了 Nuxt Icon v1 — 一个专为 Nuxt 项目量身定制的多功能、现代化的解决方案。通过建立在成熟的图标渲染技术之上并引入新颖的方法,Nuxt Icon 在性能、易用性和灵活性之间架起了桥梁。
在这篇文章中,我们将探讨图标渲染的挑战、图标解决方案的演变,以及 Nuxt Icon 如何结合这些方法的优点,为开发者提供无缝的体验。
为什么图标具有挑战性?
乍一看,图标似乎很简单——它们本质上只是增强用户界面的微小图像元素,提供视觉提示并增强可用性。
![]()
然而,从工程角度来看,它们带来了几个挑战。理想的图标应该是:
- 可着色:能够适应主题和配色方案。
- 可缩放:在各种尺寸和分辨率下都能清晰渲染。
- 易管理:图标集可能包含数百或数千个图标。
- 高效打包:最小化网络请求。
- 优化加载:影响应用性能和用户体验。
- 动态性:支持为用户生成或运行时定义的图标进行动态加载。
![]()
满足所有这些需求需要一个经过精心设计、权衡利弊的解决方案。让我们探讨图标解决方案的演变以及它们是如何应对这些挑战的。
图标解决方案的发展历程
多年来,开发人员尝试了各种技术来高效渲染图标。让我们探索这些解决方案的演变及其面临的挑战。
1. <img> 标签:早期阶段
最直接的解决方案:使用 <img> 标签。这是早期 Web 开发中的首选方法。
你需要托管图像资源,并使用 <img> 标签链接到该图像,同时指定其宽度和高度。它很简单,不需要任何设置或运行时依赖项,并且在浏览器中原生支持。
![]()
然而,它也有缺点。图像可能会变得像素化、缺乏颜色控制,并且缩放效果不佳。每个图标都是一个独立的图像文件,这会导致大量的网络请求,速度可能会很慢,特别是在 HTTP 1.1 时代。在图像下载完成之前,你可能会看到不可见图标的闪烁,这会损害用户体验。最后,编写起来相当冗长,因为你需要指定图像的完整路径并管理相对路径。这就解释了为什么这种方法在当今的现代网站上很少使用。
2. Web 字体:图标字体
作为图标演变的下一步,Web 字体成为了一种流行的解决方案。字体本身就是矢量化且可着色的,这使其成为图标的天然契合点。
图标集提供商通常将他们的图标编译成一个特殊的字体文件,为每个图标分配一个唯一的 Unicode 字符。伴随而来的是一个 CSS 文件,它将这些 Unicode 值映射到特定的图标类。
这种方法的优点很明显:易于使用、可着色、可缩放,并且只需单个请求即可加载所有图标。
![]()
然而,也有一些缺点。预先加载大型字体文件可能会很慢,而且自定义图标集具有挑战性。此外,在字体加载之前,你可能会经历不可见图标的闪烁,因为没有可用的回退字体。
3. 内联 SVG:基于组件的图标
随着现代前端 frameworks(框架)的出现,重用 HTML 元素变得容易多了。这引出了将 SVG 标签直接作为组件内联的想法。
为了支持这种方法,许多图标集提供了针对每个框架量身定制的包装器包。例如,MDI 图标使用共享组件并通过 props 传递图标数据,而 Tabler 图标则为每个图标提供专用的组件。
由于这些是 SVG,它们本身就是可着色、可缩放的,并保留了 SVG 的所有功能。通常,图标会被打包到应用程序中,消除了额外的网络请求,并确保它们对 SSR 友好且在首次渲染时可见。
![]()
然而,这种方法也有缺点。它会生成大量的 SVG DOM 元素,当使用许多图标时会影响性能。它还会增加打包体积,并且需要对每个图标集和框架组合提供特定的集成支持,从而导致一定程度的厂商锁定。这使得切换到不同的图标集或框架变得具有挑战性。
尽管存在这些权衡,但这种方法在今天得到了广泛采用,因为对于大多数项目来说,切换图标集或框架并不是经常需要的事情。
4. Iconify 运行时:动态 API 访问
Iconify 聚合了 100 多个集合中的 200,000 多个图标,彻底革新了图标的使用方式。它的运行时解决方案通过 API 动态获取图标,无需预先打包即可动态访问任何图标。
这非常适合渲染来自用户提供的内容或其他在构建时未知的动态内容的图标。而且它非常容易设置,你甚至可以将其用作 CDN 而无需任何构建工具。
![]()
虽然这种方法提供了极大的灵活性,但也带来了一些权衡。它引入了运行时依赖项,这意味着图标只有在加载 JavaScript 并获取到图标数据后才会渲染。这种方法也对服务端渲染(SSR)和缓存层(如渐进式 Web 应用(PWA)中使用的缓存层)构成了挑战。
5. 按需组件图标
借助 Iconify 的统一接口和 Vite 的按需方法,我们开发了 unplugin-icons。此工具允许你按需将任何图标作为组件导入。
作为一种 unplugin,它支持所有主流构建工具,包括 Vite、webpack 和 rspack。我们为 Vue、React、Svelte 和 Solid 等热门框架提供了编译器。借助 Iconify,你可以在任何框架中使用任何图标,从而最大限度地减少厂商锁定。
![]()
虽然这项技术具有与以前的组件图标解决方案相同的优缺点,但与构建工具的集成使我们能够提供完整的 Iconify 图标集,同时只分发你实际使用的图标。然而,诸如 DOM 元素管理之类的运行时问题仍然存在。
6. 纯 CSS 图标
作为开发 UnoCSS 的副产品,我们发现了将图标完全嵌入 CSS 中的潜力,从而催生出了 纯 CSS 图标这一创新解决方案。
该方法将 SVG 图标内联为 data URL,并提供单个类来显示图标。经过一些调整,这些图标可以着色、缩放,甚至能够显示 SVG 动画。
浏览器可以缓存 CSS 规则,并且每个图标只需要渲染 一个 DOM 元素。这种方法将图标打包在一个 CSS 文件中,无需额外的请求。由于它是纯 CSS,图标会与界面的其余部分一起显示,零运行时需求,并且可以与 SSR 自然协作——你的服务器在服务端不需要做任何额外的工作。
![]()
唯一的缺点是无法完全自定义 SVG 内部的元素,并且需要在构建时打包图标,这不够动态。
集成到 Nuxt 中的挑战
虽然我要说纯 CSS 图标等等按需组件图标对于大多数静态用法来说已经足够了,但 Nuxt 作为功能齐全的框架,对高效集成图标提出了更多要求:
- SSR/CSR:Nuxt 同时支持服务端渲染(SSR)和客户端渲染(CSR)模式。我们非常关心最终用户体验,并希望确保图标能够瞬间渲染而不会闪烁。
- 动态图标:在诸如 Nuxt Content 之类的集成中,内容可以在运行时或从外部来源提供,这些是我们无法在构建时预知的。我们希望确保我们有能力很好地与这些场景集成。
- 性能:我们希望确保图标被高效打包,并且图标的加载经过优化以达到最佳性能。
- 自定义图标:虽然 Iconify 提供了广泛的图标供选择,但我们也知道项目拥有自己的图标集,或者想要使用 Iconify 中没有的付费图标是很常见的。支持自定义图标对我们的用户至关重要。
![]()
考虑到这些需求,让我们重新审视前面讨论的解决方案,看看它们的表现如何。
对于动态图标,Iconify 运行时脱颖而出,成为一个可行的选择。它允许动态获取图标,使其适用于在构建时未知的内容。然而,它有其缺点。对运行时依赖项的依赖意味着它无法与 SSR 无缝集成,并且由于请求指向无法访问我们本地图标设置的 Iconify 服务器,它不支持自定义图标。
相反,纯 CSS 图标提供了出色的性能和 SSR 兼容性。它们确保图标能够瞬间渲染而不会闪烁,并且打包效率高。然而,在处理动态图标时,它们显得力不从心,因为它们需要在构建时打包,并且缺乏适应运行时内容变化的灵活性。
平衡这些权衡确实具有挑战性。那么,为什么不结合这两种方法的优势呢?通过理解这些权衡,我们可以更好地理解 Nuxt Icon v1 所提供的平衡解决方案。
推出 Nuxt Icon v1:兼具两者之美
借助 Nuxt 模块系统的灵活性,Nuxt Icon 结合了两种方法的优点:CSS 图标的即时渲染以及 Iconify 图标的动态获取。这种双重方法提供了一个多功能、现代且可定制的图标解决方案,能够无缝适应你的项目需求。
双重渲染模式
为了解决渲染方法中的权衡,Nuxt Icon 引入了一个通用的 <Icon> 组件,该组件同时支持 CSS 和 SVG 模式,且两者都对 SSR 友好。根据你的定制需求,你可以为每个图标在这些模式之间切换。
在 CSS 模式下,图标在 SSR 期间包含在 CSS 中,确保它们即时渲染而没有任何运行时成本。在 SVG 模式下,图标在 SSR 期间内联为 HTML,提供相同的即时渲染优势。这两种方法都确保图标在初始屏幕上毫不延迟地出现,从而提供无缝的用户体验。
![]()
图标包(Icon Bundles)
动态图标带来了独特的挑战,特别是在高效加载它们方面。为了解决这个问题,我们利用了 Iconify 的 API,它允许我们通过网络请求按需提供任何图标。然而,仅依赖此 API 可能会引入延迟,特别是当服务器在地理位置上距离你的用户较远时。
为了缓解这种情况,我们引入了图标包(Icon Bundles)的概念。我们可以将经常使用的图标直接打包到 Client Bundle 中。这确保了这些图标无需额外的网络请求即可即时渲染。然而,由于可能导致打包体积增大,打包所有可能的图标是不可行的。
鉴于 Nuxt 是一个全栈框架,我们可以通过引入 Server Bundle 来实现平衡。在服务端,打包体积不是主要问题,这允许我们包含更广泛的图标集。在 SSR 期间,可以根据需要快速获取这些图标并将其发送给客户端。这种设置既确保了常用图标的高性能,又提供了将 Iconify 中的任何图标作为回退来提供的灵活性。
通过将用于静态图标的客户端打包和用于动态图标的服务端打包结合起来,我们在性能和灵活性之间实现了最佳平衡。
![]()
数据流
以下是一个数据流图,说明了 Nuxt Icon 如何请求图标数据
- 你使用
<Icon>组件并提供图标的name。 - Nuxt Icon 会首先检查该图标是否在
Client Bundle或 SSR 负载中可用(在 SSR 时已知的图标将出现在负载中)。如果是,图标将立即渲染。 - 如果图标在客户端不可用,Nuxt Icon 将从随你的 Nuxt 应用一起发布的服务器 API 获取图标数据。在服务端点内部,它将从
Server Bundle中查询该图标是否可用。 - 在此过程中,涉及多个缓存系统。服务端点缓存、HTTP 缓存和客户端缓存,以确保高效、快速地获取图标。由于图标数据不经常更改,我们使用硬缓存策略来确保最佳性能。
- 当图标对客户端和服务端都未知时(动态图标),服务端点将回退到 Iconify API 以获取图标数据。由于服务端点已被缓存,无论有多少客户端请求它,每个唯一的图标都只会调用一次 Iconify API,从而节省双方的资源。
![]()
这种分层方法确保了高效的图标分发,在速度和灵活性之间取得了平衡,同时尽可能保持动态。并且平衡了各个解决方案之间的权衡。
立即试用 Nuxt Icon
Nuxt Icon v1 代表了图标渲染领域多年创新的集大成者。无论你是在构建动态应用、静态网站还是介于两者之间的任何内容,Nuxt Icon 都能适应你的需求。
通过运行以下命令,可以轻松将 Nuxt Icon 添加到你的项目中
npx nuxi module add icon
然后,在你的 Vue 组件中导入 <Icon> 组件,并按照 Iconify 的规范提供图标 name
<template>
<Icon name="i-lucide-activity" />
</template>
通过文档探索更多内容,体验其各项功能,并让我们知道你的想法。我们很高兴看到 Nuxt Icon 如何改变你的项目!
祝您 Nuxting 愉快 ✨