<date>

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

| 命令 | 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 如何选择? | 优先使用 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 CSS | UnoCSS |

|------|--------------|--------|

| 基础原子类 | ✅ 完整支持 | ✅ 完全兼容(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 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 的开发规范强调:

  1. 类型安全优先:TypeScript + 显式类型定义

  2. 性能意识shallowRef > ref、避免深度侦听、虚拟滚动

  3. 工具链协同:Vite + UnoCSS + Vitest 深度整合

  4. 代码质量:单一职责、ESLint 自动化、Git Hooks

  5. 现代化实践:Composition API、<script setup>、组合式函数

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


参考资料:

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