UnoCSS Svelte Scoped:把原子样式编译进 Svelte 组件<style>块的 Scoped 样式方案
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
本篇介绍@unocss/svelte-scoped包的完整用法与实现原理:它如何把工具类(utility class)改写为带哈希的唯一类名,并将生成的 CSS 直接写入对应 Svelte 组件的<style>块,从而摆脱"全局样式表不断膨胀"的困境。读完本文,你将掌握其 Vite 插件与 Svelte 预处理器两种集成方式的配置方法、类名改写支持的全部语法形态、--at-apply/theme()等样式块指令的处理机制,以及预设与 Transformer 的支持边界,并能对照仓库源码理解哈希类名生成、:global()包裹与全局占位符替换等关键实现。
核心机制:从全局 CSS 到组件级<style>块
标准 UnoCSS/Tailwind 方案会把所有工具类样式集中写入一个全局 CSS 文件并保证选择器顺序正确;而 Svelte Scoped 则把样式分散到各个 Svelte 组件自己的<style>块中。文档中的转换示例非常直观——组件里的:
<div class="mb-1" />会被改写为:
<div class="uno-ei382o" /> <style> :global(.uno-ei382o) { margin-bottom: 0.25rem; } </style>这里有两个关键设计:
- 类名哈希化:原始工具类名(如
mb-1)被替换为类名前缀 + 哈希值。哈希的输入是文件路径 + 类名组合,因此即使两个不同组件都使用mb-1,生成出的类名也不会冲突,可以放心声明为全局类; - 保留全局语义:虽然样式物理上分散在各组件中,但这些类在运行时仍是"全局类",可以感知组件外部的上下文(如父级
.dark、[dir="rtl"]),这正是通过 Svelte 的:global()语法实现——:global()让 Svelte 编译器跳过默认的 CSS 哈希,让选择器以原文形式进入产物。
什么时候该用 Svelte Scoped
| 使用场景 | 推荐 | 说明 |
|---|---|---|
| 小型应用 | 不推荐 | 一个全局 CSS 文件更省心,直接用 Vite 集成即可 |
| 大型应用 | 推荐 | 避免全局 CSS 文件随项目增长而无限膨胀 |
| 组件库 | 推荐 | 生成的样式直接落在构建产物组件内,消费方构建管线无需再引入 UnoCSS |
对应地,包内提供两个入口(见 package.json 的exports字段):
@unocss/svelte-scoped/vite:Vite 插件,用于 Svelte / SvelteKit 应用;@unocss/svelte-scoped/preprocess:Svelte 预处理器,用于构建组件库。
类名改写:支持哪些写法、dev 与 prod 有何不同
由于 Svelte Scoped 会改写工具类名,工具类必须出现在它能识别的位置。官方支持的语法形态如下:
| 支持的语法 | 示例 |
|---|---|
class属性 | <div class="mb-1" /> |
class:指令 | <div class:mb-1={condition} /> |
class:指令简写 | <div class:logo /> |
组件的classprop | <Button class="mb-1" /> |
clsx风格表达式 | <div class={["mb-1", { logo, 'font-bold': isBold() }, isUnderlined() && 'underline' ]} /> |
此外,class属性中的模板表达式(如<div class="mb-1 {foo ? 'mr-1' : 'mr-2'}" />)也受支持(Svelte Scoped 被设计为工具类项目的直接替换方案),但官方建议逐步迁移到clsx写法。需要注意的是:如果把类名写在<script>块里、或使用了 attributify 模式,就无法被直接识别,需要借助safelist或额外的预处理步骤(见后文"预设支持")。
dev 与 prod 的类名差异
Vite 插件会自动区分dev与build环境,而 preprocessor 需要手动通过combine选项控制(详见"Svelte 预处理器"一节):
- 开发模式(
combine: false):每个工具类单独哈希保留,例如class="mb-1 mr-1"变成class="_mb-1_9hwi32 _mr-1_84jfy4",方便在浏览器开发者工具中逐个开关联调; - 生产模式(
combine: true):合并为单个类名,默认前缀uno-加上基于文件名 + 类名生成的哈希,如class="uno-84dke3"。
从源码 generateClassName.ts 可以确认这一逻辑:
export function generateClassName(body: string, options: TransformClassesOptions, filename: string): string { const { classPrefix = 'uno-', combine = true, hashFn = hash, } = options if (combine) { const classPlusFilenameHash = hashFn(body + filename) return `${classPrefix}${classPlusFilenameHash}` } else { const filenameHash = hashFn(filename) return `_${body}_${filenameHash}` // certain classes (!mt-1, md:mt-1, space-x-1) break when coming at the beginning of a shortcut } }可以看到 prod 类名是前缀 + hash(类名 + 文件名);dev 类名保留原始类名并附加文件哈希(前缀_是为了避免!mt-1、md:mt-1、space-x-1这类类名处于 shortcut 开头时破坏匹配)。测试用例 basic/OutputProd.svelte 展示了 preprocessor 入口下 prod 产物的实际形态(注意 preprocessor 默认前缀是usp-):
<div class="usp-dyjh78" /> <style> :global(.usp-dyjh78) { --un-bg-opacity: 1; background-color: rgb(239 68 68 / var(--un-bg-opacity)); font-weight: 600; } </style>两种入口默认前缀不同(Vite 插件为uno-,preprocessor 为usp-),这一刻意区分是为了避免"用 preprocessor 构建的组件库被用到 Vite 插件项目中"时前缀撞车产生 bug,见 types.d.ts 中classPrefix的注释。
上下文感知:样式分散但语义不分散
尽管样式分散在各组件里,它们仍是全局类,能感知所在组件外部的元素状态。
依赖父级属性的类
<div class="dark:mb-2 rtl:right-0"></div>会被改写为(dark、[dir="rtl"]上下文选择器由:global()包裹):
<div class="uno-3hashz"></div> <style> :global(.dark .uno-3hashz) { margin-bottom: 0.5rem; } :global([dir="rtl"] .uno-3hashz) { right: 0rem; } </style>子元素相互影响的类
space-x-*这类依赖兄弟元素关系的工具类同样跨组件生效:
<div class="space-x-1"> <div>Status: online</div> <Button>FAQ</Button> <Button>Login</Button> </div>改写后(注意Button是独立组件,但其内部元素仍参与选择器匹配):
<div class="uno-7haszz"> <div>Status: online</div> <Button>FAQ</Button> <Button>Login</Button> </div> <style> :global(.uno-7haszz > :not([hidden]) ~ :not([hidden])) { --un-space-x-reverse: 0; margin-left: calc(0.25rem * calc(1 - var(--un-space-x-reverse))); margin-right: calc(0.25rem * var(--un-space-x-reverse)); } </style>向子组件传递类
给组件添加classprop,即可在任意消费处传入自定义类:
<Button class="px-2 py-1">Login</Button>改写为:
<Button class="uno-4hshza">Login</Button> <style> :global(.uno-4hshza) { padding-left: 0.5rem; padding-right: 0.5rem; padding-top: 0.25rem; padding-bottom: 0.25rem; } </style>接收组件最简单的落地方式是把类挂到某个元素上,例如div class="{$$props.class} foo bar" />。
样式块内的 Apply 指令与 theme()
Svelte Scoped 可以直接处理<style>块中的 apply 指令,默认支持--at-apply与@apply两种写法(可通过applyVariables选项自定义,传false禁用,见 types.d.ts)。
它还能正确处理上下文依赖类(如dark:text-white、rtl:ml-2)——这是通用@unocss/transformer-directives包做不到的,因为后者并非为 Svelte 样式块专门设计。例如:
<div /> <style> div { --at-apply: rtl:ml-2; } </style>会被改写为:
<div /> <style> :global([dir="rtl"]) div { margin-right: 0.5rem; } </style>注意选择器拆分策略:[dir="rtl"]被:global()包裹,防止 Svelte 编译器因组件内不存在带dir属性的元素而把它剥离;而div本身不能包进:global(),否则样式会污染全局所有div。
theme()指令同样受支持(transformThemeDirective选项默认true,可关闭以兼容 Tailwind 语法);但@screen不支持。
Vite 插件集成(应用场景)
在 Svelte / SvelteKit 应用中使用:把生成的样式直接注入组件,仅把最少的必要样式放入全局样式表。仓库中提供了 SvelteKit 示例 可参考。
安装
pnpm add -D unocss @unocss/svelte-scoped # 或 yarn add -D unocss @unocss/svelte-scoped npm install -D unocss @unocss/svelte-scoped bun add -D unocss @unocss/svelte-scoped添加插件
把@unocss/svelte-scoped/vite加入 Vite 配置:
import { sveltekit } from '@sveltejs/kit/vite' import UnoCSS from '@unocss/svelte-scoped/vite' import { defineConfig } from 'vite' export default defineConfig({ plugins: [ UnoCSS({ // injectReset: '@unocss/reset/normalize.css', // 也可传入自定义 reset 路径 // ...其他 Svelte Scoped 选项 }), sveltekit(), ], })Vite 插件入口实现见 _vite/index.ts:它实际上是一个插件数组,由GlobalStylesPlugin(负责全局样式与 reset 注入)、ConfigHMRPlugin(配置热更新)、transformPlugin(组件改写,可通过onlyGlobal: true关闭)以及可选的cssFileTransformers插件组成。若未在配置中指定 preset,默认使用presetUno()。
injectReset可传入@unocss/reset内置的normalize.css、eric-meyer.css、sanitize/sanitize.css、tailwind.css、tailwind-compat.css,也可以是相对项目根目录的自定义文件路径(如./src/reset.css),甚至node_modules中其他包里的 CSS 路径,完整取值见 _vite/types.d.ts。
全局样式占位符
几乎全部样式都在组件内,但仍有几类必须进入全局样式表:preflights、safelist,以及可选的 reset。做法是在<head>中加入%unocss-svelte-scoped.global%占位符:Svelte 项目中放在index.html,SvelteKit 项目中放在app.html的%sveltekit.head%之前:
<head> <!-- ... --> <title>SvelteKit using UnoCSS Svelte Scoped</title> %unocss-svelte-scoped.global% %sveltekit.head% </head>该占位符在源码 _vite/constants.ts 中定义,对应的全局样式产物文件名为unocss-svelte-scoped-global.css;插件同时提供了onlyGlobal选项——当你的项目是用 preprocessor 构建的组件库、但仍想为 demo 应用补齐 reset / preflights / safelist 等全局样式时,可将它设为true。
Svelte 预处理器集成(组件库场景)
构建不依赖"伴随 CSS 文件"的组件库时,用@unocss/svelte-scoped/preprocess在编译期把样式直接写进组件。仓库中提供了 SvelteKit 库示例。
安装
与 Vite 插件相同:
pnpm add -D unocss @unocss/svelte-scoped添加预处理器
把@unocss/svelte-scoped/preprocess加入 Svelte 配置:
import adapter from '@sveltejs/adapter-auto' import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' import UnoCSS from '@unocss/svelte-scoped/preprocess' const config = { preprocess: [ vitePreprocess(), UnoCSS({ // ... 预处理器选项 }), ], // 其他 Svelte 配置 }从 _preprocess/index.ts 可以看到,preprocessor 返回一个PreprocessorGroup,包含markup与style两个处理器:markup负责发现并改写类名、把生成的 CSS 注入<style>块;style负责uno-preflights/uno-safelist属性以及 apply / theme 指令的处理。若没有从 Vite 插件获得共享上下文,它独立加载配置并默认使用presetWind3()。
开发环境不要合并类名
Vite 插件会自动按dev/build切换combine,preprocessor 则需要手动设置。可借助 cross-env(此处为文档原文引用的 npm 包名)修改 dev 脚本:
{ "dev": "cross-env NODE_ENV=development vite dev" }然后在svelte.config.js中:
+const prod = process.env.NODE_ENV !== 'development' const config = { preprocess: [ vitePreprocess(), UnoCSS({ + combine: prod, }), ], }组件级 Preflights 与 Safelist
使用 preprocessor 时,可以把 preflights 放进具体需要的组件:
<style uno-preflights></style>以句点开头的特殊 preflights(如.prose :where(a):not(:where(.not-prose, .not-prose *)))会被:global()包裹,避免被 Svelte 编译器剥离。如果你的类不依赖 preflights,或消费方应用已包含 preflights,这一步可以省略。
同理,把 safelist 类写入组件:
<style uno-safelist></style>safelist 样式同样会被:global()包裹。源码中还能看到uno:preflights旧写法(Svelte 3 时代)已被标记弃用,Svelte 4 因自定义属性不能包含冒号而拆分了该属性,因此当前推荐使用连字符写法uno-preflights。
配置:uno.config.ts 与预设 / Transformer 支持边界
把 UnoCSS 设置放进uno.config.ts(configOrPath选项也可直接传配置对象或路径):
import { defineConfig } from 'unocss' export default defineConfig({ // ...UnoCSS 选项 })其余配置细节参见 配置指南 与 配置参考。但有几个 Svelte Scoped 特有的边界必须注意:
1. 不支持 Extractor,也不支持transformers。由于常规 UnoCSS 全局用法与 Svelte Scoped 用法存在本质差异,extractor 无法生效;Vite 插件入口会直接对config.transformers抛出错误(见 _vite/index.ts 中的throw new Error(...)),并提示改用cssFileTransformers。
2. 预设按情况支持:
| 预设 | 支持 | 说明 |
|---|---|---|
@unocss/preset-mini、@unocss/preset-wind3、@unocss/preset-icons、@unocss/preset-web-fonts | 是 | 所有只依赖 rules/variants/preflights 的社区预设同理可用 |
@unocss/preset-typography | 是 | 它通过向 preflights 注入规则集工作,使用时需把prose加入 safelist 才能触发;其余类(如prose-pink)可组件内作用域化。自v66.5.0起prose样式重构为普通rule,不再需要加 safelist |
@unocss/preset-rem-to-px | 是 | 所有仅修改样式输出的预设都可以 |
@unocss/preset-attributify | 否 | 需改用unplugin-attributify-to-class插件(attributifyToClass({ include: [/\.svelte$/] })),并置于 Svelte Scoped Vite 插件之前 |
@unocss/preset-tagify | 否 | 添加自定义 extractor 的预设无法工作;需自行写 preprocessor 把<text-red>Hi</text-red>转成<span class="text-red">Hi</span> |
对其它预设:如果它不依赖传统class="..."写法,需先把类名预处理进class属性;如果它像 typography 那样注入.prose类,需把触发类加入 safelist。
3. Transformer 仅支持 CSS 文件。可以把 transformer 加到cssFileTransformers选项中,用于处理 css/postcss/sass/scss/less/stylus/styl 文件:
import transformerDirectives from '@unocss/transformer-directives' export default defineConfig({ plugins: [ UnoCSS({ cssFileTransformers: [transformerDirectives()], }), sveltekit(), ], })而 Svelte 组件内部的 transformer 因 Svelte Scoped 的工作机制不被支持——Svelte 组件内的 apply/theme 指令已由 Svelte Scoped 自身的style处理器覆盖(见上文"Apply 指令"一节),无需也不应再叠加 transformer。
源码走读:一次组件改写的完整调用链
结合仓库实现,可以还原 Vite 插件处理一个.svelte文件的完整链路:
- 发现类名:transformClasses/index.ts 中
findClasses扫描组件源码,找出所有class属性、class:指令、clsx表达式等位置的工具类候选; - 逐类处理:
processClasses对每个类名调用 UnoGenerator 校验可匹配性(needsGenerated/isShortcut等辅助模块判断哪些需要实际生成规则),并按 generateClassName.ts 生成最终类名,同时通过MagicString的overwrite精确改写源码中的类名片段; - 生成样式:把本组件收集到的规则(含组件级 shortcut)推入
uno.config.shortcuts后调用uno.generate,且明确传入preflights: false, safelist: false——这正对应"preflights/safelist 走全局样式表、其余走组件"的架构分工; - 全局包裹与注入:preprocessor 路径下,生成 CSS 先经
wrapSelectorsWithGlobal(wrapGlobal.ts)把需要跨组件上下文的选择器包上:global(),再由addGeneratedStylesIntoStyleBlock写入<style>块,最后输出带 source map 的改写结果; - 全局样式兜底:
GlobalStylesPlugin在开发时向<head>注入带特定标题的占位样式(unocss-svelte-scoped global styles,见 constants.ts),生产时替换%unocss-svelte-scoped.global%为真实的全局 CSS 内容。
所有选项的类型定义集中在三个文件:_vite/types.d.ts(classPrefix、injectReset、onlyGlobal、cssFileTransformers)、_preprocess/types.d.ts(configOrPath)与 types.d.ts(combine、hashFn、hashSafelistClasses、applyVariables、transformThemeDirective),Vite 插件选项是 preprocessor 选项的超集,两者可无缝切换。其中hashFn允许替换默认哈希算法;hashSafelistClasses默认false,即 safelist 类名(含 shortcut)保持原文直通,与全局生成的 safelist CSS 精确匹配,设为true可恢复旧版行为。
实践建议:何时值得切换到 Scoped
文档给出的判断标准很具体:当你在大型项目中每次写下.md:max-w-[50vw]这类"只用一次"的类、却因全局样式表持续膨胀而感到肉疼时,就是尝试 Svelte Scoped 的信号。犹豫要不要"恰好需要的那个类"会抑制创造力——当然你可以退而求其次用--at-apply: md:max-w-[50vw],但那样既繁琐又丢失了样式在上下文中的表达力。同理,项目里图标种类一多,把它们全部加进全局样式表的代价也会显现;当每个组件都承载自己的样式与图标时,项目扩张不再需要为每一次新增做成本分析。
最后,@unocss/svelte-scoped采用 MIT 许可,作者为 Jacob Bowdoin(2022 至今),应用场景参考 examples/vite-svelte-scoped 与 examples/sveltekit-scoped,组件库场景参考 examples/sveltekit-preprocess。
【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考