Appearance
终极玩法:创建你自己的预设
到目前为止,我们已经学习了 UnoCSS 的所有核心概念:规则、变体、快捷方式、主题、转换器等。现在是时候将这些知识整合起来,创建属于你自己的预设了。
预设(Preset)是 UnoCSS 配置的封装和复用单元。通过创建预设,你可以将公司的设计系统、个人的常用配置打包成可复用的模块,在多个项目间共享。
本章将从零开始,带你创建一个完整的自定义预设。
1. 预设的本质
1.1 预设是什么
预设本质上是一个返回配置对象的函数。这个配置对象可以包含规则、变体、快捷方式、主题、转换器等任何 UnoCSS 支持的配置项。
ts
import { Preset } from 'unocss'
export function myPreset(): Preset {
return {
name: 'my-preset',
rules: [
// 规则
],
variants: [
// 变体
],
shortcuts: {
// 快捷方式
},
theme: {
// 主题
},
}
}1.2 预设的合并
当项目使用多个预设时,UnoCSS 会将它们的配置深度合并。后面的预设可以覆盖或扩展前面预设的配置。
ts
export default defineConfig({
presets: [
presetUno(), // 基础预设
presetIcons(), // 图标预设
myPreset(), // 你的自定义预设
],
})1.3 预设的参数
预设函数可以接受参数,让使用者自定义行为:
ts
interface MyPresetOptions {
prefix?: string
colors?: Record<string, string>
}
export function myPreset(options: MyPresetOptions = {}): Preset {
const { prefix = '', colors = {} } = options
return {
name: 'my-preset',
// 根据 options 生成配置
}
}2. 创建基础预设
让我们从一个简单的预设开始,逐步添加功能。
2.1 项目结构
创建一个新的 npm 包来存放预设:
my-preset/
├── src/
│ ├── index.ts
│ ├── rules.ts
│ ├── shortcuts.ts
│ ├── theme.ts
│ └── variants.ts
├── package.json
└── tsconfig.json2.2 package.json
json
{
"name": "unocss-preset-my",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.js"
}
},
"peerDependencies": {
"unocss": ">=0.50.0"
},
"devDependencies": {
"unocss": "^0.58.0",
"typescript": "^5.0.0"
}
}2.3 入口文件
ts
// src/index.ts
import type { Preset } from 'unocss'
import { rules } from './rules'
import { shortcuts } from './shortcuts'
import { theme } from './theme'
import { variants } from './variants'
export interface MyPresetOptions {
prefix?: string
}
export function presetMy(options: MyPresetOptions = {}): Preset {
return {
name: 'unocss-preset-my',
rules,
shortcuts,
theme,
variants,
}
}
export default presetMy3. 编写预设规则
规则是预设的核心,定义了类名到 CSS 的映射。
3.1 静态规则
ts
// src/rules.ts
import type { Rule } from 'unocss'
export const rules: Rule[] = [
// 简单的静态规则
['flex-center', { display: 'flex', alignItems: 'center', justifyContent: 'center' }],
['absolute-center', { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }],
// 可见性
['invisible', { visibility: 'hidden' }],
['visible', { visibility: 'visible' }],
]3.2 动态规则
ts
export const rules: Rule[] = [
// 动态间距规则
[/^space-(\d+)$/, ([, d]) => ({ gap: `${Number(d) * 0.25}rem` })],
// 动态颜色规则
[/^brand-(\w+)-(\d+)$/, ([, color, shade], { theme }) => {
const colorValue = theme.colors?.brand?.[color]?.[shade]
if (colorValue) {
return { color: colorValue }
}
}],
// 带选项的规则
[/^truncate-(\d+)$/, ([, lines]) => ({
display: '-webkit-box',
'-webkit-line-clamp': lines,
'-webkit-box-orient': 'vertical',
overflow: 'hidden',
})],
]3.3 规则分层
可以为规则指定层级:
ts
export const rules: Rule[] = [
['base-styles', {
fontFamily: 'system-ui, sans-serif',
lineHeight: '1.5',
}, { layer: 'base' }],
]4. 编写预设变体
变体扩展了类名的应用条件。
4.1 简单变体
ts
// src/variants.ts
import type { Variant } from 'unocss'
export const variants: Variant[] = [
// 打印媒体查询
{
name: 'print',
match(matcher) {
if (!matcher.startsWith('print:')) return matcher
return {
matcher: matcher.slice(6),
parent: '@media print',
}
},
},
// 横屏/竖屏
{
name: 'landscape',
match(matcher) {
if (!matcher.startsWith('landscape:')) return matcher
return {
matcher: matcher.slice(10),
parent: '@media (orientation: landscape)',
}
},
},
]4.2 选择器变体
ts
export const variants: Variant[] = [
// 第一个子元素
{
name: 'first-child',
match(matcher) {
if (!matcher.startsWith('first-child:')) return matcher
return {
matcher: matcher.slice(12),
selector: s => `${s} > *:first-child`,
}
},
},
// 支持 RTL
{
name: 'rtl',
match(matcher) {
if (!matcher.startsWith('rtl:')) return matcher
return {
matcher: matcher.slice(4),
selector: s => `[dir="rtl"] ${s}`,
}
},
},
]4.3 参数化变体
ts
export const variants: Variant[] = [
// 支持任意断点
{
name: 'at-breakpoint',
match(matcher) {
const match = matcher.match(/^at-\[(\d+)px\]:/)
if (!match) return matcher
return {
matcher: matcher.slice(match[0].length),
parent: `@media (min-width: ${match[1]}px)`,
}
},
},
]5. 编写预设主题
主题定义设计令牌。
5.1 颜色主题
ts
// src/theme.ts
import type { Theme } from 'unocss/preset-uno'
export const theme: Theme = {
colors: {
brand: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
},
secondary: {
// ...
},
},
semantic: {
success: '#22c55e',
warning: '#f59e0b',
error: '#ef4444',
info: '#3b82f6',
},
},
}5.2 排版主题
ts
export const theme: Theme = {
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
serif: ['Merriweather', 'Georgia', 'serif'],
mono: ['JetBrains Mono', 'monospace'],
},
fontSize: {
xs: ['0.75rem', { lineHeight: '1rem' }],
sm: ['0.875rem', { lineHeight: '1.25rem' }],
base: ['1rem', { lineHeight: '1.5rem' }],
lg: ['1.125rem', { lineHeight: '1.75rem' }],
xl: ['1.25rem', { lineHeight: '1.75rem' }],
'2xl': ['1.5rem', { lineHeight: '2rem' }],
},
}5.3 动画主题
ts
export const theme: Theme = {
animation: {
keyframes: {
'slide-in': '{from{transform:translateX(-100%)}to{transform:translateX(0)}}',
'slide-out': '{from{transform:translateX(0)}to{transform:translateX(100%)}}',
'fade-in': '{from{opacity:0}to{opacity:1}}',
'scale-in': '{from{transform:scale(0.9);opacity:0}to{transform:scale(1);opacity:1}}',
},
durations: {
'slide-in': '300ms',
'slide-out': '300ms',
'fade-in': '200ms',
'scale-in': '200ms',
},
timingFns: {
'slide-in': 'ease-out',
'slide-out': 'ease-in',
'fade-in': 'ease-out',
'scale-in': 'ease-out',
},
},
}6. 编写预设快捷方式
快捷方式封装常用的类组合。
6.1 组件快捷方式
ts
// src/shortcuts.ts
export const shortcuts = {
// 按钮
'btn': 'px-4 py-2 rounded-lg font-medium transition-colors inline-flex items-center justify-center gap-2',
'btn-primary': 'btn bg-brand-primary-500 text-white hover:bg-brand-primary-600',
'btn-secondary': 'btn bg-gray-200 text-gray-800 hover:bg-gray-300',
'btn-ghost': 'btn bg-transparent hover:bg-gray-100',
// 输入框
'input': 'w-full px-4 py-2 border border-gray-300 rounded-lg focus:border-brand-primary-500 focus:ring-2 focus:ring-brand-primary-500/20 outline-none transition-colors',
// 卡片
'card': 'bg-white rounded-xl shadow-md overflow-hidden',
'card-header': 'px-6 py-4 border-b border-gray-100',
'card-body': 'px-6 py-4',
'card-footer': 'px-6 py-4 border-t border-gray-100 bg-gray-50',
}6.2 动态快捷方式
ts
export const shortcuts = [
// 静态快捷方式
{ btn: 'px-4 py-2 rounded-lg font-medium transition-colors' },
// 动态快捷方式
[/^btn-(\w+)$/, ([, color]) => `btn bg-${color}-500 text-white hover:bg-${color}-600`],
[/^badge-(\w+)$/, ([, color]) => `inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-${color}-100 text-${color}-800`],
]7. 添加 Preflight
预设可以包含基础样式重置。
7.1 Preflight 配置
ts
// src/index.ts
export function presetMy(options: MyPresetOptions = {}): Preset {
return {
name: 'unocss-preset-my',
rules,
shortcuts,
theme,
variants,
preflights: [
{
layer: 'base',
getCSS: ({ theme }) => `
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-family: ${theme.fontFamily?.sans?.join(', ') || 'system-ui, sans-serif'};
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
`,
},
],
}
}8. 预设选项设计
好的预设应该提供灵活的配置选项。
8.1 完整的选项接口
ts
export interface MyPresetOptions {
// 前缀
prefix?: string
// 功能开关
preflight?: boolean
darkMode?: 'class' | 'media'
// 主题扩展
colors?: Record<string, Record<string, string>>
fonts?: Record<string, string[]>
// 其他选项
important?: boolean
}8.2 处理选项
ts
export function presetMy(options: MyPresetOptions = {}): Preset {
const {
prefix = '',
preflight = true,
darkMode = 'class',
colors = {},
fonts = {},
important = false,
} = options
// 合并颜色
const mergedColors = {
...defaultColors,
...colors,
}
// 根据选项生成规则
const finalRules = rules.map(rule => {
if (important && Array.isArray(rule) && typeof rule[1] === 'object') {
// 为所有规则添加 !important
const declarations = rule[1] as Record<string, string>
const importantDeclarations = Object.fromEntries(
Object.entries(declarations).map(([k, v]) => [k, `${v} !important`])
)
return [rule[0], importantDeclarations, rule[2]]
}
return rule
})
return {
name: 'unocss-preset-my',
rules: finalRules,
shortcuts,
theme: {
colors: mergedColors,
fontFamily: { ...defaultFonts, ...fonts },
},
variants,
preflights: preflight ? [...defaultPreflights] : [],
}
}9. 测试预设
预设应该有完善的测试。
9.1 单元测试
ts
// tests/rules.test.ts
import { createGenerator } from '@unocss/core'
import { presetMy } from '../src'
import { describe, it, expect } from 'vitest'
describe('preset-my rules', () => {
const uno = createGenerator({
presets: [presetMy()],
})
it('should generate flex-center', async () => {
const { css } = await uno.generate('flex-center')
expect(css).toContain('display: flex')
expect(css).toContain('align-items: center')
expect(css).toContain('justify-content: center')
})
it('should handle dynamic rules', async () => {
const { css } = await uno.generate('space-4')
expect(css).toContain('gap: 1rem')
})
})9.2 快照测试
ts
it('should match snapshot', async () => {
const { css } = await uno.generate('btn btn-primary card')
expect(css).toMatchSnapshot()
})10. 发布预设
10.1 构建
使用 tsup 或 unbuild 构建预设:
bash
npx tsup src/index.ts --format cjs,esm --dts10.2 发布到 npm
bash
npm login
npm publish10.3 文档
好的预设需要完善的文档,包括安装说明、配置选项、可用的类名列表和示例代码。
11. 小结
本章从零开始创建了一个完整的 UnoCSS 预设。
预设是配置的封装单元,本质是一个返回配置对象的函数。预设可以接受参数,让使用者自定义行为。多个预设的配置会被深度合并。
预设结构包括规则(定义类名到 CSS 的映射,支持静态和动态规则)、变体(扩展类名的应用条件)、主题(定义设计令牌如颜色、字体、间距等)、快捷方式(封装常用的类组合)和 Preflight(基础样式重置)。
预设选项设计应该考虑功能开关、主题扩展、行为修改等。好的选项设计能让预设更灵活,适应不同项目的需求。
测试对于预设很重要,包括单元测试验证各个功能,快照测试确保输出稳定。
发布预设需要正确的构建配置和完善的文档。好的预设应该有清晰的 API、详细的使用说明和丰富的示例。
创建自己的预设是 UnoCSS 进阶的标志。通过预设,你可以将设计系统、团队规范打包成可复用的模块,大大提升开发效率和一致性。
下一章我们将进入生态与工具链部分,学习如何在各种框架中集成 UnoCSS。