<date>

February 11, 2026

</date>

initializing...

0%

Anthony Fu 的 Vue3 开发规范完整解读

Anthony Fu 的 Vue3 开发规范完整解读

Published on: Last modified:

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 - 通用包管理器命令

命令npmyarnpnpmbun
ninpm installyarn installpnpm installbun install
nr devnpm run devyarn run devpnpm run devbun run dev
nunpm updateyarn upgradepnpm updatebun update
nun lodashnpm uninstall lodashyarn remove lodashpnpm remove lodashbun remove lodash
ncinpm ciyarn install --frozen-lockfilepnpm install --frozen-lockfilebun install --frozen-lockfile
nlx vitestnpx vitestyarn dlx vitestpnpm dlx vitestbunx 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 如何选择?优先使用 refref 可以存储任何类型,而 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

计算属性

问题建议
计算属性可以有副作用吗?不应该。计算属性应该是纯函数,只做计算和返回值。副作用应该放在 watchwatchEffect 中。
为什么计算属性是只读的?默认只读,但可以提供 setter。推荐只读设计,修改应通过源数据。
计算属性什么时候重新计算?只有当依赖的响应式数据变化时才重新计算(懒执行 + 缓存)。这是相比 method 的主要优势。
计算属性的条件依赖如何工作?只追踪当前执行分支的依赖。if (flag) return a 时只追踪 flaga,不追踪 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) => {})。注意对象类型的 oldValnewVal 可能指向同一个引用。
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> 的同步代码中调用,不能在 setTimeoutasync 函数中。
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。例如 useMouseuseFetch
返回值应该是什么?返回包含响应式状态和方法的对象。使用 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-elseTransitionGroup
为什么列表项需要 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?不能用 windowdocument 等浏览器 API。生命周期只有 setuponServerPrefetch
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 CSSUnoCSS
基础原子类✅ 完整支持✅ 完全兼容(presetWind)
图标方案需要额外库(React Icons 等)✅ 内置 10 万+ 图标(纯 CSS)
属性化模式❌ 不支持✅ presetAttributify
Variant Group❌ 不支持hover:(bg-red text-white)
自定义规则复杂插件系统✅ 正则/函数直接定义
编译模式❌ 不支持✅ 编译为哈希类
性能JIT 编译快✅ 更快(Vite 原生)
生态整合Standalone✅ Vite/Nuxt 深度集成

与 Anthony Fu 技术栈的协同

  1. Vite 原生设计:UnoCSS 为 Vite 设计,HMR 极快
  2. Nuxt 一等公民@nuxt/unocss 开箱即用
  3. 作者生态: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 3Vue 元框架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 组件、录制功能
tsdownTypeScript 打包工具零配置、类型声明生成、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 的开发规范强调:

  1. 类型安全优先:TypeScript + 显式类型定义
  2. 性能意识shallowRef > ref、避免深度侦听、虚拟滚动
  3. 工具链协同:Vite + UnoCSS + Vitest 深度整合
  4. 代码质量:单一职责、ESLint 自动化、Git Hooks
  5. 现代化实践:Composition API、<script setup>、组合式函数

通过遵循这些规范,可以构建更快、更可维护、更具扩展性的 Vue 3 应用。


参考资料:

译者注: 本文基于 antfu/skills 仓库于 2026 年 2 月的内容整理翻译,随着生态演进,部分实践可能更新,请以官方文档为准。