vue

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Vue 3 - Progressive JavaScript Framework

Vue 3 - 渐进式JavaScript框架

Overview

概述

Vue 3 is a progressive framework for building user interfaces with emphasis on approachability, performance, and flexibility. It features the Composition API for better logic reuse, a powerful reactivity system, and single-file components (.vue files).
Key Features:
  • Composition API: setup() with ref, reactive, computed, watch
  • Reactivity System: Fine-grained reactive data tracking
  • Single-File Components: Template, script, style in one file
  • Vue Router: Official routing for SPAs
  • Pinia: Modern state management (Vuex successor)
  • TypeScript: First-class TypeScript support
  • Vite: Lightning-fast development with HMR
Installation:
bash
undefined
Vue 3 是一个渐进式框架,用于构建用户界面,强调易用性、性能和灵活性。它具备用于更好逻辑复用的组合式API、强大的响应式系统以及单文件组件(.vue 文件)。
核心特性:
  • 组合式API:搭配ref、reactive、computed、watch的setup()函数
  • 响应式系统:细粒度的响应式数据追踪
  • 单文件组件:将模板、脚本、样式整合在一个文件中
  • Vue Router:官方SPA路由解决方案
  • Pinia:现代化状态管理工具(Vuex的继任者)
  • TypeScript:一等公民级别的TypeScript支持
  • Vite:支持热模块替换(HMR)的极速开发构建工具
安装方式:
bash
undefined

Create new Vue 3 project (recommended)

创建新的Vue 3项目(推荐)

npm create vue@latest my-app cd my-app npm install npm run dev
npm create vue@latest my-app cd my-app npm install npm run dev

Or with Vite template

或使用Vite模板

npm create vite@latest my-app -- --template vue-ts
undefined
npm create vite@latest my-app -- --template vue-ts
undefined

Composition API Fundamentals

组合式API基础

setup() Function

setup() 函数

vue
<script setup lang="ts">
// Modern <script setup> syntax (recommended)
import { ref, computed, onMounted } from 'vue';

// Reactive state
const count = ref(0);
const message = ref('Hello Vue 3');

// Computed values
const doubled = computed(() => count.value * 2);

// Methods
function increment() {
  count.value++;
}

// Lifecycle hooks
onMounted(() => {
  console.log('Component mounted');
});
</script>

<template>
  <div>
    <p>Count: {{ count }} (Doubled: {{ doubled }})</p>
    <button @click="increment">Increment</button>
  </div>
</template>
vue
<script setup lang="ts">
// 现代化的<script setup>语法(推荐)
import { ref, computed, onMounted } from 'vue';

// 响应式状态
const count = ref(0);
const message = ref('Hello Vue 3');

// 计算属性
const doubled = computed(() => count.value * 2);

// 方法
function increment() {
  count.value++;
}

// 生命周期钩子
onMounted(() => {
  console.log('组件已挂载');
});
</script>

<template>
  <div>
    <p>计数: {{ count }} (翻倍后: {{ doubled }})</p>
    <button @click="increment">增加</button>
  </div>
</template>

Reactive State with ref() and reactive()

使用ref()和reactive()创建响应式状态

vue
<script setup lang="ts">
import { ref, reactive } from 'vue';

// ref() - for primitives and objects (needs .value in script)
const count = ref(0);
const user = ref({ name: 'Alice', age: 30 });

console.log(count.value); // 0
console.log(user.value.name); // 'Alice'

// reactive() - for objects only (no .value needed)
const state = reactive({
  todos: [] as Todo[],
  filter: 'all',
  error: null as string | null
});

console.log(state.todos); // []
state.todos.push({ id: 1, text: 'Learn Vue', done: false });
</script>

<template>
  <!-- In template, .value is automatic for refs -->
  <p>Count: {{ count }}</p>
  <p>User: {{ user.name }}</p>
  <p>Todos: {{ state.todos.length }}</p>
</template>
vue
<script setup lang="ts">
import { ref, reactive } from 'vue';

// ref() - 用于原始值和对象(在脚本中需要使用.value)
const count = ref(0);
const user = ref({ name: 'Alice', age: 30 });

console.log(count.value); // 0
console.log(user.value.name); // 'Alice'

// reactive() - 仅用于对象(无需使用.value)
const state = reactive({
  todos: [] as Todo[],
  filter: 'all',
  error: null as string | null
});

console.log(state.todos); // []
state.todos.push({ id: 1, text: '学习Vue', done: false });
</script>

<template>
  <!-- 在模板中,ref的.value会自动解析 -->
  <p>计数: {{ count }}</p>
  <p>用户: {{ user.name }}</p>
  <p>待办事项数量: {{ state.todos.length }}</p>
</template>

Computed Properties

计算属性

vue
<script setup lang="ts">
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// Read-only computed
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// Writable computed
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(value: string) {
    const parts = value.split(' ');
    firstName.value = parts[0];
    lastName.value = parts[1];
  }
});

// Complex computations
interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const todos = ref<Todo[]>([
  { id: 1, text: 'Learn Vue', done: true },
  { id: 2, text: 'Build app', done: false }
]);

const completedTodos = computed(() =>
  todos.value.filter(t => t.done)
);

const activeTodos = computed(() =>
  todos.value.filter(t => !t.done)
);

const progress = computed(() =>
  todos.value.length > 0
    ? (completedTodos.value.length / todos.value.length) * 100
    : 0
);
</script>

<template>
  <div>
    <p>Full Name: {{ fullName }}</p>
    <p>Progress: {{ progress.toFixed(1) }}%</p>
    <p>Active: {{ activeTodos.length }} | Done: {{ completedTodos.length }}</p>
  </div>
</template>
vue
<script setup lang="ts">
import { ref, computed } from 'vue';

const firstName = ref('John');
const lastName = ref('Doe');

// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

// 可写计算属性
const fullNameWritable = computed({
  get() {
    return `${firstName.value} ${lastName.value}`;
  },
  set(value: string) {
    const parts = value.split(' ');
    firstName.value = parts[0];
    lastName.value = parts[1];
  }
});

// 复杂计算
interface Todo {
  id: number;
  text: string;
  done: boolean;
}

const todos = ref<Todo[]>([
  { id: 1, text: '学习Vue', done: true },
  { id: 2, text: '构建应用', done: false }
]);

const completedTodos = computed(() =>
  todos.value.filter(t => t.done)
);

