vue
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseVue 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
undefinedVue 3 是一个渐进式框架,用于构建用户界面,强调易用性、性能和灵活性。它具备用于更好逻辑复用的组合式API、强大的响应式系统以及单文件组件(.vue 文件)。
核心特性:
- 组合式API:搭配ref、reactive、computed、watch的setup()函数
- 响应式系统:细粒度的响应式数据追踪
- 单文件组件:将模板、脚本、样式整合在一个文件中
- Vue Router:官方SPA路由解决方案
- Pinia:现代化状态管理工具(Vuex的继任者)
- TypeScript:一等公民级别的TypeScript支持
- Vite:支持热模块替换(HMR)的极速开发构建工具
安装方式:
bash
undefinedCreate 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
undefinednpm create vite@latest my-app -- --template vue-ts
undefinedComposition 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 2 | Vue 3 | Notes |
|---|---|---|
| | Composition API |
| | Function-based |
| | Explicit watchers |
| | Import from 'vue' |
| | defineEmits |
| | TypeScript support |
| Mixins | Composables | Better composition |
| Merged into | Simplified |
| Filters | Functions or computed | Removed |
| Vue 2 | Vue 3 | 说明 |
|---|---|---|
| | 组合式API |
| | 函数式写法 |
| | 显式监听器 |
| | 从'vue'导入 |
| | 使用defineEmits |
| | 支持TypeScript |
| Mixins | 可组合函数 | 更优的组合方式 |
| 合并到 | 简化设计 |
| 过滤器 | 函数或计算属性 | 已移除 |
From React to Vue 3
从React到Vue 3
| React | Vue 3 | Notes |
|---|---|---|
| | Need .value in script |
| | Auto-tracked |
| | Explicit deps |
| | Lifecycle |
| Not needed | Auto-stable |
| | Similar |
| | Direct mutation |
| JSX | Template | HTML-like syntax |
| React | Vue 3 | 说明 |
|---|---|---|
| | 脚本中需要使用.value |
| | 自动追踪依赖 |
| | 显式依赖 |
| | 生命周期钩子 |
| 无需使用 | 自动保持稳定 |
| | 用法类似 |
| | 直接修改状态 |
| JSX | 模板 | 类HTML语法 |
Best Practices
最佳实践
- Use Composition API over Options API for better type inference and composition
- Prefer for primitives,
ref()for objects or just usereactive()everywhereref() - Use for derived state instead of methods
computed() - Destructure props early with for type safety
defineProps() - Use for less boilerplate and better performance
<script setup> - Key your v-for loops with unique IDs for proper reactivity
- Use Pinia over Vuex for better TypeScript support and devtools
- Lazy load routes and heavy components for faster initial load
- Use composables to extract and reuse logic across components
- Enable Vue DevTools for debugging reactivity and component tree
- 优先使用组合式API而非选项式API,获得更好的类型推断和组合能力
- 原始值用ref(),对象用reactive() 或统一使用ref()
- 使用computed()处理派生状态,而非方法
- 尽早用defineProps()解构Props,保证类型安全
- 使用<script setup>,减少样板代码并提升性能
- v-for循环使用唯一ID作为key,保证正确的响应式更新
- 使用Pinia替代Vuex,获得更好的TypeScript支持和开发者工具体验
- 懒加载路由和大型组件,提升首屏加载速度
- 使用可组合函数,提取并复用跨组件逻辑
- 启用Vue开发者工具,调试响应式状态和组件树
Resources
资源
- Vue 3 Docs: https://vuejs.org/guide/introduction.html
- Vue Router: https://router.vuejs.org/
- Pinia: https://pinia.vuejs.org/
- Vite: https://vitejs.dev/
- Vue DevTools: https://devtools.vuejs.org/
- Awesome Vue: https://github.com/vuejs/awesome-vue
- Vue 3官方文档: https://vuejs.org/guide/introduction.html
- Vue Router: https://router.vuejs.org/
- Pinia: https://pinia.vuejs.org/
- Vite: https://vitejs.dev/
- Vue开发者工具: https://devtools.vuejs.org/
- Awesome Vue: https://github.com/vuejs/awesome-vue
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 and
defineProps<>()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 提供极速开发体验和热模块替换
- 可组合函数 用于提取和复用跨组件逻辑
- 渐进式 可从简单场景逐步扩展到复杂应用