logologo

带你深入了解 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 时,我们会将shortcutsrules进行一并处理 主要是因为当我们的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 #

UnoCSSGenerate 核心,主要是将我们的 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 Arrayoptionsid 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

蜀ICP备2022005364号 2022 © Chris