FEATURED · 精选文章

airi 仓库 UnoCSS Safelist 与 Blocklist 指南:强制收录与排除工具类的正确姿势

发布时间 / 2026/9/10 9:46:49
来源 / 创域科博编辑部
栏目 / 资讯中心
airi 仓库 UnoCSS Safelist 与 Blocklist 指南:强制收录与排除工具类的正确姿势 airi 仓库 UnoCSS Safelist 与 Blocklist 指南强制收录与排除工具类的正确姿势【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airiUnoCSS 是构建时按需扫描的原子 CSS 引擎凡是在构建期“看不见”的类名都不会生成对应样式——这正是数据驱动 UICMS 文案、路由 meta、运行时拼类名最容易踩的坑。本文以 airi 开源仓库根配置为实例系统讲解safelist强制收录与blocklist强制排除的全部语法形态、适用场景与工程化取舍读完即可写出既能兜住动态类名、又不会撑爆产物体积的配置。本文对应的 UnoCSS 能力清单与速查页位于仓库的.agents/skills/unocss/references/core-safelist.md其中整理的知识基于 UnoCSS 66.x仓库根目录及各个 app/package 的uno.config.ts则提供了真实可运行的落地范式。为什么需要 Safelist按需提取与动态类名难题UnoCSS 的核心理念是按需生成它只在构建期扫描源码把出现过的工具类转成 CSS。这意味着三个基本事实详见同目录的提取机制说明扫描发生在构建期模板里拼接出来的类名不会被识别例如div classp-${size}永远生成不出任何p-*样式.ts/.js文件默认不在 Vite 管道提取范围内只有.vue、.md、.html、.jsx/.tsx等会被默认扫描数据驱动的类名后端下发、路由表配置、用户主题往往只以「字符串变量的值」形式存在扫描器无法追踪其具体取值。官方给出的三条出路中最简单直接的一条就是Safelist——把已知的取值集合预先声明无论源码里写没写都强制生成。另外两条分别是「静态映射」把组合写死成可被扫描的常量与「运行时方案」引入unocss/runtime做真正的运行时生成本文第 5、6 节会结合仓库实践逐一展开。Safelist无条件强制收录defineConfig中的safelist接收一个字符串数组。数组里每个元素代表一个「无论是否被扫描到都必定输出的工具类」export default defineConfig({ safelist: [ p-1, p-2, p-3, // 用 JS 表达式批量生成等价于 p-1...p-4 ...Array.from({ length: 4 }, (_, i) p-${i 1}), ], })由于safelist本质就是普通数组完全可以借助展开运算符、Array.from、.map()等 JS 能力做程序化批量生成——这正是本仓库根配置里大量使用的技巧后文详述。函数形式静态数组写不下的动态逻辑除了字符串safelist的每个元素还可以是函数。函数返回值会被并入 safelistsafelist: [ p-1, () [m-1, m-2], // 返回字符串数组 (context) { // 接收上下文对象可读主题 const colors Object.keys(context.theme.colors || {}) return colors.map(c bg-${c}-500) }, ]函数形式的价值在于与主题联动context.theme.colors里新增了某个色板时safelist 会自动同步无需手工维护。你可以据此为「自定义预设新增的每一个色名」都兜底生成一整档-500色阶。常见用法从 CMS 与组件变体推导全组合core-safelist.md给出了两个最具代表性的落地场景——其一是不确定 CMS 会下发哪种语义色先把所有语义色 × 三种用法做笛卡尔积收进 safelist其二是组件变体variant × size的全面兜底safelist: [ // 动态颜色来自 CMS无法在构建期枚举具体类名 () [primary, secondary].flatMap(c [ bg-${c}, text-${c}, border-${c}, ]), // 组件变体样式表是运行时传入的 className 字符串 () { const variants [primary, danger] const sizes [sm, md, lg] return variants.flatMap(v sizes.map(s btn-${v}-${s})) }, ]注意上面这个例子同时体现了设计意图只有当类名真的无法在构建期以字面量形式出现时才需要用 safelist 兜底如果变体表是模板里的静态字符串交给自动提取即可详见下文最佳实践。Blocklist无条件强制排除blocklist与safelist相反用于禁止某些工具类被生成——例如品牌色管控、无障碍约束、废弃类名下线等场景blocklist: [ p-1, // 精确匹配单个类名 /^p-[2-4]$/, // 正则匹配一批类名 ]附带提示信息当被排除的类名命中时你可能希望构建工具给出明确反馈如「请改用 X」此时可以把数组元素写成二元组blocklist: [ [bg-red-500, { message: Use bg-red-600 instead }], [/^text-xs$/, { message: Use text-sm for accessibility }], ]{ message }用于承载替换建议或约束说明很适合团队里统一色板、强制无障碍字号下限这类「约定大于配置」的场景。Safelist vs Blocklist对照表与优先级两种机制的语法能力边界不同必须记牢表格内容逐字继承自参考文档特性SafelistBlocklist用途总是包含总是排除字符串✅✅正则❌✅函数✅❌两处最容易写错的地方Safelist 不支持正则想做「1 到 10 的 p-*」这类批量声明请用Array.from或字符串数组展开而不是正则Blocklist 不支持函数且当某个工具类同时出现在safelist与blocklist时Blocklist 优先原文档注明Blocklist wins if utility is in both。因此做「白名单例外」式的精细控制时不要假设 safelist 能覆盖 blocklist。仓库实战一airi 根配置中的 Safelist 三合一设计airi 的 monorepo 把 UnoCSS 配置做成了可跨端复用的共享模块根目录 uno.config.ts 导出一个sharedUnoConfig()内部在defineConfig中拼装了三个来源的 safelistuno.config.ts#L169-L173safelist: [ ...prose prose-sm m-auto text-left.split( ), // markdown 排版工具类 ...safelistAllPrimaryBackgrounds(), // primary 色阶全量兜底 ...safelistSettingsEntryIcons(), // 设置页入口图标 ],prose prose-sm m-auto text-left.split( )这段在仓库里出现多次apps/component-calling/uno.config.ts、packages/ui-loading-screens/uno.config.ts、packages/ui-transitions/uno.config.ts 均含同一写法它的语义是prose系列排版类由 markdown 渲染器在运行时注入到 HTML源码模板里永远扫不到字面量类名所以必须无条件收录否则文档正文会失去排版样式。这也是docs/uno.config.ts顶部维护了top-0、hidden、opacity-0、group-hover:opacity-100、lg:flex、transition-opacity等一批交互类 safelist 的同类原因。用函数全量枚举 primary 色阶与透明度sharedUnoConfig用presetChromaticproj-airi 自定义预设以baseHue生成primary主题色阶。配套的辅助函数safelistAllPrimaryBackgroundsuno.config.ts#L59-L67用两层循环把「全部色深 × 全部透明度」的笛卡尔积一次性收进 safelistexport function safelistAllPrimaryBackgrounds(): string[] { return [undefined, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950].map((shade) { const prefix shade ? bg-primary-${shade} : bg-primary return [ prefix, ...[5, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map(opacity ${prefix}/${opacity}), ] }).flat() }从代码可以直接推导出产出规模12 个色深档位含无后缀的bg-primary本身各自再配 11 档透明度写法/5、/10、…/10012 × 12 144条类名。这是典型的「用函数形式做穷举」——当运行时可能依据主题/配置拼接出任意深度的强调色背景时设置页中大量出现的bg-primary-50、bg-primary-500/20、dark:bg-primary-900/30等写法可见一斑与其依赖扫描碰运气不如把可枚举空间一次收编。需要清醒认识到144 条只是一个颜色维度上的成本。Safelist 的体积与穷举范围成正比这也是为什么最佳实践要求「只兜真正动态的部分」而不是把静态写法也搬进来。为路由 meta 中的设置入口图标兜底第二个辅助函数safelistSettingsEntryIconsuno.config.ts#L69-L81把设置页每个入口图标的 Iconify 类名集中声明export function safelistSettingsEntryIcons(): string[] { return [ i-solar:emoji-funny-square-bold-duotone, i-solar:people-nearby-bold-duotone, i-solar:leaf-bold-duotone, i-solar:armchair-2-bold-duotone, i-solar:database-bold-duotone, i-solar:wi-fi-router-bold-duotone, i-solar:layers-bold-duotone, i-solar:box-minimalistic-bold-duotone, i-solar:filters-bold-duotone, ] }为什么这些图标必须 safelist看设置首页的实现就明白了packages/stage-pages/src/pages/settings/index.vue#L29-L40 通过router.getRoutes()聚合所有子路由把route.meta.icon这个运行时字符串取出来再以数据绑定方式塞给IconItem :iconitem.icon。图标类名以「数据」而非「模板静态类」的形态流转属于典型的构建期不可见场景同时这些字符串分散在各页面route元数据里例如 memory/index.vue 的i-solar:leaf-bold-duotone一旦扫描范围或解析规则变化就容易集体丢失。用 safelist 集中登记是最稳妥的收口方式。项目甚至为这条约定写了守护测试 packages/stage-shared/src/uno-settings-icons.test.ts它断言辅助函数返回的图标清单与sharedUnoConfig().safelist保持「数组包含」关系任何一侧的误删都会让 CI 红灯从机制上防止「配置在、safelist 忘同步」的回归。仓库实战二共享 Safelist 的 monorepo 多端复用airi 的 safelist 设计最值得借鉴的一点是一次声明、处处生效。根目录的sharedUnoConfig()通过export default mergeConfigs([sharedUnoConfig(), {...本地差异}])的方式被大量端侧复用例如 apps/stage-web/uno.config.ts、apps/stage-pocket/uno.config.ts、apps/stage-tamagotchi/uno.config.ts、apps/ui-server-auth/uno.config.ts、packages/stage-ui/uno.config.ts、packages/scenarios-stage-tamagotchi-browser/uno.config.ts 以及 plugins/airi-plugin-web-extension/uno.config.ts// 以 apps/stage-web/uno.config.ts 为例有删节 export default mergeConfigs([ sharedUnoConfig(), // presets、safelist、rules、theme 全部继承 { presets: [ /* 端侧专属 preset如字体在线拉取 */ ], rules: [ /* 端侧专属自定义规则 */ ], }, ])这意味着在任意一个 app 里写bg-primary-700/40、i-solar:wi-fi-router-bold-duotone都能确信样式会被生成——safelist 的收益被整个 monorepo 摊薄并放大。mergeConfigs会深合并各段配置本地配置只写增量即可不会因为某个子端忘了复制 safelist 而样式静默丢失。最佳实践优先静态映射而非无脑加 Safelistcore-safelist.md在结尾给出了一个常常被忽略的工程忠告凡是能写成静态映射的就交给 UnoCSS 自动提取safelist 只用来兜真正动态的部分// ✅ 更好组合写在静态对象里UnoCSS 扫描源码即可自动提取 const sizes { sm: text-sm p-2, md: text-base p-4, } // ❌ 避免明明可被静态提取却靠 safelist 硬塞徒增产物体积 safelist: [text-sm, text-base, p-2, p-4]判断依据很简单类名的所有可能取值是否在源码中以字面量出现过出现过——交给提取器产物更小只有数据/运行时拼接才值得进 safelist。这与提取机制的另外两件配套工具要一起用放宽提取范围若组件把类名写进了.ts/.js常量可在content.pipeline.include中把这类文件加入扫描airi 根配置的content.pipeline就显式 include 了(components|src)/**/*.{js,ts,vue}与**/stage-ui/**、**/ui/**并 excludenode_modules详见提取机制说明Magic Comments对少量无法被管道覆盖的文件用// unocss-include强制纳入扫描比手工维护 safelist 更内聚Safelist 兜底前两者都够不到的数据类名才动用 safelist。若连同 safelist 也无法穷举如运行时任意取值最后的方案是引入unocss/runtime做真正的运行时生成——那是另一套取舍不在本文讨论范围。小结一张决策路线图你的场景推荐手段类名在模板中以字面量出现什么都不用做自动提取类名写在.ts/.js常量中扩展content.pipeline.include或// unocss-include类名来自路由 meta / CMS / 用户配置取值可枚举safelist字符串 函数穷举需要彻底禁止某些类名品牌色、无障碍、废弃下线blocklist字符串 / 正则 / 二元组 message类名运行时任意拼接、不可枚举unocss/runtime运行时生成记住三条硬规则safelist 不收正则、blocklist 不收函数、两者冲突时 blocklist 赢优先静态映射减小体积把 safelist 做成共享配置 单元测试守护让「动态类名静默丢失」这类问题在 monorepo 里根本没有生存空间。airi 根配置的safelistAllPrimaryBackgrounds、safelistSettingsEntryIcons与 stage-shared 守护测试 就是这套方法论的可运行范本可以直接迁移到你自己的 UnoCSS 工程中。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