const activeTodos = computed(() =>
  todos.value.filter(t => !t.done)
);

const progress = computed(() =>
  todos.value.length > 0
    ? (completedTodos.value.length / todos.value.length) * 100
    : 0
);
</script>

<template>
  <div>
    <p>全名: {{ fullName }}</p>
    <p>完成进度: {{ progress.toFixed(1) }}%</p>
    <p>未完成: {{ activeTodos.length }} | 已完成: {{ completedTodos.length }}</p>
  </div>
</template>

Watchers and Side Effects

监听器与副作用

vue
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue';

const count = ref(0);
const user = ref({ name: 'Alice', age: 30 });

// watch() - explicit dependencies
watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`);
});

// Watch multiple sources
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
  console.log('Count or user changed');
});

// Watch object property (needs getter)
watch(
  () => user.value.name,
  (newName, oldName) => {
    console.log(`Name changed from ${oldName} to ${newName}`);
  }
);

// Deep watch for nested objects
watch(
  user,
  (newUser) => {
    console.log('User object changed deeply');
  },
  { deep: true }
);

// watchEffect() - automatic dependency tracking
watchEffect(() => {
  // Automatically watches count and user
  console.log(`Count: ${count.value}, User: ${user.value.name}`);
});

// Cleanup function
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('Delayed effect');
  }, 1000);

  onCleanup(() => {
    clearTimeout(timer);
  });
});
</script>
vue
<script setup lang="ts">
import { ref, watch, watchEffect } from 'vue';

const count = ref(0);
const user = ref({ name: 'Alice', age: 30 });

// watch() - 显式指定依赖
watch(count, (newVal, oldVal) => {
  console.log(`计数从${oldVal}变为${newVal}`);
});

// 监听多个源
watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => {
  console.log('计数或用户信息已更改');
});

// 监听对象属性(需要使用getter函数)
watch(
  () => user.value.name,
  (newName, oldName) => {
    console.log(`姓名从${oldName}变为${newName}`);
  }
);

// 深度监听嵌套对象
watch(
  user,
  (newUser) => {
    console.log('用户对象发生深度变更');
  },
  { deep: true }
);

// watchEffect() - 自动追踪依赖
watchEffect(() => {
  // 自动监听count和user
  console.log(`计数: ${count.value}, 用户: ${user.value.name}`);
});

// 清理函数
watchEffect((onCleanup) => {
  const timer = setTimeout(() => {
    console.log('延迟副作用执行');
  }, 1000);

  onCleanup(() => {
    clearTimeout(timer);
  });
});
</script>

Component Props and Events

组件Props与事件

Defining Props (TypeScript)

定义Props(TypeScript)

vue
<script setup lang="ts">
// Type-safe props with defineProps
interface Props {
  title: string;
  count?: number;
  tags?: string[];
  user: {
    name: string;
    email: string;
  };
  disabled?: boolean;
}

// With defaults
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  tags: () => [],
  disabled: false
});

// Access props
console.log(props.title);
console.log(props.count);
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>Count: {{ count }}</p>
    <p>Tags: {{ tags.join(', ') }}</p>
  </div>
</template>
vue
<script setup lang="ts">
// 类型安全的Props
export interface Props {
  title: string;
  count?: number;
  tags?: string[];
  user: {
    name: string;
    email: string;
  };
  disabled?: boolean;
}

// 带默认值的Props
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  tags: () => [],
  disabled: false
});

// 访问Props
console.log(props.title);
console.log(props.count);
</script>

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>计数: {{ count }}</p>
    <p>标签: {{ tags.join(', ') }}</p>
  </div>
</template>

Emitting Events

触发事件

vue
<script setup lang="ts">
// Define emitted events with types
const emit = defineEmits<{
  update: [value: number];
  submit: [data: { name: string; email: string }];
  delete: [id: number];
}>();

function handleClick() {
  emit('update', 42);
}

function handleSubmit() {
  emit('submit', { name: 'Alice', email: 'alice@example.com' });
}
</script>

<template>
  <button @click="handleClick">Update</button>
  <button @click="handleSubmit">Submit</button>
</template>
vue
<script setup lang="ts">
// 定义带类型的触发事件
const emit = defineEmits<{
  update: [value: number];
  submit: [data: { name: string; email: string }];
  delete: [id: number];
}>();

function handleClick() {
  emit('update', 42);
}

function handleSubmit() {
  emit('submit', { name: 'Alice', email: 'alice@example.com' });
}
</script>

<template>
  <button @click="handleClick">更新</button>
  <button @click="handleSubmit">提交</button>
</template>

v-model for Two-Way Binding

使用v-model实现双向绑定

vue
<!-- Child: CustomInput.vue -->
<script setup lang="ts">
// v-model creates 'modelValue' prop and 'update:modelValue' event
const props = defineProps<{
  modelValue: string;
  placeholder?: string;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: string];
}>();

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement;
  emit('update:modelValue', target.value);
}
</script>

<template>
  <input
    :value="modelValue"
    @input="handleInput"
    :placeholder="placeholder"
  />
</template>

<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const searchQuery = ref('');
</script>

<template>
  <CustomInput v-model="searchQuery" placeholder="Search..." />
  <p>Searching for: {{ searchQuery }}</p>
</template>
vue
<!-- 子组件: CustomInput.vue -->
<script setup lang="ts">
// v-model会自动创建'modelValue' Prop和'update:modelValue'事件
const props = defineProps<{
  modelValue: string;
  placeholder?: string;
}>();

const emit = defineEmits<{
  'update:modelValue': [value: string];
}>();

function handleInput(event: Event) {
  const target = event.target as HTMLInputElement;
  emit('update:modelValue', target.value);
}
</script>

<template>
  <input
    :value="modelValue"
    @input="handleInput"
    :placeholder="placeholder"
  />
</template>

<!-- 父组件: Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const searchQuery = ref('');
</script>

<template>
  <CustomInput v-model="searchQuery" placeholder="搜索..." />
  <p>正在搜索: {{ searchQuery }}</p>
</template>

Multiple v-model Bindings

多v-model绑定

vue
<!-- Child: UserForm.vue -->
<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
}>();

const emit = defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
}>();
</script>

<template>
  <div>
    <input
      :value="firstName"
      @input="emit('update:firstName', ($event.target as HTMLInputElement).value)"
    />
    <input
      :value="lastName"
      @input="emit('update:lastName', ($event.target as HTMLInputElement).value)"
    />
  </div>
</template>

<!-- Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import UserForm from './UserForm.vue';

const first = ref('John');
const last = ref('Doe');
</script>

<template>
  <UserForm v-model:first-name="first" v-model:last-name="last" />
  <p>Full name: {{ first }} {{ last }}</p>
</template>
vue
<!-- 子组件: UserForm.vue -->
<script setup lang="ts">
defineProps<{
  firstName: string;
  lastName: string;
}>();

const emit = defineEmits<{
  'update:firstName': [value: string];
  'update:lastName': [value: string];
}>();
</script>

<template>
  <div>
    <input
      :value="firstName"
      @input="emit('update:firstName', ($event.target as HTMLInputElement).value)"
    />
    <input
      :value="lastName"
      @input="emit('update:lastName', ($event.target as HTMLInputElement).value)"
    />
  </div>
</template>

<!-- 父组件: Parent.vue -->
<script setup lang="ts">
import { ref } from 'vue';
import UserForm from './UserForm.vue';

const first = ref('John');
const last = ref('Doe');
</script>

<template>
  <UserForm v-model:first-name="first" v-model:last-name="last" />
  <p>全名: {{ first }} {{ last }}</p>
</template>

Template Syntax

模板语法

Directives

指令

vue
<script setup lang="ts">
import { ref, reactive } from 'vue';

const message = ref('Hello Vue');
const isActive = ref(true);
const hasError = ref(false);
const items = ref(['Apple', 'Banana', 'Cherry']);
const user = ref({ name: 'Alice', email: 'alice@example.com' });

const formData = reactive({
  username: '',
  agree: false,
  gender: 'male',
  interests: [] as string[]
});
</script>

<template>
  <!-- Text interpolation -->
  <p>{{ message }}</p>

  <!-- Raw HTML (careful with XSS!) -->
  <div v-html="'<strong>Bold</strong>'"></div>

  <!-- Attribute binding -->
  <div :id="'container-' + user.name"></div>
  <img :src="user.avatar" :alt="user.name" />

  <!-- Class binding -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[isActive ? 'active' : '', hasError && 'error']"></div>

  <!-- Style binding -->
  <div :style="{ color: 'red', fontSize: '16px' }"></div>
  <div :style="{ color: isActive ? 'green' : 'gray' }"></div>

  <!-- Conditional rendering -->
  <p v-if="isActive">Active</p>
  <p v-else-if="hasError">Error</p>
  <p v-else>Inactive</p>

  <!-- v-show (toggles display CSS) -->
  <p v-show="isActive">Visible when active</p>

  <!-- List rendering -->
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ index + 1 }}. {{ item }}
    </li>
  </ul>

  <!-- Object iteration -->
  <div v-for="(value, key) in user" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- Event handling -->
  <button @click="isActive = !isActive">Toggle</button>
  <button @click.prevent="handleSubmit">Submit</button>
  <input @keyup.enter="handleSearch" />

  <!-- Form binding -->
  <input v-model="formData.username" />
  <input type="checkbox" v-model="formData.agree" />
  <input type="radio" v-model="formData.gender" value="male" />
  <input type="radio" v-model="formData.gender" value="female" />
  <select v-model="formData.interests" multiple>
    <option>Reading</option>
    <option>Gaming</option>
    <option>Coding</option>
  </select>
</template>
vue
<script setup lang="ts">
import { ref, reactive } from 'vue';

const message = ref('Hello Vue');
const isActive = ref(true);
const hasError = ref(false);
const items = ref(['苹果', '香蕉', '樱桃']);
const user = ref({ name: 'Alice', email: 'alice@example.com' });

const formData = reactive({
  username: '',
  agree: false,
  gender: 'male',
  interests: [] as string[]
});
</script>

<template>
  <!-- 文本插值 -->
  <p>{{ message }}</p>

  <!-- 原始HTML(注意XSS风险!) -->
  <div v-html="'<strong>加粗文本</strong>'"></div>

  <!-- 属性绑定 -->
  <div :id="'container-' + user.name"></div>
  <img :src="user.avatar" :alt="user.name" />

  <!-- 类名绑定 -->
  <div :class="{ active: isActive, 'text-danger': hasError }"></div>
  <div :class="[isActive ? 'active' : '', hasError && 'error']"></div>

  <!-- 样式绑定 -->
  <div :style="{ color: 'red', fontSize: '16px' }"></div>
  <div :style="{ color: isActive ? 'green' : 'gray' }"></div>

  <!-- 条件渲染 -->
  <p v-if="isActive">激活状态</p>
  <p v-else-if="hasError">错误状态</p>
  <p v-else>未激活状态</p>

  <!-- v-show(切换display CSS属性) -->
  <p v-show="isActive">激活时可见</p>

  <!-- 列表渲染 -->
  <ul>
    <li v-for="(item, index) in items" :key="index">
      {{ index + 1 }}. {{ item }}
    </li>
  </ul>

  <!-- 对象遍历 -->
  <div v-for="(value, key) in user" :key="key">
    {{ key }}: {{ value }}
  </div>

  <!-- 事件处理 -->
  <button @click="isActive = !isActive">切换状态</button>
  <button @click.prevent="handleSubmit">提交</button>
  <input @keyup.enter="handleSearch" />

  <!-- 表单绑定 -->
  <input v-model="formData.username" />
  <input type="checkbox" v-model="formData.agree" />
  <input type="radio" v-model="formData.gender" value="male" />
  <input type="radio" v-model="formData.gender" value="female" />
  <select v-model="formData.interests" multiple>
    <option>阅读</option>
    <option>游戏</option>
    <option>编程</option>
  </select>
</template>

Event Modifiers

事件修饰符

vue
<template>
  <!-- Prevent default -->
  <form @submit.prevent="handleSubmit">
    <button type="submit">Submit</button>
  </form>

  <!-- Stop propagation -->
  <div @click="handleOuter">
    <button @click.stop="handleInner">Click me</button>
  </div>

  <!-- Capture mode -->
  <div @click.capture="handleCapture">...</div>

  <!-- Self (only if event.target is the element itself) -->
  <div @click.self="handleSelf">...</div>

  <!-- Once (trigger at most once) -->
  <button @click.once="handleOnce">Click once</button>

  <!-- Key modifiers -->
  <input @keyup.enter="handleEnter" />
  <input @keyup.esc="handleEscape" />
  <input @keyup.ctrl.s="handleSave" />
  <input @keyup.shift.t="handleShiftT" />

  <!-- Mouse button modifiers -->
  <div @click.left="handleLeftClick"></div>
  <div @click.right="handleRightClick"></div>
  <div @click.middle="handleMiddleClick"></div>
</template>
vue
<template>
  <!-- 阻止默认行为 -->
  <form @submit.prevent="handleSubmit">
    <button type="submit">提交</button>
  </form>

  <!-- 阻止事件冒泡 -->
  <div @click="handleOuter">
    <button @click.stop="handleInner">点击我</button>
  </div>

  <!-- 捕获模式 -->
  <div @click.capture="handleCapture">...</div>

  <!-- 仅当事件目标是元素本身时触发 -->
  <div @click.self="handleSelf">...</div>

  <!-- 仅触发一次 -->
  <button @click.once="handleOnce">点击一次</button>

  <!-- 按键修饰符 -->
  <input @keyup.enter="handleEnter" />
  <input @keyup.esc="handleEscape" />
  <input @keyup.ctrl.s="handleSave" />
  <input @keyup.shift.t="handleShiftT" />

  <!-- 鼠标按钮修饰符 -->
  <div @click.left="handleLeftClick"></div>
  <div @click.right="handleRightClick"></div>
  <div @click.middle="handleMiddleClick"></div>
</template>

Lifecycle Hooks

生命周期钩子

vue
<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue';

// Before component is mounted
onBeforeMount(() => {
  console.log('Component about to mount');
});

// After component is mounted (DOM is ready)
onMounted(() => {
  console.log('Component mounted');
  // Good place for API calls, DOM manipulation
  fetchData();
});

// Before component updates due to reactive changes
onBeforeUpdate(() => {
  console.log('Component about to update');
});

// After component updates
onUpdated(() => {
  console.log('Component updated');
  // Careful: can cause infinite loops if you update state here
});

// Before component unmounts
onBeforeUnmount(() => {
  console.log('Component about to unmount');
  // Clean up subscriptions, timers, etc.
});

// After component unmounts
onUnmounted(() => {
  console.log('Component unmounted');
});

// Error handling
onErrorCaptured((err, instance, info) => {
  console.error('Error captured:', err, info);
  return false; // Prevent propagation
});

async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data);
}
</script>
vue
<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured
} from 'vue';

// 组件挂载前
onBeforeMount(() => {
  console.log('组件即将挂载');
});

// 组件挂载后(DOM已就绪)
onMounted(() => {
  console.log('组件已挂载');
  // 适合发起API请求、操作DOM的时机
  fetchData();
});

// 组件更新前(响应式状态变更导致)
onBeforeUpdate(() => {
  console.log('组件即将更新');
});

// 组件更新后
onUpdated(() => {
  console.log('组件已更新');
  // 注意:在此处更新状态可能导致无限循环
});

// 组件卸载前
onBeforeUnmount(() => {
  console.log('组件即将卸载');
  // 清理订阅、定时器等
});

// 组件卸载后
onUnmounted(() => {
  console.log('组件已卸载');
});

// 错误捕获
onErrorCaptured((err, instance, info) => {
  console.error('捕获到错误:', err, info);
  return false; // 阻止错误继续传播
});

async function fetchData() {
  const response = await fetch('/api/data');
  const data = await response.json();
  console.log(data);
}
</script>

Provide/Inject (Dependency Injection)

Provide/Inject(依赖注入)

vue
<!-- Parent.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue';
import type { InjectionKey } from 'vue';

interface Theme {
  primary: string;
  secondary: string;
}

// Create typed injection key
export const ThemeKey: InjectionKey<Theme> = Symbol('theme');

const theme = ref<Theme>({
  primary: '#007bff',
  secondary: '#6c757d'
});

// Provide to all descendants
provide(ThemeKey, theme.value);
provide('userPermissions', ['read', 'write']);
</script>

<!-- Child.vue (any depth) -->
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from './Parent.vue';

// Inject with type safety
const theme = inject(ThemeKey);
const permissions = inject<string[]>('userPermissions', []);

// With default value
const config = inject('config', { debug: false });
</script>

<template>
  <div :style="{ color: theme?.primary }">
    Themed content
  </div>
</template>
vue
<!-- 父组件: Parent.vue -->
<script setup lang="ts">
import { ref, provide } from 'vue';
import type { InjectionKey } from 'vue';

interface Theme {
  primary: string;
  secondary: string;
}

// 创建带类型的注入Key
export const ThemeKey: InjectionKey<Theme> = Symbol('theme');

const theme = ref<Theme>({
  primary: '#007bff',
  secondary: '#6c757d'
});

// 提供给所有后代组件
provide(ThemeKey, theme.value);
provide('userPermissions', ['read', 'write']);
</script>

<!-- 子组件: Child.vue(任意层级) -->
<script setup lang="ts">
import { inject } from 'vue';
import { ThemeKey } from './Parent.vue';

// 类型安全的注入
const theme = inject(ThemeKey);
const permissions = inject<string[]>('userPermissions', []);

// 带默认值的注入
const config = inject('config', { debug: false });
</script>

<template>
  <div :style="{ color: theme?.primary }">
    主题化内容
  </div>
</template>

Vue Router Integration

Vue Router集成

Basic Setup

基础配置

typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'), // Lazy loading
    props: true // Pass route params as props
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;
typescript
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';
import type { RouteRecordRaw } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/user/:id',
    name: 'User',
    component: () => import('@/views/User.vue'), // 懒加载
    props: true // 将路由参数作为Props传递
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue')
  }
];

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
});

export default router;

Navigation and Route Access

导航与路由访问

vue
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router';
import { computed } from 'vue';

const router = useRouter();
const route = useRoute();

// Access route params
const userId = computed(() => route.params.id);
const querySearch = computed(() => route.query.search);

// Programmatic navigation
function goToUser(id: number) {
  router.push({ name: 'User', params: { id } });
}

function goToAbout() {
  router.push('/about');
}

function goBack() {
  router.back();
}

function replaceRoute() {
  router.replace({ name: 'Home' }); // No history entry
}
</script>

<template>
  <nav>
    <!-- Declarative navigation -->
    <RouterLink to="/">Home</RouterLink>
    <RouterLink :to="{ name: 'About' }">About</RouterLink>
    <RouterLink :to="{ name: 'User', params: { id: 123 } }">
      User 123
    </RouterLink>

    <!-- Active link styling -->
    <RouterLink
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      Dashboard
    </RouterLink>
  </nav>

  <button @click="goToUser(456)">Go to User 456</button>
  <button @click="goBack">Back</button>

  <p>Current user ID: {{ userId }}</p>
  <p>Search query: {{ querySearch }}</p>

  <!-- Render matched component -->
  <RouterView />
</template>
vue
<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router';
import { computed } from 'vue';

const router = useRouter();
const route = useRoute();

// 访问路由参数
const userId = computed(() => route.params.id);
const querySearch = computed(() => route.query.search);

// 编程式导航
function goToUser(id: number) {
  router.push({ name: 'User', params: { id } });
}

function goToAbout() {
  router.push('/about');
}

function goBack() {
  router.back();
}

function replaceRoute() {
  router.replace({ name: 'Home' }); // 不会在历史记录中添加条目
}
</script>

<template>
  <nav>
    <!-- 声明式导航 -->
    <RouterLink to="/">首页</RouterLink>
    <RouterLink :to="{ name: 'About' }">关于</RouterLink>
    <RouterLink :to="{ name: 'User', params: { id: 123 } }">
      用户123
    </RouterLink>

    <!-- 激活链接样式 -->
    <RouterLink
      to="/dashboard"
      active-class="active"
      exact-active-class="exact-active"
    >
      控制台
    </RouterLink>
  </nav>

  <button @click="goToUser(456)">跳转到用户456</button>
  <button @click="goBack">返回</button>

  <p>当前用户ID: {{ userId }}</p>
  <p>搜索关键词: {{ querySearch }}</p>

  <!-- 渲染匹配的组件 -->
  <RouterView />
</template>

Navigation Guards

导航守卫

typescript
// router/index.ts
import { createRouter } from 'vue-router';

const router = createRouter({
  // ... routes
});

// Global before guard
router.beforeEach((to, from, next) => {
  const isAuthenticated = checkAuth();

  if (to.meta.requiresAuth && !isAuthenticated) {
    next({ name: 'Login', query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

// Global after hook
router.afterEach((to, from) => {
  document.title = `${to.meta.title || 'App'} - My App`;
});

// Per-route guard
const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (isAdmin()) {
        next();
      } else {
        next('/unauthorized');
      }
    }
  }
];
vue
<!-- Component guard -->
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';

// Confirm before leaving
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('You have unsaved changes. Leave anyway?');
    return answer;
  }
});

// React to route changes (same component, different params)
onBeforeRouteUpdate((to, from) => {
  console.log(`Route updated from ${from.params.id} to ${to.params.id}`);
  fetchData(to.params.id);
});
</script>
typescript
// router/index.ts
import { createRouter } from 'vue-router';

const router = createRouter({
  // ... 路由配置
});

// 全局前置守卫
router.beforeEach((to, from, next) => {
  const isAuthenticated = checkAuth();

  if (to.meta.requiresAuth && !isAuthenticated) {
    next({ name: 'Login', query: { redirect: to.fullPath } });
  } else {
    next();
  }
});

// 全局后置钩子
router.afterEach((to, from) => {
  document.title = `${to.meta.title || '应用'} - 我的应用`;
});

// 路由独享守卫
const routes = [
  {
    path: '/admin',
    component: Admin,
    beforeEnter: (to, from, next) => {
      if (isAdmin()) {
        next();
      } else {
        next('/unauthorized');
      }
    }
  }
];
vue
<!-- 组件内守卫 -->
<script setup lang="ts">
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router';

// 离开前确认
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('你有未保存的更改,确定要离开吗?');
    return answer;
  }
});

// 响应路由变更(同一组件,不同参数)
onBeforeRouteUpdate((to, from) => {
  console.log(`路由参数从${from.params.id}变为${to.params.id}`);
  fetchData(to.params.id);
});
</script>

Pinia State Management

Pinia状态管理

Store Definition

定义Store

typescript
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

// Composition API style (recommended)
export const useCounterStore = defineStore('counter', () => {
  // State
  const count = ref(0);
  const name = ref('Counter Store');

  // Getters (computed)
  const doubleCount = computed(() => count.value * 2);
  const isPositive = computed(() => count.value > 0);

  // Actions
  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  async function fetchCount() {
    const response = await fetch('/api/count');
    const data = await response.json();
    count.value = data.count;
  }

  return {
    count,
    name,
    doubleCount,
    isPositive,
    increment,
    decrement,
    fetchCount
  };
});

// Options API style (alternative)
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
    token: ''
  }),

  getters: {
    isLoggedIn: (state) => state.user !== null,
    fullName: (state) => state.user ? `${state.user.firstName} ${state.user.lastName}` : ''
  },

  actions: {
    async login(email: string, password: string) {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      this.user = data.user;
      this.token = data.token;
    },

    logout() {
      this.user = null;
      this.token = '';
    }
  }
});
typescript
// stores/counter.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';

// 组合式API风格(推荐)
export const useCounterStore = defineStore('counter', () => {
  // 状态
  const count = ref(0);
  const name = ref('计数器Store');

  // Getter(计算属性)
  const doubleCount = computed(() => count.value * 2);
  const isPositive = computed(() => count.value > 0);

  // Action
  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  async function fetchCount() {
    const response = await fetch('/api/count');
    const data = await response.json();
    count.value = data.count;
  }

  return {
    count,
    name,
    doubleCount,
    isPositive,
    increment,
    decrement,
    fetchCount
  };
});

// 选项式API风格(备选)
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null as User | null,
    token: ''
  }),

  getters: {
    isLoggedIn: (state) => state.user !== null,
    fullName: (state) => state.user ? `${state.user.firstName} ${state.user.lastName}` : ''
  },

  actions: {
    async login(email: string, password: string) {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ email, password })
      });
      const data = await response.json();
      this.user = data.user;
      this.token = data.token;
    },

    logout() {
      this.user = null;
      this.token = '';
    }
  }
});

Using Stores in Components

在组件中使用Store

vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
const userStore = useUserStore();

// Get reactive refs from store
const { count, doubleCount } = storeToRefs(counterStore);
const { user, isLoggedIn } = storeToRefs(userStore);

// Actions can be destructured directly (they're not reactive)
const { increment, decrement } = counterStore;

// Access state directly
console.log(counterStore.count);

// Modify state directly
counterStore.count++;

// Or use $patch for multiple changes
counterStore.$patch({
  count: 10,
  name: 'Updated Counter'
});

// Reset state
counterStore.$reset();
</script>

<template>
  <div>
    <p>Count: {{ count }} (Double: {{ doubleCount }})</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>

    <div v-if="isLoggedIn">
      <p>Welcome, {{ user?.firstName }}!</p>
      <button @click="userStore.logout()">Logout</button>
    </div>
  </div>
</template>
vue
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
import { useUserStore } from '@/stores/user';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
const userStore = useUserStore();

// 从Store中获取响应式引用
const { count, doubleCount } = storeToRefs(counterStore);
const { user, isLoggedIn } = storeToRefs(userStore);

// Action可以直接解构(它们不是响应式的)
const { increment, decrement } = counterStore;

// 直接访问状态
console.log(counterStore.count);

// 直接修改状态
counterStore.count++;

// 或使用$patch批量修改
counterStore.$patch({
  count: 10,
  name: '更新后的计数器'
});

// 重置状态
counterStore.$reset();
</script>

<template>
  <div>
    <p>计数: {{ count }} (翻倍后: {{ doubleCount }})</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>

    <div v-if="isLoggedIn">
      <p>欢迎, {{ user?.firstName }}!</p>
      <button @click="userStore.logout()">登出</button>
    </div>
  </div>
</template>

Store Composition (Accessing Other Stores)

Store组合(访问其他Store)

typescript
// stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useUserStore } from './user';

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([]);
  const userStore = useUserStore();

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  const canCheckout = computed(() =>
    userStore.isLoggedIn && items.value.length > 0
  );

  async function checkout() {
    if (!canCheckout.value) return;

    await fetch('/api/checkout', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${userStore.token}`
      },
      body: JSON.stringify({ items: items.value })
    });

    items.value = [];
  }

  return { items, total, canCheckout, checkout };
});
typescript
// stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { useUserStore } from './user';

