- 前端
【免费下载链接】BewlyCat
BewlyCat——基于BewlyBewly开发的Bilibili拓展
BewlyCat 的组件目录中有一份仅三行的 README,却点明了整个扩展的图标基础设施:借助 [Iconify] 生态,几乎可以使用任何图标集,且构建时只会打包实际用到的图标。本文以此为骨架,结合仓库中 unocss.config.ts、Icon.vue、package.json 以及 Dock、SideBar、设置页等大量组件的真实用法,深入剖析 BewlyCat 是如何把"海量图标集 + 按需打包"落地的——读完你将掌握静态原子类、动态图标 safelist、本地图标组件与 B 站专属 SVG 符号四种图标使用姿势,以及它们各自的适用场景。
一、原文档说了什么:三条核心事实
src/components/README.md的内容可以归纳为三个要点,它们是理解 BewlyCat 图标体系的钥匙:
- 图标来源:通过 Iconify 的力量,几乎可以使用任何图标集(Iconify 聚合了数千个开源图标集,以
集合名:图标名的形式寻址)。 - 按需打包:它只会把"你实际用到的图标"打进产物,而不是把整个图标集搬进浏览器。
- 工具链参考:文档提及 vite-plugin-icons 供进一步了解细节。
值得注意的一点是:当前仓库并未直接安装vite-plugin-icons或unplugin-icons(见 package.json 的 devDependencies),实际承担"按需打包"职责的是UnoCSS 的presetIcons预设,配套的@iconify/json(v2.2.376)作为图标数据源被声明在 devDependencies 中。这正体现了 README 的"示例性"定位——它是这套方案在 BewlyCat 中的真实实现(下文将逐一验证)。
二、基础设施:UnoCSS presetIcons 与图标数据源
2.1 依赖与数据源
package.json 中与图标体系直接相关的依赖:
"devDependencies": { "@iconify/json": "^2.2.376", "unocss": "^66.4.2" }@iconify/json:Iconify 的 JSON 图标数据包,包含几乎所有主流开源图标集的矢量数据。它只存在于 devDependencies,因为运行时并不需要向 Iconify 服务器请求任何东西——所有图标数据都在构建期被编译进产物。unocss:提供presetIcons,负责在扫描源码时发现图标类名、按需生成对应图标的 CSS。
2.2 presetIcons 的默认样式
unocss.config.ts 中配置了presetIcons并为每个图标注入统一的默认 CSS 属性:
presetIcons({ extraProperties: { 'display': 'inline-block', 'vertical-align': 'middle', 'width': '1.2em', 'height': '1.2em', }, }),这意味着所有i-前缀的图标类默认都是行内块元素、垂直居中对齐、尺寸跟随字号(1.2em),天然适配文字混排场景;想放大缩小只需通过字号工具类(如text-xl)控制,无需单独指定宽高。例如 Dock.vue 中:
<div i-mingcute:settings-3-line text-xl group-hover:rotate-180 transition="transform duration-400 ease-out" />i-mingcute:settings-3-line负责渲染齿轮图标,text-xl借助 1.2em 规则放大它,再配合transition/hover实现悬停旋转的动效——图标被当作一个普通的内联元素参与布局与动画。
2.3 扫描范围
unocss.config.ts的content.filesystem声明了扫描范围:**/*.{js,ts,vue,svelte,jsx,tsx,mdx,md,astro,elm,php,phtml,html},覆盖整个仓库源码。凡是出现在这些文件中的i-集合:图标名类名,都会触发按需生成;未出现的图标则完全不会进入产物——这就是"只打包你用的图标"的机制本质。
三、静态用法:原子类,哪里需要哪里贴
3.1 基本形式
静态图标最常见的写法是直接把i-<集合>:<图标名>当作 class 写在元素上,零 JS 成本。仓库中遍布这种用法,例如:
- ArticleCard.vue:
<div i-tabler:eye />(浏览量)、<div i-tabler:thumb-up />(点赞)、<div i-tabler:message />(评论数); - SearchBar.vue:
<div i-tabler:search block align-middle />; - IframeDrawer.vue:
i-mingcute:external-link-line(外链)、i-mingcute:close-line(关闭); - MomentCard 系列:
i-mingcute:more-2-line、i-mingcute:play-circle-line、i-mingcute:up-line/down-line等展开收起图标; - MomentVideoStrip.vue:
i-line-md:confirm与i-mingcute:carplay-line组合表达"已加入稍后再看"的状态切换。
3.2 状态驱动的静态图标
静态类名也可以被条件逻辑驱动,实现"同一个位置、不同状态下显示不同图标"。典型例子在 Dock.vue:
<Icon :icon="isLayoutEditing ? 'mingcute:check-line' : 'mingcute:edit-3-line'" />以及 ContextMenu.vue:
<i v-if="option.checked !== undefined" class="item-check" :class="{ 'i-mingcute:check-line': option.checked }" aria-hidden="true" />这类写法仍属于"编译期可静态分析"的范畴:类名字符串以字面量形式出现在模板中,UnoCSS 依然能在构建时收集到它们。
四、动态用法:BewLocalIcon 与 safelist 机制
当图标名来自运行时数据(如用户配置、设置项定义、接口返回值)时,UnoCSS 无法在构建期扫描到具体类名。BewlyCat 为此准备了本地图标组件 + safelist 的组合拳。
4.1 本地图标组件 Icon.vue
Icon.vue 是一个仅有 19 行的轻量组件,完整实现如下:
<script setup lang="ts"> defineOptions({ name: 'BewLocalIcon' }) defineProps<{ icon: string }>() </script> <template> <span class="bew-local-icon" :class="`i-${icon}`" /> </template> <style scoped> .bew-local-icon { display: inline-block; flex: none; vertical-align: middle; } </style>它接收一个icon: stringprop,渲染时拼出i-${icon}类名;bew-local-icon样式保证图标在 flex 布局中不被压缩(flex: none)。该组件被 Dock.vue、SideBar.vue、MomentsPop.vue、DislikeDialog.vue、VideoCardCover.vue、opusDetailDrawerLayout.ts 等多个模块引用,是动态图标的统一入口。
4.2 动态图标名从哪来
动态图标名最典型的来源是设置页的分类导航。SettingsCategoryLayout.vue 定义了CategoryPage接口,每个页面携带icon与iconActivated两个图标名:
export interface CategoryPage { value: string titleKey: string descriptionKey?: string icon: string iconActivated: string component: Component groupKey?: string warning?: boolean badgeKey?: string }模板中根据激活态切换图标(SettingsCategoryLayout.vue):
<span class="settings-category-icon" :class="activePage === page.value ? page.iconActivated : page.icon" />同一机制也出现在 SettingsSectionHeading.vue:<span v-if="icon" class="settings-section-heading__icon" :class="icon" />,直接以运行时传入的图标类渲染标题图标。此外 Settings/types.ts、TopBar 的 MorePop.vue、DockAndSidebar.vue 等都以icon: string字段形式承载动态图标名。
4.3 safelist:把"可能用到"的图标钉进产物
由于上述图标名是运行时字符串,UnoCSS 扫描不到,BewlyCat 在 unocss.config.ts 中通过safelist显式声明了一组固定图标类,注释写得很直白:
Runtime Icon components resolve to these local UnoCSS icons. Keeping the finite list here prevents @iconify/vue from fetching icons after mount.
这句话揭示了两个设计意图:
- 动态图标组件最终都解析到这组本地 UnoCSS 图标上,由 safelist 保证它们始终存在于构建产物中;
- 这样可以避免组件挂载后再去运行时获取图标(对应 "@iconify/vue from fetching icons after mount" 的隐患),保证离线可用、无网络抖动。
safelist 里的条目涵盖了明快风格图标(mingcute)、深色模式下切换动画(line-md)、通用操作图标(tabler/mdi)等,例如:
- 首页导航:
i-mingcute:home-5-line/home-5-fill、i-mingcute:search-2-line/search-2-fill、i-mingcute:tv-2-line/tv-2-fill、i-mingcute:star-line/star-fill、i-mingcute:time-line/time-fill(对应 Dock 的分区入口); - 主题切换动画:
i-line-md:sunny-outline-to-moon-loop-transition、i-line-md:moon-alt-to-sunny-outline-loop-transition等,被 Dock.vue 与 SideBar.vue 在isDark判断下动态使用; - 编辑器操作:
i-mdi:undo-variant、i-mdi:redo-variant、i-mingcute:edit-3-line、i-mingcute:check-line; - 互动/内容:
i-mingcute:thumb-up-2-line、i-mingcute:play-circle-line、i-mingcute:danmaku-line、i-mingcute:carplay-line等。
实践要点:凡是经由BewLocalIcon或:class绑定、图标名来自变量/数据的场景,都必须在 safelist 中登记对应类名,否则构建产物会缺失该图标。safelist 是有上限的"白名单",这也倒逼项目把动态图标收敛到有限集合,而不是放任任意字符串。
五、图标命名空间:仓库实际用到的图标集
从全仓库的i-前缀扫描结果看,BewlyCat 主要使用以下四个 Iconify 命名空间,各司其职:
| 命名空间 | 典型图标 | 主要用途 |
|---|---|---|
mingcute: | home-5-line、settings-3-line、play-circle-line、danmaku-line、layout-grid-line | 导航、功能入口、内容操作,中性风格,占比最大 |
tabler: | eye、eye-off、search、copy、brand-github、brand-bilibili | 通用操作图标与品牌标识 |
line-md: | sunny-outline-to-moon-loop-transition、confirm、arrow-small-up | 带过渡动画的状态图标(尤其是明暗主题切换) |
mdi: | undo-variant、redo-variant | 少量补充型操作图标 |
命名空间前缀本身也是 UnoCSS presetIcons 的寻址规则:i-<集合名>:<图标名>。更换图标风格时只需替换集合名与图标名,类名结构与布局样式完全不变,这正体现了"几乎可以使用任何图标集"的灵活性。
六、B 站专属图形:本地 SVG symbol 的补充
Iconify 覆盖通用图标,但 B 站生态中大量平台专属图形(频道分区图标、顶栏入口、用户面板、播放器控件等)并不在开源图标集中。BewlyCat 在 svgIcons.ts 中维护了一个巨大的内联 SVG sprite 字符串,通过<symbol id="...">定义了一百多个专属图标,命名空间包括:
channel-*:频道分区图标,如channel-anime、channel-game、channel-dance、channel-guochuang(国创)、channel-vlog、channel-tuiguang(推广)等,涵盖动画、游戏、生活、知识、影视、直播等全部分区;header-*:顶栏入口,如header-channel、header-search、header-history、header-hot、header-message、header-vip、header-login、header-creation等;widget-*:卡片与小组件,如widget-favorite、widget-danmaku、widget-play-count、widget-watch-later、widget-up、widget-follow、widget-people等;user-*、palette-*、creator-*、history-*、rate-*等:用户面板、设置调色、创作中心、历史记录、等级皇冠等细分场景。
这类 SVG 符号与i-原子类互补:前者承载品牌与平台语义(无法用通用图标替代、甚至自带品牌色),后者承载通用交互语义(可随主题色/字号自由缩放)。图标体系由此形成"Iconify 通用图标 + 本地 SVG 专属符号"的双层结构。
七、构建视角:按需打包是如何实现的
结合 vite.config.ts 与 unocss 配置可以还原整条构建链路:
- UnoCSS 依据
content.filesystem扫描全仓库,收集i-*类名(静态类 + safelist 类); presetIcons从@iconify/json中提取对应图标矢量数据,仅对收集到的条目生成 CSS 规则;- 未在源码中出现、也未登记进 safelist 的图标不会进入产物——这就是"只 bundle 你使用的图标"在 BewlyCat 中的落地形态;
- 平台专属图形以
<symbol>形式随 svgIcons.ts 打包,通过<use href="#id">引用,同样不存在整包冗余问题。
这种方案相比在运行时引入@iconify/vue并依赖其按需 fetch 的做法,优势在于零运行时网络依赖:所有图标在构建期固化为本地 CSS/SVG,安装扩展后即可离线渲染,也避免了组件挂载后再取图标的闪烁与失败。
八、小结:四种用法速查
| 场景 | 用法 | 关键文件 |
|---|---|---|
| 模板内静态图标 | 直接写i-集合:图标名类名 | ArticleCard.vue、IframeDrawer.vue |
| 少量条件切换 | :class绑定静态字符串 | ContextMenu.vue |
| 运行时数据驱动的图标 | BewLocalIcon组件 + safelist 登记 | Icon.vue、SettingsCategoryLayout.vue、Dock.vue |
| B 站平台专属图形 | 内联 SVG<symbol>+<use> | svgIcons.ts |
从 README 的三行说明出发,BewlyCat 用unocss presetIcons + @iconify/json实现了"任何图标集 + 按需打包";用safelist与BewLocalIcon解决了动态图标名的构建期收集难题;再用内联 SVG symbol 补足了 Iconify 覆盖不到的平台专属图形。这套组合既保证了扩展体积的克制,也保证了运行时图标的零依赖、零闪烁,是浏览器扩展项目中图标体系的成熟范本。
- 前端
【免费下载链接】BewlyCat
BewlyCat——基于BewlyBewly开发的Bilibili拓展
相关推荐
BewlyBewly 组件图标体系实战:基于 Iconify 与 UnoCSS 的按需图标加载指南
BewlyBewly 组件图标体系实战:基于 Iconify 与 UnoCSS 的按需图标加载指南 导读 本篇技术指南聚焦 BewlyBewly 仓库中 src
前端BiliNote 浏览器扩展组件体系解析:unplugin-vue-components 自动注册、按需加载与 Iconify 图标方案
BiliNote 浏览器扩展组件体系解析:unplugin vue components 自动注册、按需加载与 Iconify 图标方案 导读 本文以 Bill
AI 应用大模型RAG语音后端前端桌面应用Docusaurus v3 文档站快速搭建实战:5 分钟起步并接入 Orama 全文搜索
Docusaurus v3 文档站快速搭建实战:5 分钟起步并接入 Orama 全文搜索 本文以当前仓库 sandboxes/plugin docusaurus
向量数据库RAG
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考