23. 与 Vue 集成
1. 概述
Vue 3 使用 TypeScript 重写,提供了出色的 TypeScript 支持。Vue 的组合式 API(Composition API)与 TypeScript 配合使用,可以提供完整的类型推断和类型安全。
┌─────────────────────────────────────────────────────────────┐ │ TypeScript + Vue 3 集成 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 组件类型 │ │ ├── defineComponent:组件定义函数 │ │ ├── defineProps:Props 定义 │ │ ├── defineEmits:事件定义 │ │ └── defineExpose:暴露属性 │ │ │ │ 响应式类型 │ │ ├── ref:响应式引用 │ │ ├── reactive:响应式对象 │ │ ├── computed:计算属性 │ │ └── watch:侦听器 │ │ │ │ 组合式 API 类型 │ │ ├── provide / inject:依赖注入 │ │ ├── template refs:模板引用 │ │ ├── lifecycle hooks:生命周期钩子 │ │ └── composables:组合函数 │ │ │ └─────────────────────────────────────────────────────────────┘2. 项目初始化
2.1 创建 Vue + TypeScript 项目
# 使用 Vite(推荐)npmcreate vite@latest my-vue-app ----templatevue-ts# 使用 Vue CLInpminstall-g@vue/cli vue create my-vue-app# 选择 TypeScript# 使用 create-vue(官方推荐)npmcreate vue@latest# 选择 TypeScript 支持2.2 tsconfig.json 配置
{"compilerOptions":{"target":"ES2020","useDefineForClassFields":true,"module":"ESNext","lib":["ES2020","DOM","DOM.Iterable"],"skipLibCheck":true,"moduleResolution":"bundler","allowImportingTsExtensions":true,"resolveJsonModule":true,"isolatedModules":true,"noEmit":true,"jsx":"preserve","strict":true,"noUnusedLocals":true,"noUnusedParameters":true,"noFallthroughCasesInSwitch":true},"include":["src/**/*.ts","src/**/*.tsx","src/**/*.vue"],"references":[{"path":"./tsconfig.node.json"}]}2.3 环境声明文件
// env.d.ts/// <reference types="vite/client" />declaremodule'*.vue'{importtype{DefineComponent}from'vue';constcomponent:DefineComponent<{},{},any>;exportdefaultcomponent;}3. 组件定义
3.1 使用 defineComponent
<script lang="ts"> import { defineComponent } from 'vue'; // 定义 Props 类型 interface Props { title: string; count?: number; } // 定义组件 export default defineComponent({ name: 'MyComponent', props: { title: { type: String, required: true }, count: { type: Number, default: 0 } }, setup(props: Props) { // props 自动推断类型 console.log(props.title); return () => ( <div> <h1>{props.title}</h1> <p>Count: {props.count}</p> </div> ); } }); </script>3.2 组合式 API 语法(推荐)
<script setup lang="ts"> import { ref, computed } from 'vue'; // 定义 Props interface Props { title: string; initialCount?: number; } const props = withDefaults(defineProps<Props>(), { initialCount: 0 }); // 定义 Emits interface Emits { (e: 'update:count', value: number): void; (e: 'submit', data: { name: string; age: number }): void; } const emit = defineEmits<Emits>(); // 响应式状态 const count = ref(props.initialCount); const name = ref<string>(''); // 计算属性 const doubleCount = computed(() => count.value * 2); // 方法 const increment = () => { count.value++; emit('update:count', count.value); }; const handleSubmit = () => { emit('submit', { name: name.value, age: count.value }); }; // 暴露给父组件的方法 defineExpose({ reset: () => { count.value = props.initialCount; } }); </script> <template> <div> <h1>{{ props.title }}</h1> <p>Count: {{ count }}</p> <p>Double: {{ doubleCount }}</p> <input v-model="name" placeholder="Enter name" /> <button @click="increment">Increment</button> <button @click="handleSubmit">Submit</button> </div> </template>4. Props 类型定义
4.1 基础 Props
<script setup lang="ts"> // 基础类型 interface Props { // 必需属性 title: string; // 可选属性 count?: number; // 联合类型 status: 'pending' | 'success' | 'error'; // 对象类型 user: { id: number; name: string; email: string; }; // 数组类型 items: string[]; // 函数类型 onClick: (value: string) => void; } // 使用 withDefaults 设置默认值 const props = withDefaults(defineProps<Props>(), { count: 0, items: () => [] }); // 复杂 Props 定义 interface User { id: number; name: string; } interface FormProps { user: User; onSubmit?: (data: User) => void; onCancel?: () => void; } const formProps = defineProps<FormProps>(); </script>4.2 泛型 Props
<script setup lang="ts" generic="T"> // 泛型组件 defineProps<{ items: T[]; renderItem: (item: T, index: number) => string; }>(); // 使用 // <List :items="users" :renderItem="(user) => user.name" /> </script>5. Emits 类型定义
<script setup lang="ts"> // 基础 Emits const emit = defineEmits<{ (e: 'update', value: string): void; (e: 'delete', id: number): void; (e: 'change', value: string, oldValue: string): void; }>(); // 使用对象语法 const emitObj = defineEmits({ update: (value: string) => typeof value === 'string', delete: (id: number) => typeof id === 'number', change: (value: string, oldValue: string) => { return typeof value === 'string' && typeof oldValue === 'string'; } }); // 使用 const handleUpdate = () => { emit('update', 'new value'); }; </script>6. 响应式类型
6.1 ref 和 reactive
<script setup lang="ts"> import { ref, reactive } from 'vue'; // ref 类型推断 const count = ref(0); // Ref<number> const message = ref('Hello'); // Ref<string> // 显式指定 ref 类型 const user = ref<User | null>(null); const status = ref<'idle' | 'loading' | 'success' | 'error'>('idle'); // reactive 类型 interface FormData { username: string; email: string; age: number; } const form = reactive<FormData>({ username: '', email: '', age: 0 }); // 使用泛型 const list = ref<User[]>([]); // 只读 ref const readonlyCount = readonly(count); </script>6.2 computed
<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: () => `${firstName.value} ${lastName.value}`, set: (value: string) => { const [first, last] = value.split(' '); firstName.value = first; lastName.value = last; } }); // 显式类型 const doubleCount = computed<number>(() => count.value * 2); </script>6.3 watch
<script setup lang="ts"> import { ref, watch, watchEffect } from 'vue'; const count = ref(0); const user = ref<User | null>(null); // 监听单个 ref watch(count, (newVal, oldVal) => { console.log(`Count changed from ${oldVal} to ${newVal}`); }); // 监听多个 watch([count, user], ([newCount, newUser], [oldCount, oldUser]) => { console.log('Multiple values changed'); }); // 深度监听 watch(user, (newUser, oldUser) => { console.log('User changed'); }, { deep: true }); // 立即执行 watch(count, (newVal) => { console.log(`Count is ${newVal}`); }, { immediate: true }); // watchEffect watchEffect(() => { console.log(`Count is ${count.value}`); }); </script>7. 模板引用
<script setup lang="ts"> import { ref, onMounted } from 'vue'; // DOM 元素引用 const inputRef = ref<HTMLInputElement | null>(null); // 组件实例引用 import ChildComponent from './ChildComponent.vue'; const childRef = ref<InstanceType<typeof ChildComponent> | null>(null); onMounted(() => { // 操作 DOM inputRef.value?.focus(); // 调用子组件方法 childRef.value?.reset(); }); // 动态引用(v-for) const itemRefs = ref<HTMLElement[]>([]); </script> <template> <input ref="inputRef" type="text" /> <ChildComponent ref="childRef" /> <div v-for="item in items" :key="item.id" :ref="(el) => itemRefs.push(el as HTMLElement)" > {{ item.name }} </div> </template>8. 生命周期钩子
<script setup lang="ts"> import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'; onBeforeMount(() => { console.log('Before mount'); }); onMounted(() => { console.log('Mounted'); }); onBeforeUpdate(() => { console.log('Before update'); }); onUpdated(() => { console.log('Updated'); }); onBeforeUnmount(() => { console.log('Before unmount'); }); onUnmounted(() => { console.log('Unmounted'); }); </script>9. Provide / Inject
<!-- Parent.vue --> <script setup lang="ts"> import { provide, ref } from 'vue'; // 定义注入的类型 interface ThemeContext { theme: 'light' | 'dark'; toggleTheme: () => void; } const theme = ref<'light' | 'dark'>('light'); const toggleTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light'; }; provide('theme', { theme, toggleTheme }); provide('count', ref(0)); </script> <template> <ChildComponent /> </template><!-- Child.vue --> <script setup lang="ts"> import { inject } from 'vue'; // 定义注入的类型 interface ThemeContext { theme: 'light' | 'dark'; toggleTheme: () => void; } // 注入并指定默认值 const themeContext = inject<ThemeContext>('theme'); const count = inject<number>('count', 0); // 带默认值 // 必需注入 const requiredTheme = inject<ThemeContext>('theme'); if (!requiredTheme) { throw new Error('Theme context not provided'); } // 使用 const { theme, toggleTheme } = requiredTheme; </script> <template> <div> <p>Current theme: {{ theme }}</p> <button @click="toggleTheme">Toggle Theme</button> </div> </template>10. 组合式函数(Composables)
// composables/useCounter.tsimport{ref,computed}from'vue';interfaceUseCounterOptions{initialValue?:number;step?:number;min?:number;max?:number;}interfaceUseCounterReturn{count:Readonly<Ref<number>>;step:Ref<number>;doubleCount:ComputedRef<number>;increment:()=>void;decrement:()=>void;setCount:(value:number)=>void;reset:()=>void;}exportfunctionuseCounter(options:UseCounterOptions={}):UseCounterReturn{const{initialValue=0,step=1,min=-Infinity,max=Infinity}=options;constcount=ref(initialValue);constcurrentStep=ref(step);constdoubleCount=computed(()=>count.value*2);constincrement=()=>{constnewValue=count.value+currentStep.value;count.value=Math.min(newValue,max);};constdecrement=()=>{constnewValue=count.value-currentStep.value;count.value=Math.max(newValue,min);};constsetCount=(value:number)=>{count.value=Math.min(Math.max(value,min),max);};constreset=()=>{count.value=initialValue;};return{count:readonly(count),step:currentStep,doubleCount,increment,decrement,setCount,reset};}// composables/useFetch.tsimport{ref,shallowRef,typeRef}from'vue';interfaceUseFetchOptions{immediate?:boolean;onError?:(error:Error)=>void;}interfaceUseFetchReturn<T>{data:Ref<T|null>;error:Ref<Error|null>;loading:Ref<boolean>;execute:()=>Promise<void>;}exportfunctionuseFetch<T=unknown>(url:Ref<string>|string,options:UseFetchOptions={}):UseFetchReturn<T>{const{immediate=true,onError}=options;constdata=shallowRef<T|null>(null);consterror=ref<Error|null>(null);constloading=ref(false);consturlRef=typeofurl==='string'?ref(url):url;constexecute=async()=>{loading.value=true;error.value=null;try{constresponse=awaitfetch(urlRef.value);if(!response.ok)thrownewError('Fetch failed');constjson=awaitresponse.json();data.value=json;}catch(err){error.value=errinstanceofError?err:newError('Unknown error');onError?.(error.value);}finally{loading.value=false;}};if(immediate){execute();}return{data,error,loading,execute};}<!-- 使用组合式函数 --> <script setup lang="ts"> import { useCounter } from './composables/useCounter'; import { useFetch } from './composables/useFetch'; // 使用计数器 const { count, increment, decrement, doubleCount, reset } = useCounter({ initialValue: 10, step: 2, min: 0, max: 20 }); // 使用数据获取 const { data, loading, error, execute } = useFetch<User[]>('/api/users'); </script> <template> <div> <h2>Counter</h2> <p>Count: {{ count }}</p> <p>Double: {{ doubleCount }}</p> <button @click="increment">+</button> <button @click="decrement">-</button> <button @click="reset">Reset</button> <h2>Users</h2> <div v-if="loading">Loading...</div> <div v-else-if="error">Error: {{ error.message }}</div> <ul v-else> <li v-for="user in data" :key="user.id">{{ user.name }}</li> </ul> <button @click="execute">Refresh</button> </div> </template>11. 完整示例:Todo 应用
<!-- TodoApp.vue --> <script setup lang="ts"> import { ref, computed } from 'vue'; // ============ 类型定义 ============ interface Todo { id: number; text: string; completed: boolean; createdAt: Date; } type FilterType = 'all' | 'active' | 'completed'; // ============ 状态 ============ const todos = ref<Todo[]>([ { id: 1, text: 'Learn Vue 3', completed: false, createdAt: new Date() }, { id: 2, text: 'Learn TypeScript', completed: false, createdAt: new Date() } ]); const filter = ref<FilterType>('all'); const newTodoText = ref(''); // ============ 计算属性 ============ const filteredTodos = computed(() => { switch (filter.value) { case 'active': return todos.value.filter(t => !t.completed); case 'completed': return todos.value.filter(t => t.completed); default: return todos.value; } }); const activeCount = computed(() => todos.value.filter(t => !t.completed).length); const completedCount = computed(() => todos.value.filter(t => t.completed).length); // ============ 方法 ============ const addTodo = () => { if (newTodoText.value.trim()) { todos.value.push({ id: Date.now(), text: newTodoText.value.trim(), completed: false, createdAt: new Date() }); newTodoText.value = ''; } }; const toggleTodo = (id: number) => { const todo = todos.value.find(t => t.id === id); if (todo) { todo.completed = !todo.completed; } }; const deleteTodo = (id: number) => { todos.value = todos.value.filter(t => t.id !== id); }; const clearCompleted = () => { todos.value = todos.value.filter(t => !t.completed); }; // ============ 生命周期 ============ // 保存到本地存储 import { watch } from 'vue'; watch(todos, (newTodos) => { localStorage.setItem('todos', JSON.stringify(newTodos)); }, { deep: true }); // 加载本地存储数据 const stored = localStorage.getItem('todos'); if (stored) { try { todos.value = JSON.parse(stored); } catch (e) { console.error('Failed to load todos'); } } </script> <template> <div class="todo-app"> <h1>Todo App</h1> <!-- 添加表单 --> <form @submit.prevent="addTodo"> <input v-model="newTodoText" type="text" placeholder="Add a new todo..." /> <button type="submit">Add</button> </form> <!-- 过滤按钮 --> <div class="filters"> <button v-for="f in ['all', 'active', 'completed']" :key="f" :class="{ active: filter === f }" @click="filter = f as FilterType" > {{ f }} </button> </div> <!-- 待办列表 --> <ul> <li v-for="todo in filteredTodos" :key="todo.id" :class="{ completed: todo.completed }" > <input type="checkbox" :checked="todo.completed" @change="toggleTodo(todo.id)" /> <span>{{ todo.text }}</span> <button @click="deleteTodo(todo.id)">Delete</button> </li> </ul> <!-- 统计 --> <div class="stats"> <span>{{ activeCount }} items left</span> <button v-if="completedCount > 0" @click="clearCompleted" > Clear completed ({{ completedCount }}) </button> </div> </div> </template> <style scoped> .completed { text-decoration: line-through; opacity: 0.6; } .filters button.active { font-weight: bold; color: blue; } </style>12. 总结
| 类型 | 用途 | 示例 |
|---|---|---|
defineComponent | 组件定义 | defineComponent({ ... }) |
defineProps<T>() | Props 定义 | defineProps<{ title: string }>() |
defineEmits<T>() | 事件定义 | defineEmits<{ (e: 'click'): void }>() |
withDefaults | 默认值 | withDefaults(defineProps<Props>(), { count: 0 }) |
ref<T> | 响应式引用 | const count = ref<number>(0) |
reactive<T> | 响应式对象 | const state = reactive<State>({ ... }) |
computed<T> | 计算属性 | const double = computed(() => count.value * 2) |
watch | 侦听器 | watch(count, (newVal) => {}) |
inject<T> | 注入 | const data = inject<T>('key') |
InstanceType<typeof Comp> | 组件实例类型 | const ref = ref<InstanceType<typeof Child>>() |