export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([]);
  const userStore = useUserStore();

  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
  );

  const canCheckout = computed(() =>
    userStore.isLoggedIn && items.value.length > 0
  );

  async function checkout() {
    if (!canCheckout.value) return;

    await fetch('/api/checkout', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${userStore.token}`
      },
      body: JSON.stringify({ items: items.value })
    });

    items.value = [];
  }

  return { items, total, canCheckout, checkout };
});

Composables (Reusable Logic)

可组合函数(复用逻辑)

Custom Composables

自定义可组合函数

typescript
// composables/useFetch.ts
import { ref, type Ref } from 'vue';

interface UseFetchOptions {
  immediate?: boolean;
}

export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
  const data = ref<T | null>(null) as Ref<T | null>;
  const error = ref<Error | null>(null);
  const loading = ref(false);

  async function execute() {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(response.statusText);
      data.value = await response.json();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  }

  if (options.immediate) {
    execute();
  }

  return { data, error, loading, execute };
}

// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
  const storedValue = localStorage.getItem(key);
  const data = ref<T>(
    storedValue ? JSON.parse(storedValue) : defaultValue
  ) as Ref<T>;

  watch(
    data,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return data;
}

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function update(event: MouseEvent) {
    x.value = event.pageX;
    y.value = event.pageY;
  }

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  return { x, y };
}
typescript
// composables/useFetch.ts
import { ref, type Ref } from 'vue';

interface UseFetchOptions {
  immediate?: boolean;
}

export function useFetch<T>(url: string, options: UseFetchOptions = {}) {
  const data = ref<T | null>(null) as Ref<T | null>;
  const error = ref<Error | null>(null);
  const loading = ref(false);

  async function execute() {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(response.statusText);
      data.value = await response.json();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  }

  if (options.immediate) {
    execute();
  }

  return { data, error, loading, execute };
}

// composables/useLocalStorage.ts
import { ref, watch, type Ref } from 'vue';

export function useLocalStorage<T>(key: string, defaultValue: T): Ref<T> {
  const storedValue = localStorage.getItem(key);
  const data = ref<T>(
    storedValue ? JSON.parse(storedValue) : defaultValue
  ) as Ref<T>;

  watch(
    data,
    (newValue) => {
      localStorage.setItem(key, JSON.stringify(newValue));
    },
    { deep: true }
  );

  return data;
}

// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue';

export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  function update(event: MouseEvent) {
    x.value = event.pageX;
    y.value = event.pageY;
  }

  onMounted(() => {
    window.addEventListener('mousemove', update);
  });

  onUnmounted(() => {
    window.removeEventListener('mousemove', update);
  });

  return { x, y };
}

Using Composables

使用可组合函数

vue
<script setup lang="ts">
import { useFetch } from '@/composables/useFetch';
import { useLocalStorage } from '@/composables/useLocalStorage';
import { useMouse } from '@/composables/useMouse';

interface User {
  id: number;
  name: string;
  email: string;
}

const { data: user, loading, error, execute } = useFetch<User>(
  '/api/user/123',
  { immediate: true }
);

const settings = useLocalStorage('app-settings', {
  theme: 'dark',
  language: 'en'
});

const { x, y } = useMouse();
</script>

<template>
  <div>
    <div v-if="loading">Loading...</div>
    <div v-else-if="error">Error: {{ error.message }}</div>
    <div v-else-if="user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>

    <p>Theme: {{ settings.theme }}</p>
    <button @click="settings.theme = settings.theme === 'dark' ? 'light' : 'dark'">
      Toggle Theme
    </button>

    <p>Mouse: {{ x }}, {{ y }}</p>
  </div>
</template>
vue
<script setup lang="ts">
import { useFetch } from '@/composables/useFetch';
import { useLocalStorage } from '@/composables/useLocalStorage';
import { useMouse } from '@/composables/useMouse';

interface User {
  id: number;
  name: string;
  email: string;
}

const { data: user, loading, error, execute } = useFetch<User>(
  '/api/user/123',
  { immediate: true }
);

const settings = useLocalStorage('app-settings', {
  theme: 'dark',
  language: 'en'
});

const { x, y } = useMouse();
</script>

<template>
  <div>
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">错误: {{ error.message }}</div>
    <div v-else-if="user">
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>

    <p>主题: {{ settings.theme }}</p>
    <button @click="settings.theme = settings.theme === 'dark' ? 'light' : 'dark'">
      切换主题
    </button>

    <p>鼠标位置: {{ x }}, {{ y }}</p>
  </div>
</template>

Testing with Vitest

使用Vitest测试

Component Testing

组件测试

typescript
// Counter.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from '@/components/Counter.vue';

describe('Counter', () => {
  it('renders initial count', () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain('Count: 0');
  });

  it('increments count on button click', async () => {
    const wrapper = mount(Counter);

    await wrapper.find('button').trigger('click');

    expect(wrapper.text()).toContain('Count: 1');
  });

  it('accepts initial count prop', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 10 }
    });

    expect(wrapper.text()).toContain('Count: 10');
  });

  it('emits update event', async () => {
    const wrapper = mount(Counter);

    await wrapper.find('button').trigger('click');

    expect(wrapper.emitted('update')).toBeTruthy();
    expect(wrapper.emitted('update')![0]).toEqual([1]);
  });
});
typescript
// Counter.test.ts
import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import Counter from '@/components/Counter.vue';

describe('Counter', () => {
  it('渲染初始计数', () => {
    const wrapper = mount(Counter);
    expect(wrapper.text()).toContain('计数: 0');
  });

  it('点击按钮增加计数', async () => {
    const wrapper = mount(Counter);

    await wrapper.find('button').trigger('click');

    expect(wrapper.text()).toContain('计数: 1');
  });

  it('接受初始计数Prop', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 10 }
    });

    expect(wrapper.text()).toContain('计数: 10');
  });

  it('触发update事件', async () => {
    const wrapper = mount(Counter);

    await wrapper.find('button').trigger('click');

    expect(wrapper.emitted('update')).toBeTruthy();
    expect(wrapper.emitted('update')![0]).toEqual([1]);
  });
});

Testing with Pinia

结合Pinia测试

typescript
// UserProfile.test.ts
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, it, expect } from 'vitest';
import UserProfile from '@/components/UserProfile.vue';
import { useUserStore } from '@/stores/user';

describe('UserProfile', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('displays user name when logged in', () => {
    const userStore = useUserStore();
    userStore.user = { id: 1, firstName: 'Alice', lastName: 'Smith' };

    const wrapper = mount(UserProfile);

    expect(wrapper.text()).toContain('Alice Smith');
  });

  it('shows login prompt when not logged in', () => {
    const wrapper = mount(UserProfile);
    expect(wrapper.text()).toContain('Please log in');
  });
});
typescript
// UserProfile.test.ts
import { mount } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
import { beforeEach, describe, it, expect } from 'vitest';
import UserProfile from '@/components/UserProfile.vue';
import { useUserStore } from '@/stores/user';

describe('UserProfile', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('登录时显示用户名', () => {
    const userStore = useUserStore();
    userStore.user = { id: 1, firstName: 'Alice', lastName: 'Smith' };

    const wrapper = mount(UserProfile);

    expect(wrapper.text()).toContain('Alice Smith');
  });

  it('未登录时显示登录提示', () => {
    const wrapper = mount(UserProfile);
    expect(wrapper.text()).toContain('请登录');
  });
});

TypeScript Best Practices

TypeScript最佳实践

Component Props with Interface

使用接口定义组件Props

vue
<script setup lang="ts">
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface Props {
  user: User;
  showEmail?: boolean;
  onUpdate?: (user: User) => void;
}

const props = withDefaults(defineProps<Props>(), {
  showEmail: true
});

// Type-safe emits
const emit = defineEmits<{
  update: [user: User];
  delete: [userId: number];
}>();

function handleUpdate() {
  emit('update', props.user);
}
</script>
vue
<script setup lang="ts">
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface Props {
  user: User;
  showEmail?: boolean;
  onUpdate?: (user: User) => void;
}

const props = withDefaults(defineProps<Props>(), {
  showEmail: true
});

// 类型安全的事件触发
const emit = defineEmits<{
  update: [user: User];
  delete: [userId: number];
}>();

function handleUpdate() {
  emit('update', props.user);
}
</script>

Generic Components

泛型组件

vue
<script setup lang="ts" generic="T">
interface Props<T> {
  items: T[];
  keyFn: (item: T) => string | number;
  renderItem: (item: T) => string;
}

const props = defineProps<Props<T>>();
</script>

<template>
  <ul>
    <li v-for="item in items" :key="keyFn(item)">
      {{ renderItem(item) }}
    </li>
  </ul>
</template>
vue
<script setup lang="ts" generic="T">
interface Props<T> {
  items: T[];
  keyFn: (item: T) => string | number;
  renderItem: (item: T) => string;
}

const props = defineProps<Props<T>>();
</script>

<template>
  <ul>
    <li v-for="item in items" :key="keyFn(item)">
      {{ renderItem(item) }}
    </li>
  </ul>
</template>

Performance Optimization

性能优化

Virtual Scrolling for Large Lists

虚拟滚动处理大型列表

vue
<script setup lang="ts">
import { ref, computed } from 'vue';

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `Item ${i}`
})));

const containerHeight = 400;
const itemHeight = 40;
const scrollTop = ref(0);

const visibleCount = Math.ceil(containerHeight / itemHeight);
const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight));
const endIndex = computed(() => startIndex.value + visibleCount);
const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
);

const offsetY = computed(() => startIndex.value * itemHeight);
const totalHeight = computed(() => items.value.length * itemHeight);

function handleScroll(event: Event) {
  scrollTop.value = (event.target as HTMLElement).scrollTop;
}
</script>

<template>
  <div
    class="virtual-list"
    :style="{ height: containerHeight + 'px', overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: itemHeight + 'px' }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>
vue
<script setup lang="ts">
import { ref, computed } from 'vue';

const items = ref(Array.from({ length: 10000 }, (_, i) => ({
  id: i,
  text: `条目 ${i}`
})));

const containerHeight = 400;
const itemHeight = 40;
const scrollTop = ref(0);

const visibleCount = Math.ceil(containerHeight / itemHeight);
const startIndex = computed(() => Math.floor(scrollTop.value / itemHeight));
const endIndex = computed(() => startIndex.value + visibleCount);
const visibleItems = computed(() =>
  items.value.slice(startIndex.value, endIndex.value)
);

const offsetY = computed(() => startIndex.value * itemHeight);
const totalHeight = computed(() => items.value.length * itemHeight);

function handleScroll(event: Event) {
  scrollTop.value = (event.target as HTMLElement).scrollTop;
}
</script>

<template>
  <div
    class="virtual-list"
    :style="{ height: containerHeight + 'px', overflow: 'auto' }"
    @scroll="handleScroll"
  >
    <div :style="{ height: totalHeight + 'px', position: 'relative' }">
      <div :style="{ transform: `translateY(${offsetY}px)` }">
        <div
          v-for="item in visibleItems"
          :key="item.id"
          :style="{ height: itemHeight + 'px' }"
        >
          {{ item.text }}
        </div>
      </div>
    </div>
  </div>
</template>

Lazy Loading Components

懒加载组件

vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

// Lazy load heavy component
const HeavyComponent = defineAsyncComponent(() =>
  import('@/components/HeavyComponent.vue')
);

// With loading/error states
const AsyncComponent = defineAsyncComponent({
  loader: () => import('@/components/AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200, // Show loading after 200ms
  timeout: 3000 // Error if takes > 3s
});
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>
vue
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';

// 懒加载大型组件
const HeavyComponent = defineAsyncComponent(() =>
  import('@/components/HeavyComponent.vue')
);

// 带加载/错误状态的懒加载组件
const AsyncComponent = defineAsyncComponent({
  loader: () => import('@/components/AsyncComponent.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200, // 200ms后显示加载组件
  timeout: 3000 // 超过3秒触发错误
});
</script>

<template>
  <Suspense>
    <template #default>
      <HeavyComponent />
    </template>
    <template #fallback>
      <LoadingSpinner />
    </template>
  </Suspense>
</template>

Migration Guide

迁移指南

From Vue 2 to Vue 3

从Vue 2到Vue 3

Vue 2Vue 3Notes
data() { return {} }
ref()
,
reactive()
Composition API
computed: {}
computed(() => {})
Function-based
watch: {}
watch()
,
watchEffect()
Explicit watchers
mounted()
onMounted()
Import from 'vue'
this.$emit()
emit()
defineEmits
props: {}
defineProps<>()
TypeScript support
MixinsComposablesBetter composition
$listeners
Merged into
$attrs
Simplified
FiltersFunctions or computedRemoved
Vue 2Vue 3说明
data() { return {} }
ref()
,
reactive()
组合式API
computed: {}
computed(() => {})
函数式写法
watch: {}
watch()
,
watchEffect()
显式监听器
mounted()
onMounted()
从'vue'导入
this.$emit()
emit()
使用defineEmits
props: {}
defineProps<>()
支持TypeScript
Mixins可组合函数更优的组合方式
$listeners
合并到
$attrs
简化设计
过滤器函数或计算属性已移除

From React to Vue 3

从React到Vue 3

ReactVue 3Notes
useState(0)
ref(0)
Need .value in script
useMemo(() => x * 2, [x])
computed(() => x.value * 2)
Auto-tracked
useEffect(() => {}, [x])
watch(x, () => {})
Explicit deps
useEffect(() => {}, [])
onMounted()
Lifecycle
useCallback
Not neededAuto-stable
props.name
props.name
Similar
setState(prev => prev + 1)
count.value++
Direct mutation
JSXTemplateHTML-like syntax
ReactVue 3说明
useState(0)
ref(0)
脚本中需要使用.value
useMemo(() => x * 2, [x])
computed(() => x.value * 2)
自动追踪依赖
useEffect(() => {}, [x])
watch(x, () => {})
显式依赖
useEffect(() => {}, [])
onMounted()
生命周期钩子
useCallback
无需使用自动保持稳定
props.name
props.name
用法类似
setState(prev => prev + 1)
count.value++
直接修改状态
JSX模板类HTML语法

Best Practices

最佳实践

  1. Use Composition API over Options API for better type inference and composition
  2. Prefer
    ref()
    for primitives,
    reactive()
    for objects
    or just use
    ref()
    everywhere
  3. Use
    computed()
    for derived state
    instead of methods
  4. Destructure props early with
    defineProps()
    for type safety
  5. Use
    <script setup>
    for less boilerplate and better performance
  6. Key your v-for loops with unique IDs for proper reactivity
  7. Use Pinia over Vuex for better TypeScript support and devtools
  8. Lazy load routes and heavy components for faster initial load
  9. Use composables to extract and reuse logic across components
  10. Enable Vue DevTools for debugging reactivity and component tree
  1. 优先使用组合式API而非选项式API,获得更好的类型推断和组合能力
  2. 原始值用ref(),对象用reactive() 或统一使用ref()
  3. 使用computed()处理派生状态,而非方法
  4. 尽早用defineProps()解构Props,保证类型安全
  5. 使用<script setup>,减少样板代码并提升性能
  6. v-for循环使用唯一ID作为key,保证正确的响应式更新
  7. 使用Pinia替代Vuex,获得更好的TypeScript支持和开发者工具体验
  8. 懒加载路由和大型组件,提升首屏加载速度
  9. 使用可组合函数,提取并复用跨组件逻辑
  10. 启用Vue开发者工具,调试响应式状态和组件树

Resources

资源

Summary

总结

  • Vue 3 features Composition API with
    setup()
    ,
    ref()
    ,
    reactive()
    ,
    computed()
    ,
    watch()
  • Single-File Components (.vue) combine template, script, and style
  • TypeScript first-class support with
    defineProps<>()
    and
    defineEmits<>()
  • Vue Router for client-side routing with lazy loading and guards
  • Pinia modern state management with Composition API style
  • Vite lightning-fast development with HMR
  • Composables extract and reuse logic across components
  • Progressive adopt incrementally from simple to complex
  • Vue 3 具备使用ref、reactive、computed、watch的组合式API和setup()函数
  • 单文件组件(.vue)整合模板、脚本和样式
  • TypeScript 一等公民支持,通过defineProps<>()和defineEmits<>()实现类型安全
  • Vue Router 提供客户端路由,支持懒加载和导航守卫
  • Pinia 现代化状态管理,支持组合式API风格
  • Vite 提供极速开发体验和热模块替换
  • 可组合函数 用于提取和复用跨组件逻辑
  • 渐进式 可从简单场景逐步扩展到复杂应用