带你深入了解 UnoCSS 核心引擎
Wed Oct 19 2022 8 months ago
dirs #
src/
├── extractors/ # UnoCSS 提取器(提取给`UnoCSS`引擎进行解析)
│ └── ...
├── generator/ # 引擎核心功能
│ └── index.ts
├── utils/ # UnoCSS 工具函数
│ └── index.ts
└── config.ts/ # ResolveConfig
generator #
如果你想使用UnoCSS
的核心功能,你可以直接使用createGenerator
函数。
const uno = createGenerator({ // 你的配置 })
export function createGenerator(config?: UserConfig, defaults?: UserConfigDefaults) {
return new UnoGenerator(config, defaults)
}
我们可以看到,createGenerator
函数返回了一个UnoGenerator
类的实例,这个类包含了所有的核心功能。
export class UnoGenerator {
constructor(
public userConfig: UserConfig = {},
public defaults: UserConfigDefaults = {},
) {
this.config = resolveConfig(userConfig, defaults)
this.events.emit('config', this.config)
}
// ...
UnoCSS
在构造实例的时候会先去合并用户的配置和默认的配置项resolveConfig
。
resolveconfig #
于是我们进入到 config.ts
,我们来看看是如何合并用户配置的。
const config = Object.assign({}, defaults, userConfig) as UserConfigDefaults
const rawPresets = (config.presets || []).flatMap(toArray).map(resolvePreset)
const sortedPresets = [
...rawPresets.filter(p => p.enforce === 'pre'),
...rawPresets.filter(p => !p.enforce),
...rawPresets.filter(p => p.enforce === 'post'),
]
const layers = Object.assign(DEFAULT_LAYERS, ...rawPresets.map(i => i.layers), userConfig.layers)
UnoCSS
采用assign
合并配置,并对配置中的预设进行enforce
排序,也对layer
进行合并。
type mergeKey = 'rules' | 'variants' | 'extractors' | 'shortcuts' | 'preflights' | 'preprocess' | 'postprocess' | 'extendTheme' | 'safelist'
function mergePresets<T extends mergeKey>(key: T): Required<UserConfig>[T] {
return uniq([
...sortedPresets.flatMap(p => toArray(p[key] || []) as any[]),
...toArray(config[key] || []) as any[],
])
}
mergePresets
函数会对预设中的部分key
进行合并,并返回一个数组。
此举主要是为了合并多个预设中的相同字段的值进行汇总。
extractors #
const extractors = mergePresets('extractors')
if (!extractors.length)
extractors.push(extractorSplit)
extractors.sort((a, b) => (a.order || 0) - (b.order || 0))
用户可以在预设中,自定义提取器,于是我们对其进行合并,并对它的执行顺序进行排序
rules #
我们都知道,UnoCSS
有动静态规则,用户可以完全控制规则,使UnoCSS
按照其逻辑进行返回内容。
const rules = mergePresets('rules')
const rulesStaticMap: ResolvedConfig['rulesStaticMap'] = {}
const rulesSize = rules.length
const rulesDynamic = rules
.map((rule, i) => {
if (isStaticRule(rule)) {
const prefix = rule[2]?.prefix || ''
rulesStaticMap[prefix + rule[0]] = [i, rule[1], rule[2], rule]
// delete static rules so we can't skip them in matching
// but keep the order
return undefined
}
return [i, ...rule]
})
.filter(Boolean)
.reverse() as ResolvedConfig['rulesDynamic']
我们通过循环所有的规则将动静态规则分离:
// types
{
rulesStaticMap: Record<string, [number, CSSObject | CSSEntries, RuleMeta | undefined, Rule] | undefined>
rulesDynamic: [number, ...DynamicRule][] // [规则的索引, 具体的规则逻辑][]
}
UnoCSS
的动态解析逻辑为倒序解析,所以在最后会有 reverse
动作。因为静态规则是一个对象,如果遇到重复的 key
,会后者覆盖前者。
用户自定义动态规则时,优先会解析后面的规则 如果你的静态规则与
presetUno
或者其他预设规则冲突,不必担心,在mergePresets
时,优先以用户规则为主。
Check The Playground
shortcuts #
我们知道我们的shortcuts
也有动静态区分,StaticShortcutMap
DynamicShortcut<Theme>
,而且shortcuts
的类型不一,既可以是Array
Or Object
,所以我们需要对其进行处理:
export function resolveShortcuts(shortcuts: UserShortcuts): Shortcut[] {
return toArray(shortcuts).flatMap((s) => {
if (Array.isArray(s))
return [s]
return Object.entries(s)
})
}
还记得我们的rawPreset
吗?在preset
Resolve
时,我们会将shortcuts
和rules
进行一并处理
主要是因为当我们的preset
中指定了prefix
layer
时,为了使其工作在正确的预设环境中,我们需要将其进行一次Resolve
。
if (preset.prefix || preset.layer) {
const apply = (i: Rule | Shortcut) => {
if (!i[2])
i[2] = {}
const meta = i[2]
if (meta.prefix == null && preset.prefix)
meta.prefix = preset.prefix
if (meta.layer == null && preset.layer)
meta.prefix = preset.layer
}
shortcuts?.forEach(apply)
preset.rules?.forEach(apply)
}
themes #
const theme = clone([
...sortedPresets.map(p => p.theme || {}),
config.theme || {},
].reduce((a, p) => mergeDeep(a, p), {}))
;(mergePresets('extendTheme') as ThemeExtender<any>[]).forEach(extendTheme => extendTheme(theme))
我们将所有预设中的 theme
和用户自定义的 theme
进行深度合并,最后将 extendTheme
中的函数执行,将 theme
作为参数传入,这样用户就可以对 theme
进行扩展。
将来我会在
presetMini
中解析theme
,并介绍其中的用法,敬请期待。
autocomplete #
提取每个预设中的 autocomplete
,并将其合并为一个数组,最后将其作为 autocomplete
的值。
const autocomplete = {
templates: uniq(sortedPresets.map(p => toArray(p.autocomplete?.templates)).flat()),
extractors: sortedPresets.map(p => toArray(p.autocomplete?.extractors)).flat()
.sort((a, b) => (a.order || 0) - (b.order || 0)),
}
ResolveConfig
流程结束,最后返回我们的 config
。
return {
mergeSelectors: true,
warn: true,
blocklist: [],
sortLayers: layers => layers,
...config,
presets: sortedPresets,
envMode: config.envMode || 'build',
shortcutsLayer: config.shortcutsLayer || 'shortcuts',
layers,
theme,
rulesSize,
rulesDynamic,
rulesStaticMap,
preprocess: mergePresets('preprocess') as Preprocessor[],
postprocess: mergePresets('postprocess') as Postprocessor[],
preflights: mergePresets('preflights'),
autocomplete,
variants: mergePresets('variants').map(normalizeVariant),
shortcuts: resolveShortcuts(mergePresets('shortcuts')),
extractors,
safelist: mergePresets('safelist'),
}
generate #
UnoCSS
的 Generate
核心,主要是将我们的 input
解析生成 css
。
async generate(
input: string | Set<string> | string[],
options: GenerateOptions = {},
): Promise<GenerateResult> {
const {
id, // 在提取token阶段用到文件id
scope, // 生成的css的作用域
preflights = true, // 是否生成preflights
safelist = true, // 是否应用safelist
minify = false, // 是否压缩css
} = options
// ...
}
首先我们先观察函数的参数与返回值,我们可以看到,input
可以是 string
Set
Array
,options
有 id
scope
preflights
safelist
minify
,返回值是一个Promise
GenerateResult
。
applyextractors #
UnoCSS
会将 input
交给 extractors
进行解析提取token
,最终提取出的 token
会是Set
string
的形式,确保每个 token
是唯一的。
const tokens: Readonly<Set<string>> = isString(input)
? await this.applyExtractors(input, id)
: Array.isArray(input)
? new Set(input)
: input
因为我们的 config.extractors
是一个数组,所以我们需要遍历它,将 input
交给每个 extractor
进行解析,最后将所有解析出来的 token
合并为一个 Set
。
const context: ExtractorContext = {
get original() { return code },
code,
id,
}
for (const extractor of this.config.extractors) {
const result = await extractor.extract(context)
if (result) {
for (const token of result)
set.add(token)
}
}
如果我们应用了safelist
,那么我们还需要将safelist
中的token
合并到set
中。
if (safelist)
this.config.safelist.forEach(s => tokens.add(s))
最后我们得到了我们需要去解析的所有token
,接下来将这些token
解析成css
。