UnoCSS Extractors 深入解析:从源码中精确提取工具类用量的配置与原理

UnoCSS Extractors 深入解析:从源码中精确提取工具类用量的配置与原理 UnoCSS Extractors 深入解析从源码中精确提取工具类用量的配置与原理【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocssExtractors提取器是 UnoCSS 引擎中负责扫描源代码、找出工具类utilities用法的关键环节它决定了一段源码里哪些 token 会被送入引擎匹配规则、最终生成 CSS。本文以官方配置文档 docs/config/extractors.md 为主体结合仓库内核心实现packages-engine/core、Pug/Attributify/Svelte/MDC 等真实提取器源码讲解 extractors 的配置方式、默认提取器机制、执行顺序以及如何编写自己的提取器帮助你准确控制 UnoCSS 的提取行为、解决类名未被提取或误提取等问题。一、什么是 Extractors在 UnoCSS 的按需on-demand工作流中代码只经过两条主线提取extract与生成generate。Extractors 处于提取阶段其职责是从源码中提取工具类的使用usage of utilities并输出候选 selector 列表交给引擎继续匹配规则、生成 CSS。在 packages-engine/core/src/types.ts 中Extractor被定义为一个非常轻量的接口export interface Extractor { name: string order?: number /** * Extract the code and return a list of selectors. * * Return undefined to skip this extractor. */ extract?: (ctx: ExtractorContext) AwaitableSetstring | CountableSetstring | string[] | undefined | void }每个提取器只需要做两件事声明一个唯一的name实现extract(ctx)接收ExtractorContext返回提取到的候选字符串集合string[]、Setstring或CountableSetstring返回undefined或void则跳过该提取器。ExtractorContext提供了提取所需的全部上下文同文件 types.tsexport interface ExtractorContext { readonly original: string // 原始代码不可修改 code: string // 当前代码可被前置提取器改写 id?: string // 模块标识文件路径 / query 等 extracted: Setstring | CountableSetstring // 已收集到的 token 集合 envMode?: dev | build // 环境模式 }值得注意的是code字段是可写的某些转换型提取器如 Pug 提取器会先把源码编译成 HTML 再交给后续流程这正是提取器可以链式协作的底层基础。二、在uno.config.ts中配置提取器Extractors 通过配置文件中的extractors数组启用这是官方文档给出的最基本用法import { defineConfig } from unocss export default defineConfig({ extractors: [ // your extractors ], })从源码看extractors是ConfigBase上的一个标准配置项packages-engine/core/src/types.ts/** * Extractors to handle the source file and outputs possible classes/selectors * Can be language-aware. */ extractors?: Extractor[]配置合并与去重逻辑在 packages-engine/core/src/config.ts 中所有来源presets 与用户配置的 extractors 会被合并、排序function getMergedT(key: T) { return uniq(sources.flatMap(p toArray(p[key] || []) as any[])) as any } const extractors getMerged(extractors) let extractorDefault sourcesReversed .find(i i.extractorDefault ! undefined) ?.extractorDefault if (extractorDefault undefined) extractorDefault extractorSplit if (extractorDefault !extractors.includes(extractorDefault)) extractors.unshift(extractorDefault) extractors.sort((a, b) (a.order || 0) - (b.order || 0))这段逻辑揭示了三个关键点preset 与用户配置的 extractors 会被合并去重因此 preset 自带的提取器如 preset-attributify 自带的提取器无需手动注册即可生效默认提取器会被自动插入到列表头部unshift只要没有显式关闭最终按照order字段升序执行order越小越先运行。执行流程applyExtractors真正运行提取器的地方在 packages-engine/core/src/generator.ts 的applyExtractors中async applyExtractors(code, id?, extracted new Set()) { const context: ExtractorContext { original: code, code, id, extracted, envMode: this.config.envMode, } for (const extractor of this.config.extractors) { const result await extractor.extract?.(context) if (!result) continue if (isCountableSet(result) isCountableSet(extracted)) { for (const token of result) extracted.setCount(token, extracted.getCount(token) result.getCount(token)) } else { for (const token of result) extracted.add(token) } } return extracted }可以看到所有提取器顺序执行、结果累积到一个共享集合中若提取器返回CountableSettoken 的出现次数还会被累加UnoCSS 用次数做去重统计。这也解释了为什么提取器彼此独立、各自产出候选 token——最终引擎拿到的是一个大集合再逐一对每条规则做匹配。三、默认提取器extractorSplit与extractorDefault选项默认行为按分隔符切分官方文档明确指出By defaultextractorSplitwill always be applied, which splits the source code into tokens and directly feed to the engine.默认提取器extractorSplit的实现位于 packages-engine/core/src/extractors/split.tsexport const defaultSplitRE /[\\:]?[\s;{}]/g export const splitWithVariantGroupRE /([\\:]?[\s;]|:\(|\)|\)\s)/g export function splitCode(code: string): string[] { return code.split(defaultSplitRE) } export const extractorSplit: Extractor { name: unocss/core/extractor-split, order: 0, extract({ code }) { return splitCode(code) }, } export { extractorSplit as extractorDefault }它做的事情非常朴素用正则defaultSplitRE匹配空白、引号、反引号、分号、花括号等把源码切碎得到一个个 token 候选。这也是 UnoCSS 能在不解析任何 AST 的情况下对任意语言HTML、JS、TS、Vue、Svelte……都能工作的原因——代价是可能产生少量噪音 token它们会因匹配不到规则而被自然丢弃。用extractorDefault覆盖或禁用默认提取器官方文档给出了覆盖默认提取器的完整配置import { defineConfig } from unocss export default defineConfig({ extractors: [ // your extractors ], // disable the default extractor extractorDefault: false, // override the default extractor with your own extractorDefault: myExtractor, })extractorDefault的类型定义在 types.ts/** * Default extractor that are always applied. * By default it split the source code by whitespace and quotes. * * It maybe be replaced by preset or user config, * only one default extractor can be presented, * later one will override the previous one. * * Pass null or false to disable the default extractor. */ extractorDefault?: Extractor | null | false结合 config.ts 的实现可以得到三条准确的规则extractorDefault未设置回退到内置的extractorSplit默认行为extractorDefault: false或null完全禁用默认提取器只有你显式注册的 extractors 会运行extractorDefault: myExtractor用自定义提取器替换默认的 split 提取器同一时刻只存在一个默认提取器后设置者覆盖前者。使用场景举例如果你在特殊模板中写类名的方式与 split 切分规则冲突例如类名紧贴中文字符、被{}包裹可以禁用默认提取器并提供一个更贴合语法的自定义默认提取器从而减少误提取或漏提取。四、真实提取器实现参考从 Pug 到 Attributify官方文档建议读者参考两个真实实现来学习编写提取器参考一Pug 提取器实现位于 packages-presets/extractor-pug/src/index.ts它的核心思路是先编译、再提取const regexVueTemplate /template.*?lang[]pug[][^]*([\s\S]*?)\/template/g export default function extractorPug(options: Options {}): Extractor { async function compile(code: string, id: string) { const Pug await import(pug) try { return Pug.compile(code, { filename: id, doctype: html, ...options })() } catch { } } return { name: pug, order: -1, async extract(ctx) { if (!ctx.id) return if (ctx.id.endsWith(.pug) || ctx.id.includes(?vuetypetemplate)) { try { ctx.code await compile(ctx.code, ctx.id) || ctx.code } catch {} } else if (ctx.id.endsWith(.vue) || ctx.id.endsWith(.svelte)) { const matches Array.from(ctx.code.matchAll(regexVueTemplate)) let tail for (const match of matches) { if (match match[1]) tail \n${await compile(match[1].trim(), ctx.id)} } if (tail) ctx.code ${ctx.code}\n\n${tail} } return undefined }, } }这个实现揭示了编写提取器的几个重要技巧order: -1使其最先执行因为后续提取器含默认 split处理的是编译后的 HTML而不是 Pug 语法本身借助ctx.code的可写性直接把 Pug 源码编译成 HTML 写回ctx.code后面的提取器自然就能从 HTML 中切出类名按ctx.id区分文件类型.pug文件整体编译.vue/.svelte文件则用正则匹配template langpug片段单独编译避免破坏组件脚本部分编译失败静默降级catch {}吞掉 Pug 编译错误把错误留给后续构建流程统一处理避免提取器成为报错来源。参考二Attributify 提取器实现位于 packages-presets/preset-attributify/src/extractor.ts它展示了针对 HTML 属性语法做定向提取的思路const splitterRE /[\s;]/g const elementRE /[^\s]*\s((?:[^]*|[^]*|[^\n]*|\{[^}]*\}||[^]*?)*)/g const valuedAttributeRE /(\?|(?!\d|-{2}|-\d)[\w\u00A0-\uFFFF:!%.~-])?(?:([^]*)|([^]*)|\{([^}]*)\})?/g export const defaultIgnoreAttributes [placeholder, fill, opacity, stroke-opacity]它的核心逻辑extract函数会用elementRE匹配出 HTML 元素标签的属性区用valuedAttributeRE逐个解析属性名与属性值跳过ignoreAttributes中列出的噪音属性placeholder、fill、opacity、stroke-opacity等避免把这类属性值误当作类名无值属性输出[name]若开启trueToNonValued还会输出[nametrue]带值属性则按splitterRE拆分值并包装成[name~value]形式的 selector这正是 Attributify 变体的匹配语法对class/className属性则直接拆出类名。该提取器还支持prefixedOnlyprefix选项只提取带指定前缀的属性进一步缩小噪音。通过对照 preset-attributify 文档 中的使用示例可以更直观地理解它产出的 selector 与规则匹配的关系。五、提取器与内容来源的配合content配置提取器解决的是给定一段代码如何切出候选 token而代码从哪里来则由content配置决定。两者配合构成完整的提取管线。ContentOptions定义于 packages-engine/core/src/types.tsexport interface ContentOptions { /** * Glob patterns to extract from the file system, in addition to other content sources. * In dev mode, the files will be watched and trigger HMR. * default [] */ filesystem?: string[] /** * Inline text to be extracted */ inline?: (string | { code: string, id?: string } | (() Awaitablestring | { code: string, id?: string }))[] /** * Filters to determine whether to extract certain modules from the build tools transformation pipeline. * Currently only works for Vite and Webpack integration. * Set false to disable. */ pipeline?: false | { include?: FilterPattern // 默认 [/\.(vue|svelte|[jt]sx|vine.ts|mdx?|astro|elm|php|phtml|marko|html)($|\?)/] exclude?: FilterPattern // 默认 [/\.(css|postcss|sass|scss|less|stylus|styl)($|\?)/] } }它支持三种来源filesystem通过 glob 从文件系统读取内容dev 模式下会被监听并触发 HMRinline直接以字符串提供需要提取的文本适合运行时才知道类名的场景如从 CMS 内容中动态生成的类名pipeline从 Vite / Webpack 的模块转换管线中提取仅这两种集成生效。注意默认include并不包含.ts/.js文件exclude默认排除 CSS 类文件这是为了避免把 JS 字符串常量或样式文件内容误提取为类名。所有来源提取到的 usage 会被合并在一起见 types.ts 的注释说明。如果你在 JS 文件中以动态拼接方式使用工具类例如bg-${color}-500默认管线不会提取.ts/.js文件此时应使用content.filesystem或content.inline显式补充来源。六、编写你自己的提取器综合官方文档与源码实现编写一个提取器的最小模板如下import type { Extractor } from unocss/core const myExtractor: Extractor { // 唯一名称便于调试与去重 name: my-extractor, // order 决定执行顺序越小越先执行不填默认为 0 order: 100, // 返回 undefined / void 表示跳过本提取器 extract(ctx) { // ctx.original 原始代码只读 // ctx.code 可写前置提取器如 pug改写后的代码 // ctx.id 模块标识用于按文件类型分流 // ctx.extracted 共享的候选集合可直接往里 add if (ctx.id ctx.id.endsWith(.myext)) return extractFromMySyntax(ctx.code) }, } export default myExtractor编写时的实践要点均可在仓库真实提取器中对号入座善用ctx.id做语言分流Pug 提取器区分.pug与.vueSvelte 提取器只处理.svelte文件MDC 提取器packages-presets/extractor-mdc/src/index.ts用/\.(?:md|mdc|markdown)$/i正则只处理 Markdown 系列文件用order控制转换先后需要先编译再切分的提取器应给负数 order如 Pug 的-1需要后处理修正的给正数 order如 Svelte 提取器的100直接写入ctx.extracted是合法的Svelte 提取器packages-presets/extractor-svelte/src/index.ts就是这样做的——它把默认提取器切出的class:foo修正为foo并删除原始 token实现了对 split 结果的二次清洗extract({ id, extracted }) { if (id id.endsWith(.svelte)) { const items Array.from(extracted) items.forEach((r) { if (r.startsWith(class:)) { extracted.add(r.slice(6).replace(rightTrimRe, )) extracted.delete(r) } }) } }返回undefined表示不参与本轮提取例如文件类型不匹配这不会中断其他提取器的执行。完成编写后将提取器加入配置即可import { defineConfig } from unocss import myExtractor from ./my-extractor export default defineConfig({ extractors: [myExtractor], })七、小结提取器的定位与调试建议提取器是 UnoCSS 按需提取的第一步它直接决定了哪些 token 能进入规则匹配从而影响最终 CSS 的准确性与体积默认的extractorSplit通过 split.ts 中的defaultSplitRE按空白与引号切分覆盖绝大多数场景无需配置即可工作当需要语言感知的提取时Pug 模板、Attributify 属性、Svelte 的class:指令、Markdown 中的类名引用通过extractors数组追加对应提取器preset 一般已内置无需手动注册当默认切分行为不满足需求时用extractorDefault覆盖或禁用默认提取器提取的原料来源由content配置控制注意默认管线不提取.ts/.js文件、默认排除样式文件这两个边界。如果在实际项目中遇到某个类名没有生成对应 CSS可以优先从这条链路排查该类名是否出现在被提取的源码中content 来源→ 是否被某个提取器切出extractors→ 是否命中规则rules。结合 UnoCSS Inspector 查看最终提取到的 token 列表可以快速定位问题出在提取还是匹配环节。【免费下载链接】unocssThe instant on-demand atomic CSS engine.项目地址: https://gitcode.com/GitHub_Trending/un/unocss创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考