Anthony Fu 的 Vue3 开发规范完整解读
本文基于 antfu/skills 仓库整理翻译,全面解析 Anthony Fu 在 Vue 3 生态中的编码规范、最佳实践和工具链推荐。作为 Vue 核心团队成员、Vite 团队成员以及众多开源项目的作者(VueUse、UnoCSS、Vitest、Slidev 等),Anthony 的开发理念深刻影响了现代 Vue 开发生态。
第一部分:编码实践与工具链
代码组织原则
单一职责原则
保持文件和函数专注于单一职责。当文件超过 200-300 行时,考虑拆分:
// ❌ 避免:一个文件包含所有逻辑
// UserManager.ts (800 lines)
export class UserManager {
validateUser() { /* 50 lines */ }
fetchUserData() { /* 100 lines */ }
updateUserProfile() { /* 150 lines */ }
// ...
}
// ✅ 推荐:按职责拆分
// validation.ts
export function validateUser(user: User) { /* ... */ }
// api.ts
export function fetchUserData(id: string) { /* ... */ }
// profile.ts
export function updateUserProfile(data: ProfileData) { /* ... */ }
类型与常量分离
// types.ts
export interface User {
id: string
name: string
role: UserRole
}
export type UserRole = 'admin' | 'user' | 'guest'
// constants.ts
export const DEFAULT_PAGE_SIZE = 20
export const MAX_RETRIES = 3
export const API_ENDPOINTS = {
users: '/api/users',
posts: '/api/posts',
} as const
// user-service.ts
import type { User, UserRole } from './types'
import { API_ENDPOINTS } from './constants'
export async function fetchUsers(): Promise<User[]> {
const response = await fetch(API_ENDPOINTS.users)
return response.json()
}
运行时环境标注
编写同构代码时,为环境特定的代码添加明确的注释:
// ✅ 明确标注环境依赖
// @env browser
export function getWindowSize() {
return {
width: window.innerWidth,
height: window.innerHeight,
}
}
// @env node
export function readConfigFile() {
return fs.readFileSync('./config.json', 'utf-8')
}
// ✅ 同构代码无需标注
export function formatDate(date: Date): string {
return date.toISOString()
}
TypeScript 最佳实践
显式返回类型
// ❌ 避免:隐式返回类型
export function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0)
}
// ✅ 推荐:显式返回类型
export function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0)
}
// ✅ 复杂类型提取为类型别名
export type AsyncResult<T> = Promise<{ data: T; error: null } | { data: null; error: Error }>
export function fetchData<T>(url: string): AsyncResult<T> {
// ...
}
避免复杂内联类型
// ❌ 避免:复杂内联类型
function processUsers(
users: Array<{
id: string
profile: {
name: string
email: string
settings: {
theme: 'light' | 'dark'
notifications: boolean
}
}
}>
) {
// ...
}
// ✅ 推荐:提取类型定义
interface UserSettings {
theme: 'light' | 'dark'
notifications: boolean
}
interface UserProfile {
name: string
email: string
settings: UserSettings
}
interface User {
id: string
profile: UserProfile
}
function processUsers(users: User[]) {
// ...
}
注释哲学
解释"为什么"而非"怎么做"
// ❌ 避免:无意义的注释
// 循环遍历用户数组
users.forEach(user => {
// 打印用户名
console.log(user.name)
})
// ✅ 推荐:解释为什么这样做
// 使用 setTimeout 0 延迟执行,确保 DOM 更新完成后再计算高度
setTimeout(() => {
const height = element.offsetHeight
}, 0)
// ✅ 解释非直观的业务逻辑
// 价格计算需要先扣除折扣,再加税费,顺序不能颠倒
// 因为税费基于折后价计算(符合当地税法要求)
const finalPrice = (price - discount) * (1 + taxRate)
测试规范(Vitest)
文件组织
src/
utils/
format.ts # 源代码
format.test.ts # 测试文件
components/
Button.vue
Button.test.ts
测试结构
// format.test.ts
import { describe, it, expect } from 'vitest'
import { formatCurrency, formatDate } from './format'
describe('formatCurrency', () => {
it('should format USD correctly', () => {
expect(formatCurrency(1234.56, 'USD')).toBe('$1,234.56')
})
it('should handle zero', () => {
expect(formatCurrency(0, 'USD')).toBe('$0.00')
})
it('should round to 2 decimal places', () => {
expect(formatCurrency(1.234, 'USD')).toBe('$1.23')
})
})
describe('formatDate', () => {
it('should match snapshot', () => {
const date = new Date('2024-01-15T10:30:00Z')
expect(formatDate(date)).toMatchSnapshot()
})
})
工具链速查
@antfu/ni - 通用包管理器命令
| 命令 |
npm |
yarn |
pnpm |
bun |
ni |
npm install |
yarn install |
pnpm install |
bun install |
nr dev |
npm run dev |
yarn run dev |
pnpm run dev |
bun run dev |
nu |
npm update |
yarn upgrade |
pnpm update |
bun update |
nun lodash |
npm uninstall lodash |
yarn remove lodash |
pnpm remove lodash |
bun remove lodash |
nci |
npm ci |
yarn install --frozen-lockfile |
pnpm install --frozen-lockfile |
bun install --frozen-lockfile |
nlx vitest |
npx vitest |
yarn dlx vitest |
pnpm dlx vitest |
bunx vitest |
TypeScript 配置标准
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"jsx": "preserve",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"isolatedModules": true,
"noUncheckedIndexedAccess": true,
"paths": {
"~/*": ["./src/*"]
}
}
}
关键配置说明:
-
moduleResolution: "bundler" - 适配 Vite/Rollup 等现代打包工具
-
noUncheckedIndexedAccess: true - 索引访问返回 T | undefined,更安全
-
strict: true - 启用所有严格类型检查
ESLint 配置
# 安装
pnpm add -D @antfu/eslint-config eslint
# 运行
pnpm run lint --fix
// eslint.config.js
import antfu from '@antfu/eslint-config'
export default antfu({
vue: true,
typescript: true,
formatters: {
css: true,
html: true,
markdown: true,
},
})
Git Hooks 配置
# 安装
pnpm add -D simple-git-hooks lint-staged
// package.json
{
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": "eslint --fix"
}
}
pnpm Catalogs 最佳实践
# pnpm-workspace.yaml
catalogs:
# 生产依赖
prod:
vue: ^3.5.0
pinia: ^2.2.0
# 内联依赖(会被打包)
inlined:
lodash-es: ^4.17.21
# 开发依赖
dev:
vitest: ^2.0.0
typescript: ^5.6.0
# 前端特定依赖
frontend:
unocss: ^0.63.0
// package.json
{
"dependencies": {
"vue": "catalog:prod",
"lodash-es": "catalog:inlined"
},
"devDependencies": {
"vitest": "catalog:dev",
"unocss": "catalog:frontend"
}
}
第二部分:Vue 3 核心规范
基于 Vue 3.5,优先使用 TypeScript 和 <script setup>
偏好设定
| 场景 |
推荐方案 |
原因 |
| 语言选择 |
TypeScript |
类型安全、更好的 IDE 支持 |
| 脚本格式 |
<script setup lang="ts"> |
更简洁的语法、更好的性能 |
| 响应式选择 |
shallowRef > ref
|
大多数场景足够用,性能更好 |
| API 风格 |
Composition API |
更好的逻辑复用和类型推导 |
| Props 解构 |
❌ 不推荐 |
会丢失响应式 |
标准组件模板
<script setup lang="ts">
import { computed, ref, watch, onMounted } from 'vue'
import type { ComponentPublicInstance } from 'vue'
// Props 定义
interface Props {
title: string
count?: number
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
disabled: false,
})
// Emits 定义
interface Emits {
update: [value: number]
submit: [data: { name: string }]
}
const emit = defineEmits<Emits>()
// Model 双向绑定
const modelValue = defineModel<string>({ required: true })
// 响应式状态
const isLoading = ref(false)
const items = ref<Item[]>([])
// 计算属性
const displayTitle = computed(() => {
return props.disabled ? `${props.title} (已禁用)` : props.title
})
// 侦听器
watch(() => props.count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})
// 生命周期
onMounted(() => {
console.log('Component mounted')
})
// 方法
function handleClick() {
emit('update', props.count + 1)
}
// 暴露给父组件(defineExpose)
defineExpose({
focus: () => {
// 暴露的方法
},
})
</script>
<template>
<div>
<h1>{{ displayTitle }}</h1>
<button @click="handleClick" :disabled="disabled">
Count: {{ count }}
</button>
</div>
</template>
<style scoped>
/* scoped 样式 */
</style>
关键导入速查
// 核心响应式 API
import {
ref, // 深层响应式
shallowRef, // 浅层响应式(推荐)
reactive, // 深层响应式对象
shallowReactive, // 浅层响应式对象
readonly, // 只读代理
computed, // 计算属性
watch, // 侦听器
watchEffect, // 副作用侦听器
} from 'vue'
// 生命周期钩子
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount,
} from 'vue'
// 组件通信
import {
defineProps,
defineEmits,
defineModel,
defineExpose,
defineSlots,
provide,
inject,
} from 'vue'
// 工具函数
import {
nextTick, // 等待 DOM 更新
toRef, // 转换为 ref
toRefs, // 解构保持响应式
unref, // 解包 ref
isRef, // 判断是否 ref
markRaw, // 标记为非响应式
} from 'vue'
// 类型工具
import type {
Ref,
ComputedRef,
ComponentPublicInstance,
PropType,
} from 'vue'
ref vs shallowRef 性能对比
// ref - 深层响应式(递归代理所有层级)
const deepState = ref({
user: {
profile: {
name: 'John',
settings: {
theme: 'dark',
},
},
},
})
// 任何层级的修改都会触发响应
deepState.value.user.profile.settings.theme = 'light' // ✅ 响应式
// shallowRef - 浅层响应式(只代理第一层)
const shallowState = shallowRef({
user: {
profile: {
name: 'John',
settings: {
theme: 'dark',
},
},
},
})
// 只有整体替换才会触发响应
shallowState.value.user.profile.settings.theme = 'light' // ❌ 不会触发
shallowState.value = { ...shallowState.value } // ✅ 触发响应
// 性能建议:大部分场景使用 shallowRef 足够
第三部分:Vue 3 最佳实践与常见陷阱
响应式系统
| 问题 |
建议 |
| ref vs reactive 如何选择? |
优先使用 ref。ref 可以存储任何类型,而 reactive 只能用于对象。ref 在重新赋值时保持响应性,reactive 不行。 |
| 什么时候用 shallowRef? |
存储大型数据结构(如长列表、复杂嵌套对象)时,用 shallowRef 避免深层代理的性能开销。更新时需要整体替换对象。 |
| 如何阻止对象变成响应式? |
使用 markRaw()。例如存储第三方库实例(Chart.js、Monaco Editor)时,避免不必要的代理。 |
| 多次修改 ref 会触发多次渲染吗? |
不会。Vue 会将同一 tick 内的更新批量处理。如需立即看到 DOM 变化,使用 await nextTick()。 |
| ref 解包规则是什么? |
在模板中自动解包({{ count }})。在 reactive 对象中自动解包(state.count)。在数组和 Map/Set 中不解包(需要 .value)。 |
| 解构 props 会丢失响应性吗? |
是的。使用 toRefs(props) 或 toRef(props, 'key') 保持响应性,或在 computed/watch 中访问 props.xxx。 |
计算属性
| 问题 |
建议 |
| 计算属性可以有副作用吗? |
不应该。计算属性应该是纯函数,只做计算和返回值。副作用应该放在 watch 或 watchEffect 中。 |
| 为什么计算属性是只读的? |
默认只读,但可以提供 setter。推荐只读设计,修改应通过源数据。 |
| 计算属性什么时候重新计算? |
只有当依赖的响应式数据变化时才重新计算(懒执行 + 缓存)。这是相比 method 的主要优势。 |
| 计算属性的条件依赖如何工作? |
只追踪当前执行分支的依赖。if (flag) return a 时只追踪 flag 和 a,不追踪 else 分支。 |
| 计算属性内使用 array.map 有性能问题吗? |
有。每次重新计算都会创建新数组。考虑使用 shallowRef 存储映射结果,或在 watch 中手动更新。 |
侦听器
| 问题 |
建议 |
| watch 的 getter 函数是什么? |
watch(() => obj.count, ...) 中的箭头函数。推荐用 getter 而非直接传对象,可以精确控制依赖。 |
| deep: true 有性能问题吗? |
有。深度侦听需要遍历对象的所有属性。只在必要时使用,或用 getter 函数精确指定依赖。 |
| immediate: true 的执行时机是什么? |
立即执行一次,此时 DOM 可能未挂载。需要访问 DOM 时注意判断。 |
| flush 选项有什么区别? |
pre(默认):DOM 更新前执行。post:DOM 更新后执行。sync:同步执行(避免使用)。 |
| 如何在侦听器中访问旧值? |
watch(source, (newVal, oldVal) => {})。注意对象类型的 oldVal 和 newVal 可能指向同一个引用。 |
| watch vs watchEffect 如何选择? |
watchEffect:自动追踪依赖,简洁。watch:显式指定侦听源,可访问旧值,更精确。 |
组件通信
| 问题 |
建议 |
| 可以修改 props 吗? |
不可以。Props 是单向数据流,只读。需要修改时,emit 事件或使用 defineModel。 |
| 自定义事件会冒泡吗? |
不会。Vue 的自定义事件不像原生 DOM 事件那样冒泡,只触发直接父组件的监听器。 |
| 组件名应该用什么格式? |
PascalCase(MyComponent.vue)。在模板中可以用 <MyComponent> 或 <my-component>,推荐前者。 |
| defineExpose 何时使用? |
当需要父组件通过 ref 调用子组件方法时。默认 <script setup> 不暴露任何内容。 |
| 如何获取组件实例的类型? |
InstanceType<typeof MyComponent>,配合 ref<InstanceType<typeof MyComponent>>()。 |
Props 与 Emits
| 问题 |
建议 |
| Boolean props 的转换规则? |
<MyComp disabled> 等价于 :disabled="true"。<MyComp> 则是 undefined(除非有默认值)。 |
| 解构 props 会丢失响应性吗? |
是的。const { title } = defineProps() 会丢失响应性。使用 toRefs 或直接访问 props.title。 |
| props 命名约定是什么? |
JS 中用 camelCase,HTML 中用 kebab-case。defineProps<{ userName: string }>() → <Comp user-name="John">。 |
| emit 事件命名约定? |
JS 中用 camelCase,HTML 中用 kebab-case。emit('updateValue') → @update-value="handler"。 |
| defineModel 的优势是什么? |
简化 v-model 实现,自动生成 prop 和 emit。支持修饰符(.trim、.number 等)。 |
模板语法
| 问题 |
建议 |
| v-html 安全吗? |
不安全。可能导致 XSS 攻击。只用于可信内容,或使用 DOMPurify 等库清理。 |
| v-if 和 v-for 能同时用吗? |
Vue 3 中 v-if 优先级高于 v-for,但不推荐同时使用。应该用 computed 过滤或嵌套 template。 |
| v-if vs v-show 如何选择? |
v-if:条件渲染,切换开销高。v-show:CSS 切换,初始渲染开销高。频繁切换用 v-show。 |
| key 的作用是什么? |
帮助 Vue 识别节点,优化 diff 算法。列表渲染必须提供唯一 key,避免用 index。 |
| 如何绑定多个属性? |
v-bind="attrs" 可以一次性绑定对象的所有属性。例如 v-bind="{ id: 'foo', class: 'bar' }"。 |
表单与 v-model
| 问题 |
建议 |
| defineModel 的修饰符如何使用? |
内置 .trim、.number、.lazy。自定义修饰符通过 defineModel 的第二个参数处理。 |
| v-model 在组件上的原理? |
语法糖::modelValue="value" @update:modelValue="value = $event"。多个 v-model:v-model:title。 |
| 如何在 v-model 更新后访问 DOM? |
使用 await nextTick(),因为 Vue 异步更新 DOM。 |
| textarea 的 v-model 和插值的区别? |
<textarea v-model="text"> 正确。<textarea>{{ text }}</textarea> 不生效,textarea 不支持插值。 |
事件处理与修饰符
| 问题 |
建议 |
| .once 修饰符如何工作? |
事件只触发一次后自动移除监听器。@click.once="handler"。 |
| .exact 修饰符的作用? |
精确匹配修饰键。@click.ctrl.exact 只在按下 Ctrl(无其他键)时触发。 |
| .passive 和 .prevent 冲突吗? |
冲突。.passive 告诉浏览器不调用 preventDefault(),两者不能同时使用。 |
| 自定义事件可以用修饰符吗? |
可以,但需要在子组件中通过 defineEmits 的第二个参数手动实现验证逻辑。 |
生命周期
| 问题 |
建议 |
| 生命周期钩子必须同步注册吗? |
是的。必须在 setup 或 <script setup> 的同步代码中调用,不能在 setTimeout 或 async 函数中。 |
| onUpdated 钩子性能如何? |
会在任何响应式数据变化导致的重新渲染后调用,可能频繁触发。谨慎使用,考虑用 watch 替代。 |
| 如何在组件外部注册生命周期? |
使用 effectScope 创建作用域,在其中注册钩子。 |
插槽
| 问题 |
建议 |
| 插槽的作用域是什么? |
默认插槽只能访问父组件的数据。作用域插槽通过 v-slot="slotProps" 接收子组件传递的数据。 |
| defineSlots 的作用? |
仅用于类型定义,帮助 TypeScript 推导插槽的 props 类型。不影响运行时。 |
| 插槽的 fallback content 是什么? |
<slot>默认内容</slot> 中的默认内容,当父组件不提供插槽内容时显示。 |
| 动态插槽名如何使用? |
v-slot:[dynamicSlotName] 或 #[dynamicSlotName]。 |
Provide / Inject
| 问题 |
建议 |
| 应该用什么作为 injection key? |
使用 Symbol 而非字符串,避免命名冲突。export const userKey = Symbol('user')。 |
| 注入的数据可以修改吗? |
可以,但建议 mutations 集中在 provider 组件,通过提供修改方法而非直接暴露响应式状态。 |
| 如何为 inject 提供类型? |
const user = inject<User>(userKey) 或在定义 key 时指定 InjectionKey<User>。 |
组合式函数
| 问题 |
建议 |
| 命名约定是什么? |
以 use 开头,camelCase。例如 useMouse、useFetch。 |
| 返回值应该是什么? |
返回包含响应式状态和方法的对象。使用 readonly() 保护内部状态。 |
| 何时使用 options 对象模式? |
参数超过 2 个时推荐。useFetch(url, { method, headers, onSuccess })。 |
| 组合式函数可以嵌套调用吗? |
可以。一个组合式函数可以调用其他组合式函数。 |
// 示例:标准组合式函数
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.clientX
y.value = event.clientY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return {
x: readonly(x),
y: readonly(y),
}
}
Composition API
| 问题 |
建议 |
| 为什么用 Composition API 替代 mixin? |
Mixin 有命名冲突、来源不清晰、难以重用等问题。Composition API 通过函数组合解决这些问题。 |
| Composition API 和 React Hooks 有什么区别? |
Vue 的 setup 只执行一次,不受闭包陷阱影响。React Hooks 每次渲染都执行,需要依赖数组。 |
| 何时仍然使用 Options API? |
简单组件、团队不熟悉 Composition API、维护老代码时可以使用 Options API。 |
自定义指令
| 问题 |
建议 |
| 必须清理副作用吗? |
是的。在 unmounted 钩子中清理事件监听器、定时器等,避免内存泄漏。 |
| 指令命名约定? |
以 v 开头。注册时用 camelCase(vFocus),使用时用 kebab-case(v-focus)。 |
| 可以在组件上使用指令吗? |
可以,但不推荐。指令会应用到组件的根元素,多根元素组件会报警告。 |
过渡与动画
| 问题 |
建议 |
| Transition 只能包含单个子元素吗? |
是的。多个元素需要用 v-if / v-else 或 TransitionGroup。 |
| 为什么列表项需要 key? |
TransitionGroup 使用 key 追踪元素移动,实现平滑的移动动画。 |
| mode 属性的作用? |
out-in:旧元素先离开,新元素再进入。in-out:新元素先进入,旧元素再离开。 |
| 如何自定义动画时长? |
通过 duration prop:<Transition :duration="500"> 或 { enter: 500, leave: 800 }。 |
KeepAlive
| 问题 |
建议 |
| max 属性的作用? |
限制缓存组件数量,超出时移除最久未访问的。<KeepAlive :max="10">。 |
| 组件必须有 name 属性吗? |
使用 include / exclude 时需要。<script setup> 组件名默认是文件名。 |
| 特殊生命周期钩子? |
onActivated(激活时)、onDeactivated(停用时)。用于处理缓存组件的状态恢复。 |
异步组件
| 问题 |
建议 |
| delay 选项的作用? |
延迟显示加载状态,避免加载很快时出现闪烁。默认 200ms。 |
| hydration 策略是什么? |
Vue 3.5+ 支持延迟 hydration:defineAsyncComponent({ loader, hydrate: 'visible' })。 |
TypeScript 集成
| 问题 |
建议 |
| 如何为 defineProps 提供类型? |
基于类型:defineProps<{ title: string }>()。基于运行时:defineProps({ title: String })。推荐前者。 |
| withDefaults 如何使用? |
withDefaults(defineProps<Props>(), { count: 0 }),为类型定义的 props 提供默认值。 |
| 如何获取组件实例类型? |
InstanceType<typeof MyComponent>,用于 ref 的类型标注。 |
// 完整示例
import MyComponent from './MyComponent.vue'
const compRef = ref<InstanceType<typeof MyComponent>>()
onMounted(() => {
compRef.value?.focus() // 类型安全的方法调用
})
SSR 注意事项
| 问题 |
建议 |
| 如何避免跨请求状态污染? |
每个请求创建新的应用实例。避免在模块顶层创建响应式状态。 |
| 服务端可以使用哪些 API? |
不能用 window、document 等浏览器 API。生命周期只有 setup、onServerPrefetch。 |
| getSSRProps 的作用? |
在 SSR 时修改组件 props,常用于注入服务端数据。 |
性能优化
| 问题 |
建议 |
| props 稳定性为什么重要? |
子组件使用 shallowRef 时,props 引用变化会触发重新渲染。尽量保持 props 引用稳定。 |
| 何时使用虚拟滚动? |
渲染超过 1000 项的列表时。使用 vue-virtual-scroller 等库。 |
| v-once 和 v-memo 的区别? |
v-once:只渲染一次,永不更新。v-memo:条件性跳过更新,依赖数组未变时复用。 |
SFC 特性
| 问题 |
建议 |
| 如何在 scoped 样式中修改子组件? |
使用 :deep() 伪类:.parent :deep(.child) { }。 |
| scoped CSS 的限制? |
不影响子组件的根元素(会自动添加 scoped 属性)。深层元素需要 :deep()。 |
插件开发
| 问题 |
建议 |
| 插件应该用 provide/inject 吗? |
是的。插件通过 provide 提供功能,组件通过 inject 使用,比全局属性更灵活。 |
| 注入 key 命名约定? |
使用 Symbol 避免冲突:export const myPluginKey = Symbol()。 |
| 如何为插件添加类型支持? |
通过模块扩展:declare module 'vue' { interface ComponentCustomProperties { $myPlugin: MyPlugin } }。 |
第四部分:为什么选择 UnoCSS 而不是 Tailwind CSS?
核心论点:UnoCSS 是 Tailwind 的超集
UnoCSS 不是 Tailwind 的竞争者,而是增强版。通过预设系统,UnoCSS 可以 100% 兼容 Tailwind 语法:
// uno.config.ts
import { defineConfig, presetWind } from 'unocss'
export default defineConfig({
presets: [
presetWind(), // Tailwind CSS v3 兼容
// 或 presetWind({ version: 4 }) // Tailwind CSS v4 兼容
],
})
使用 presetWind 后,所有 Tailwind 类名都能正常工作:
<!-- Tailwind 语法完全兼容 -->
<div class="flex items-center justify-between p-4 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 transition">
<span class="text-xl font-bold">完全兼容</span>
</div>
UnoCSS 的独家能力
1. 纯 CSS 图标(零 JS 运行时)
UnoCSS 通过 presetIcons 支持 10 万+ Iconify 图标,编译为纯 CSS,零 JavaScript 运行时开销:
pnpm add -D @iconify-json/carbon @iconify-json/mdi
<!-- 直接用 class 引用图标,无需导入 -->
<div class="i-carbon-logo-github text-2xl" />
<div class="i-mdi-home text-red-500" />
<button class="i-carbon-arrow-right hover:i-carbon-arrow-right-filled" />
编译结果(纯 CSS):
.i-carbon-logo-github {
display: inline-block;
width: 1em;
height: 1em;
background: url("data:image/svg+xml;utf8,...") no-repeat;
background-size: 100% 100%;
}
对比 Tailwind + React Icons:
- Tailwind:需要导入 React/Vue 组件,增加 bundle 体积
- UnoCSS:纯 CSS,零 JS,图标按需编译
2. 属性化模式(Attributify)
避免 class 字符串爆炸:
<!-- Tailwind:class 字符串过长 -->
<button class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded-lg shadow-md transition duration-300 flex items-center gap-2">
提交
</button>
<!-- UnoCSS:属性化模式 -->
<button
bg="blue-500 hover:blue-600"
text="white"
font="bold"
p="y-2 x-4"
rounded="lg"
shadow="md"
transition
duration="300"
flex
items="center"
gap="2"
>
提交
</button>
3. Variant Group(变体组简写)
<!-- Tailwind:重复写 hover -->
<div class="hover:bg-red-500 hover:text-white hover:scale-105">
<!-- UnoCSS:Variant Group -->
<div class="hover:(bg-red-500 text-white scale-105)">
4. 自定义规则引擎
Tailwind 需要配置复杂的插件系统,UnoCSS 支持正则和函数定义原子类:
// uno.config.ts
import { defineConfig } from 'unocss'
export default defineConfig({
rules: [
// 正则匹配:自定义间距
[/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` })],
// 函数定义:自定义颜色
['text-brand', { color: '#3b82f6' }],
// 动态值:任意单位
[/^gap-(\d+)(px|rem|em)$/, ([, num, unit]) => ({ gap: `${num}${unit}` })],
],
shortcuts: {
// 快捷组合类
'btn-primary': 'bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600',
},
})
5. 编译模式(Compile Class)
将多个原子类编译为一个哈希类,减少 HTML 体积:
<!-- 开发模式:原子类 -->
<div class="flex items-center gap-4 bg-blue-500 p-4">
<!-- 生产模式:编译为单个类 -->
<div class="uno-abc123">
<style>
.uno-abc123 {
display: flex;
align-items: center;
gap: 1rem;
background: #3b82f6;
padding: 1rem;
}
</style>
对比总结
| 特性 |
Tailwind CSS |
UnoCSS |
| 基础原子类 |
✅ 完整支持 |
✅ 完全兼容(presetWind) |
| 图标方案 |
需要额外库(React Icons 等) |
✅ 内置 10 万+ 图标(纯 CSS) |
| 属性化模式 |
❌ 不支持 |
✅ presetAttributify |
| Variant Group |
❌ 不支持 |
✅ hover:(bg-red text-white)
|
| 自定义规则 |
复杂插件系统 |
✅ 正则/函数直接定义 |
| 编译模式 |
❌ 不支持 |
✅ 编译为哈希类 |
| 性能 |
JIT 编译快 |
✅ 更快(Vite 原生) |
| 生态整合 |
Standalone |
✅ Vite/Nuxt 深度集成 |
与 Anthony Fu 技术栈的协同
-
Vite 原生设计:UnoCSS 为 Vite 设计,HMR 极快
-
Nuxt 一等公民:
@nuxt/unocss 开箱即用
-
作者生态:Anthony Fu 同时是 UnoCSS 和 Iconify 的作者,工具链深度整合
完整配置示例
// uno.config.ts
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
// 预设
presets: [
presetUno(), // 默认预设(类似 Tailwind)
presetAttributify(), // 属性化模式
presetIcons({
scale: 1.2,
cdn: 'https://esm.sh/',
}),
presetTypography(), // 排版预设
presetWebFonts({
fonts: {
sans: 'Inter',
mono: 'Fira Code',
},
}),
],
// 转换器
transformers: [
transformerDirectives(), // @apply 指令
transformerVariantGroup(), // Variant Group
],
// 自定义规则
rules: [
['text-brand', { color: '#3b82f6' }],
],
// 快捷方式
shortcuts: {
'btn': 'px-4 py-2 rounded inline-block cursor-pointer',
'btn-primary': 'btn bg-blue-500 text-white hover:bg-blue-600',
},
// 主题扩展
theme: {
colors: {
brand: {
primary: '#3b82f6',
secondary: '#8b5cf6',
},
},
},
})
安装命令:
pnpm add -D unocss
pnpm add -D @iconify-json/carbon @iconify-json/mdi
第五部分:配套工具链一览
| 工具 |
用途 |
推荐理由 |
| Vue 3.5+ |
渐进式 JavaScript 框架 |
Composition API、性能优化、TypeScript 支持 |
| Nuxt 3 |
Vue 元框架 |
SSR/SSG、文件路由、服务端 API、SEO 优化 |
| Pinia |
状态管理 |
直观的 API、完整的 TypeScript 支持、Vue DevTools 集成 |
| Vite |
构建工具 |
极速 HMR、原生 ESM、Rollup 生产构建 |
| VitePress |
静态站点生成器 |
Vue 驱动、Markdown 扩展、主题定制 |
| Vitest |
单元测试 |
Vite 原生、与 Jest 兼容的 API、快速执行 |
| UnoCSS |
原子化 CSS 引擎 |
Tailwind 超集、纯 CSS 图标、Vite 深度集成 |
| pnpm |
包管理器 |
磁盘高效、严格依赖管理、monorepo 支持 |
| VueUse |
组合式函数集合 |
200+ 实用工具、SSR 友好、Tree-shakable |
| Slidev |
开发者幻灯片 |
Markdown 编写、Vue 组件、录制功能 |
| tsdown |
TypeScript 打包工具 |
零配置、类型声明生成、ESM/CJS 双输出 |
| Vue Router |
官方路由 |
嵌套路由、导航守卫、动态路由匹配 |
快速开始命令
# 创建 Vue 3 项目(Vite)
pnpm create vite my-vue-app --template vue-ts
# 创建 Nuxt 3 项目
pnpm dlx nuxi@latest init my-nuxt-app
# 添加 UnoCSS
pnpm add -D unocss
# 添加 VueUse
pnpm add @vueuse/core
# 添加 Pinia
pnpm add pinia
# 添加 Vitest
pnpm add -D vitest
总结
Anthony Fu 的开发规范强调:
-
类型安全优先:TypeScript + 显式类型定义
-
性能意识:
shallowRef > ref、避免深度侦听、虚拟滚动
-
工具链协同:Vite + UnoCSS + Vitest 深度整合
-
代码质量:单一职责、ESLint 自动化、Git Hooks
-
现代化实践:Composition API、
<script setup>、组合式函数
通过遵循这些规范,可以构建更快、更可维护、更具扩展性的 Vue 3 应用。
参考资料:
译者注: 本文基于 antfu/skills 仓库于 2026 年 2 月的内容整理翻译,随着生态演进,部分实践可能更新,请以官方文档为准。