使用 defineStore
的选项式语法,Pinia 会自动推断类型,但可以显式 定义类型以增强类型安全。
import { defineStore } from 'pinia';
// 1. 定义 State 的接口(可选,但推荐)
interface UserStoreState {
users: User[];
currentPage: number;
isLoading: boolean;
}
// 2. 定义 Store
export const useUserStore = defineStore('user', {
// State(显式标注类型)
state: (): UserStoreState => ({
users: [],
currentPage: 1,
isLoading: false,
}),
// Getters(自动推断返回类型)
getters: {
// 示例:过滤用户
activeUsers: (state) => state.users.filter(user => user.isActive),
// 带参数的类型标注
getUserById: (state) => {
return (userId: string) => state.users.find(user => user.id === userId);
},
},
// Actions(显式标注参数和返回类型)
actions: {
async fetchUsers() {
this.isLoading = true;
try {
const response = await api.get<User[]>(`/users?page=${this.currentPage}`);
this.users = response.data;
} catch (error) {
console.error('Failed to fetch users:', error);
} finally {
this.isLoading = false;
}
},
setPage(page: number) {
this.currentPage = page;
},
},
});
使用 storeToRefs
保持响应式:
在组件中解构 Store 时保留响应式。
import { storeToRefs } from 'pinia';
const store = useUserStore();
const { users, currentPage } = storeToRefs(store); // 保持响应式
Vue的响应式原理是其数据驱动视图的核心机制,Vue 2和Vue 3在实现方式上有显著差异,主要体现在底层依赖的API和对数据变化的检测能力上:
实现方式:
Vue 2 使用 Object.defineProperty
对对象的属性进行劫持,将其转化为 getter 和 setter。
依赖收集(Getter):
当组件渲染时访问数据属性,触发 getter
,将当前的 Watcher(依赖)收集到依赖列表中(Dep)。
触发更新(Setter):
当数据被修改时,触发 setter
,通知所有依赖的 Watcher 更新视图。
局限性:
Vue.set
或 Vue.delete
显式操作才能触发响应式更新。arr[0] = 1
)或修改数组长度(如 arr.length = 0
)无法触发更新。push
、pop
、splice
等变异方法(Mutation Methods)来支持数组响应式。示例:
// Vue 2 数据初始化
const data = { count: 0 };
Object.defineProperty(data, 'count', {
get() {
// 收集依赖
dep.depend();
return value;
},
set(newVal) {
value = newVal;
// 触发更新
dep.notify();
}
});
实现方式:
Vue 3 改用 Proxy 代理整个对象,结合 Reflect 实现更全面的拦截能力。
Proxy 的拦截操作:
Proxy 可以拦截对象的 get
、set
、deleteProperty
等操作,无需逐个劫持属性。
深层响应式:
通过递归代理嵌套对象,实现深层次的响应式跟踪。
优势:
obj.newKey = value
或 delete obj.key
也能触发响应式。arr.length
即可触发更新。示例:
// Vue 3 数据初始化
const data = { count: 0 };
const proxy = new Proxy(data, {
get(target, key, receiver) {
track(target, key); // 收集依赖
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver);
trigger(target, key); // 触发更新
return true;
}
});
特性 | Vue 2(Object.defineProperty) | Vue 3(Proxy) |
---|---|---|
属性监听范围 | 仅能劫持已存在的属性 | 支持动态新增、删除属性 |
数组监听 | 需重写数组方法(push/pop等) | 原生支持索引修改和length变化 |
深层嵌套对象 | 初始化时递归遍历所有属性 | 按需代理(访问时才递归) |
兼容性 | 支持 IE9+ | 不支持 IE(需 Polyfill) |
性能 | 初始化时递归劫持,大型对象性能较差 | 延迟代理,内存和初始化性能更优 |
复杂数据结构支持 | 不支持 Map、Set、WeakMap 等 | 支持 |
特性 | v-if | v-show |
---|---|---|
实现方式 | 动态添加/移除DOM元素 | 通过CSS display 切换显示状态 |
初始渲染 | 条件为false 时不渲染元素 |
无论条件如何,始终渲染元素 |
切换性能 | 较高(涉及DOM操作) | 较低(仅修改CSS属性) |
适用场景 | 条件不频繁切换且可能为false |
条件频繁切换 |
生命周期 | 触发组件的创建/销毁生命周期钩子 | 不触发生命周期,仅切换显示 |
与v-else 配合 |
支持逻辑分支(v-else /v-else-if ) |
不支持 |
v-if:
false
,无需渲染元素。v-show:
v-if:
条件首次为true
时触发组件的created
和mounted
钩子;条件变为false
时触发unmounted
钩子。
v-show:
无论条件如何变化,组件的生命周期钩子不会触发,元素始终存在。
beforeCreate
data
)和事件配置(methods
)之前。created
beforeMount
mounted
this.$el
。beforeUpdate
updated
beforeDestroy
(Vue 2)/ beforeUnmount
(Vue 3)
destroyed
(Vue 2)/ unmounted
(Vue 3)
activated
缓存的组件重新激活时(如切换回该组件)。deactivated
缓存的组件停用时(如切换到其他组件)。created
阶段created() {
this.fetchUserData(); // 初始化用户数据
}
mounted
阶段mounted() {
this.initChart(); // 依赖 DOM 的图表库初始化
}
activated
阶段(配合
)activated() {
this.refreshData(); // 重新请求最新数据
}
mitt
)。Vuex(Vue 2 官方方案):
state
、mutations
、actions
管理数据流。// 组件中获取状态
this.$store.state.count;
// 触发 Action
this.$store.dispatch('fetchData');
Pinia(Vue 3 推荐方案):
// 定义 Store
export const useStore = defineStore('main', {
state: () => ({ count: 0 }),
actions: { increment() { this.count++ } }
});
// 组件中使用
const store = useStore();
store.count++;
$parent
/ $children
$refs
ref
属性获取子组件实例。
storage
事件。
{{ slotProps.data }}
场景 | 推荐方式 |
---|---|
父子组件简单通信 | Props / $emit |
兄弟组件通信 | 事件总线 / 状态管理库 |
跨层级组件共享数据 | Provide/Inject |
复杂应用状态管理 | Vuex(Vue 2) / Pinia(Vue 3) |
需要直接操作子组件 | $refs |
非响应式数据或简单全局配置 | 事件总线 / 浏览器存储 |
特性 | interface |
type (类型别名) |
---|---|---|
声明合并 | ✅ 支持(同名接口自动合并) | ❌ 不支持 |
扩展方式 | 通过 extends 继承 |
通过 & (交叉类型)组合 |
适用类型范围 | 主要用于对象类型 | 支持更广泛的类型(联合、元组、字面量等) |
实现(implements) | 类可以直接实现接口 | 类不能直接实现类型别名(除非是对象类型) |
工具类型兼容性 | 完全支持(如 Partial ) |
完全支持(如 Partial ) |
interface
:允许多次声明同名接口,TypeScript 会自动合并成员。
interface User {
name: string;
}
interface User {
age: number;
}
// 最终 User 接口包含 name 和 age
const user: User = { name: "Alice", age: 25 };
type
:同名类型别名会报错(重复定义)。
type User = { name: string }; // ✅
type User = { age: number }; // ❌ Error: Duplicate identifier 'User'
interface
:通过 extends
继承其他接口。interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
interface
:主要用于定义对象类型。
interface Point {
x: number;
y: number;
}
type
:支持更复杂的类型定义:
type Status = "success" | "error";
type Coordinates = [number, number];
type Handler = (input: string) => void;
type ReadonlyUser = Readonly;
type ID = string | number;
interface
的情况Window
添加自定义属性)。type
的情况type Result<T> = { data: T } | { error: string };
type Direction = "up" | "down" | "left" | "right";
type PartialUser = Partial<User>;
interface
:
interface SearchFunc {
(source: string, keyword: string): boolean;
}
type
:
type SearchFunc = (source: string, keyword: string) => boolean;
type
)type UserID = string | number;
function getUser(id: UserID) { ... }
假设我们想要编写一个函数,该函数返回传入的参数本身。如果我们不使用泛型,我们可能需要为每种类型都写一个这样的函数,或者让函数接受并返回any
类型,但这会失去类型检查的好处。使用泛型,我们可以这样写:
function identity<T>(arg: T): T {
return arg;
}
在这个例子中,T
是一个类型变量,它代表了一个未指定的类型。当我们调用identity
函数时,可以显式地指定T
的具体类型,或者让TypeScript根据传入的参数自动推断出类型。
let output = identity<string>("myString");
console.log(output); // 输出: "myString"
// 或者让TypeScript自动推断类型
output = identity("myString"); // TypeScript会自动推断T为string
console.log(output); // 输出: "myString"
有时候,你可能希望对泛型的类型进行一些限制,只允许某些类型的值被传递。这时,你可以使用泛型约束。例如,如果你只想允许对象类型的值,可以这样做:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({length: 10, value: 'hello'}); // 正确
// loggingIdentity(37); // 错误,数字没有length属性
as
)**明确知道值的实际类型
当开发者比编译器更清楚值的类型时,强制指定类型。
// 从外部数据源获取的值
const data = JSON.parse('{ "name": "Alice" }') as { name: string };
处理联合类型的窄化
在无法通过类型守卫(typeof
/instanceof
)缩小类型范围时,手动断言。
const element = document.getElementById("input") as HTMLInputElement;
兼容旧代码或第三方库
处理类型定义不完整的第三方库返回值。
const value = (window as any).externalValue as string;
const num = "123" as number; // ❌ 错误断言,但编译通过
console.log(num.toFixed(2)); // 运行时报错
if (typeof value === "number") {
value.toFixed(2); // 安全访问
}
function assertIsNumber(value: unknown): asserts value is number {
if (typeof value !== "number") throw new Error("Not a number");
}
!
)**明确变量不为 null
/undefined
当开发者确定某个可能为空的变量在特定上下文中一定有值。
// React Ref 访问
const inputRef = useRef<HTMLInputElement>(null);
inputRef.current!.focus(); // 确认组件已挂载
访问可选属性或方法
强制跳过编译器的空值检查。
type User = { name?: string };
const user: User = { name: "Alice" };
console.log(user.name!.toUpperCase()); // 确保 name 存在
初始化延迟赋值的变量
结合 !
声明变量,稍后赋值。
let value!: string; // 明确告知编译器稍后赋值
setTimeout(() => { value = "Hello"; }, 1000);
function printName(user: User | null) {
console.log(user!.name); // ❌ 若 user 为 null,运行时崩溃
}
?.
):console.log(user?.name?.toUpperCase()); // 安全且简洁
??
):const name = user?.name ?? "Unknown";
场景 | 工具 | 风险控制 |
---|---|---|
明确知道类型,但编译器无法推断 | as |
确保逻辑正确性,避免错误断言 |
确定变量非空 | ! |
结合代码逻辑验证非空性 |
处理第三方库不完整类型 | as + 类型声明文件 |
优先完善类型定义文件 |
as
或 !
等可能引发运行时错误的手段。typeof
类型守卫适用类型:基本类型(string
、number
、boolean
、symbol
、undefined
、object
、function
)。
示例:
function printValue(value: string | number) {
if (typeof value === "string") {
console.log(value.toUpperCase()); // value 类型缩小为 string
} else {
console.log(value.toFixed(2)); // value 类型缩小为 number
}
}
instanceof
类型守卫适用类型:类的实例类型。
示例:
class Dog { bark() {} }
class Cat { meow() {} }
function handlePet(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // pet 类型缩小为 Dog
} else {
pet.meow(); // pet 类型缩小为 Cat
}
}
in
类型守卫适用类型:对象属性区分联合类型。
示例:
interface Bird { fly(): void }
interface Fish { swim(): void }
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly(); // animal 类型缩小为 Bird
} else {
animal.swim(); // animal 类型缩小为 Fish
}
}
适用类型:联合类型中的字面量值(如 "success" | "error"
)。
示例:
type Status = "success" | "error";
function logStatus(status: Status) {
if (status === "success") {
console.log("Operation succeeded");
} else {
console.error("Operation failed");
}
}
适用类型:复杂类型检查逻辑。
语法:函数返回类型为 value is Type
(类型谓词)。
示例:
interface User { name: string; age: number }
interface Admin { name: string; role: string }
function isAdmin(user: User | Admin): user is Admin {
return "role" in user; // 返回 true 时,user 被识别为 Admin
}
function handleUser(user: User | Admin) {
if (isAdmin(user)) {
console.log(user.role); // user 类型缩小为 Admin
} else {
console.log(user.age); // user 类型缩小为 User
}
}
使用 never
类型确保所有联合类型分支都被处理:
type Shape = "circle" | "square" | "triangle";
function getArea(shape: Shape) {
switch (shape) {
case "circle": return "πr²";
case "square": return "a²";
case "triangle": return "(base*height)/2";
default:
// 如果未来新增类型但未处理,此处会报错
const _exhaustiveCheck: never = shape;
throw new Error("Unknown shape");
}
}
对复杂类型进行复用:
type SuccessResponse = { status: 200; data: any };
type ErrorResponse = { status: 400 | 500; message: string };
function handleResponse(res: SuccessResponse | ErrorResponse) {
if (res.status === 200) {
console.log(res.data); // res 类型缩小为 SuccessResponse
} else {
console.error(res.message); // res 类型缩小为 ErrorResponse
}
}
defineProps
和泛型直接在 中使用TypeScript接口或类型别名定义props类型:
<script setup lang="ts">
interface User {
name: string;
age: number;
}
// 定义props类型
const props = defineProps<{
message: string; // 必传属性
count?: number; // 可选属性(通过 ? 标记)
user: User; // 复杂对象类型
tags: string[]; // 数组类型
}>();
</script>
withDefaults
结合 withDefaults
为可选props提供默认值:
const props = withDefaults(
defineProps<{
message: string;
count?: number;
}>(),
{
count: 0 // 默认值
}
);
PropType
定义复杂类型通过 PropType
包装类型,明确对象、数组或函数的结构:
import { defineComponent, PropType } from 'vue';
interface User {
name: string;
age: number;
}
export default defineComponent({
props: {
message: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
user: {
type: Object as PropType<User>, // 明确对象类型
required: true
},
tags: {
type: Array as PropType<string[]>, // 明确数组类型
default: () => []
},
onAction: {
type: Function as PropType<(id: number) => void>, // 函数类型
required: true
}
}
});
场景 | 方法 | 优点 | 注意事项 |
---|---|---|---|
Vue 3 Composition API | defineProps + 泛型 |
简洁,无需额外导入,类型直接推导 | 默认值需配合 withDefaults |
Vue 3 Options API | PropType 包装复杂类型 |
兼容Options API写法 | 需要显式定义类型 |
Vue 2 | vue-property-decorator 装饰器 |
适合类组件风格 | 依赖第三方库,Vue 3不推荐使用 |
ref
的类型标注直接通过泛型
指定类型:
import { ref } from 'vue';
// 明确标注为 number 类型
const count = ref<number>(0);
// 自动推断为 number 类型(可省略泛型)
const count = ref(0);
当 ref
存储对象时,推荐显式标注泛型:
interface User {
name: string;
age: number;
}
// 显式标注类型
const user = ref<User>({
name: 'Alice',
age: 25
});
null
的初始值使用联合类型标注初始空值:
// DOM 元素引用(需明确泛型)
const el = ref<HTMLDivElement | null>(null);
// 异步初始化数据
const data = ref<string | null>(null);
setTimeout(() => {
data.value = "Loaded"; // 安全赋值
}, 1000);
reactive
的类型标注通过接口或类型别名定义类型:
import { reactive } from 'vue';
interface State {
count: number;
list: string[];
}
// 直接标注类型
const state = reactive<State>({
count: 0,
list: []
});
// 或者依赖类型推断(推荐)
const state: State = reactive({
count: 0,
list: []
});
reactive
会自动递归转换嵌套对象,但需明确定义类型:
interface Nested {
a: number;
b: {
c: string;
};
}
const nested = reactive<Nested>({
a: 1,
b: {
c: 'nested'
}
});
computed
的类型标注根据 getter
的返回值自动推断类型:
import { ref, computed } from 'vue';
const count = ref(0);
// 自动推断为 ComputedRef
const double = computed(() => count.value * 2);
当计算逻辑复杂或需要联合类型时,显式标注:
const user = ref<User | null>(null);
// 显式标注返回类型
const userName = computed<string>(() => {
return user.value?.name || 'Guest';
});
toRef
和 toRefs
从响应式对象中提取属性并保持响应性:
import { reactive, toRef, toRefs } from 'vue';
interface State {
x: number;
y: number;
}
const state = reactive<State>({ x: 0, y: 0 });
// 提取单个属性(标注类型)
const xRef = toRef<number>(state, 'x');
// 解构所有属性(自动推断类型)
const { x, y } = toRefs(state);
在明确类型但编译器无法推断时使用 as
:
const unknownData = ref({} as Record<string, unknown>);
unknownData.value.key = "value"; // 允许动态属性
使用类型守卫缩小联合类型范围:
type Data = string | number;
const data = ref<Data>("initial");
if (typeof data.value === "string") {
data.value.toUpperCase(); // 类型安全
}
在 src/router/index.ts
中定义路由配置,并通过 RouteRecordRaw
类型标注:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
// 定义路由的 meta 字段类型(可选)
declare module 'vue-router' {
interface RouteMeta {
// 添加自定义 meta 字段
requiresAuth?: boolean;
title?: string;
}
}
// 定义路由配置数组
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
meta: {
requiresAuth: true, // ✅ 类型检查
title: '首页' // ✅ 类型检查
}
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
// 参数类型检查(通过 props)
props: (route) => ({
id: Number(route.params.id), // params.id 自动推断为 string,需手动转换
query: route.query.search
})
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
如果路由参数(params
)需要特定类型,可以通过泛型扩展 RouteParams
:
// 扩展路由参数类型(src/router/types.d.ts)
import 'vue-router';
declare module 'vue-router' {
interface RouteParams {
id: string; // 定义全局路由参数类型
postId?: string; // 可选参数
}
}
在组件中通过 useRoute
获取当前路由,并自动推断类型:
import { useRoute } from 'vue-router';
export default {
setup() {
const route = useRoute();
// 参数类型安全(id 被推断为 string)
const userId = route.params.id;
// meta 类型安全(requiresAuth 被推断为 boolean | undefined)
if (route.meta.requiresAuth) {
// 权限校验逻辑
}
return { userId };
}
};
通过联合类型限制路由的 name
字段,确保跳转时使用合法的路由名称:
// 定义路由名称的联合类型
type RouteName = 'Home' | 'User' | 'Profile';
// 路由配置中强制 name 字段为 RouteName
const routes: Array<RouteRecordRaw & { name: RouteName }> = [
{
name: 'Home',
path: '/',
component: Home
},
{
name: 'User',
path: '/user/:id',
component: User
}
];
// 编程式导航时类型检查
import { useRouter } from 'vue-router';
const router = useRouter();
router.push({ name: 'Hom' }); // ❌ 错误:'Hom' 不在 RouteName 中
router.push({ name: 'Home' }); // ✅ 正确
维度 | Options API | Composition API |
---|---|---|
逻辑组织 | 按选项分散,复杂组件难维护 | 按功能聚合,代码内聚 |
逻辑复用 | Mixins 易冲突,难以追踪 | 函数式封装,无副作用,复用灵活 |
TypeScript | 类型推导受限,需额外声明 | 天然支持,类型安全 |
响应式控制 | 全自动转换,可能性能浪费 | 显式控制,精准优化 |
生命周期 | 分散在各选项,管理不便 | 集中管理,与相关逻辑绑定 |
适用场景 | 简单组件,新手友好 | 复杂应用,追求可维护性和扩展性 |
Vue 的虚拟 DOM(Virtual DOM)是一种优化真实 DOM 操作的机制,通过抽象和高效的更新策略提升应用性能。以下是其核心原理及优势的详细解析:
抽象表示: 状态变化触发更新: Diff 算法对比差异: 批量更新真实 DOM: 静态节点提升(Static Hoisting): 补丁标志(Patch Flags): 块树优化(Block Tree): 用途:快速创建一个新类型,将 用途:从类型 工具类型基于 映射类型(Mapped Types) 和 条件类型(Conditional Types) 实现。例如, 在 Vue 3 中为全局属性(如 在 在入口文件(如 在组件中通过 若需添加更多全局属性(如 对于 Vue 2 + TypeScript 项目,需通过 使用 若需明确类型(如组件选项复杂时),使用泛型或类型断言: 为加载状态、错误处理等配置选项时,确保类型正确: 确保异步组件的Props类型在父组件中正确传递: 全局注册时扩展类型声明,确保模板中类型检查生效: 若模板中提示缺失,在类型声明文件中扩展: 结合 动态路径需通过泛型或类型工具确保类型安全: 模块未找到: Props类型不匹配: 异步组件选项类型错误: 假设我们需要创建一个 数据加载高阶组件,它能够: 首先定义 HOC 的类型签名,支持泛型参数 创建一个工厂函数 创建一个需要数据加载功能的普通组件: 在父组件中使用 HOC 包装业务组件: 如果错误传递 Props,TypeScript 会报错: 当需要手动指定数据类型时,可以显式传递泛型参数: > 若需要处理事件和插槽,可扩展高阶组件类型:
虚拟 DOM 是一个轻量级的 JavaScript 对象,用于描述真实 DOM 的结构。例如,一个真实的 {
tag: 'div',
props: { id: 'app', class: 'container' },
children: [
{ tag: 'h1', props: {}, children: 'Hello Vue' }
]
}
当组件状态(如 data
)变化时,Vue 会生成新的虚拟 DOM 树(称为 VNode 树)。
Vue 通过 Diff 算法 比较新旧虚拟 DOM 树的差异,找出需要更新的最小节点集合。
仅对真实 DOM 中变化的部分进行定向更新,避免全量渲染。虚拟 DOM 的核心优势
1. 性能优化
真实 DOM 操作涉及浏览器重排(Reflow)和重绘(Repaint),成本极高。虚拟 DOM 通过合并多次更新为单次操作,减少性能损耗。
Vue 的 Diff 算法采用 双端对比策略,优先处理头尾节点,减少遍历次数。例如,对于列表更新,Vue 会复用相同位置的节点,仅更新内容或顺序。2. 跨平台能力
虚拟 DOM 不依赖浏览器环境,可在不同平台(如服务器端 SSR、原生应用、小程序)转换为特定输出。例如:
3. 简化开发心智模型
开发者只需描述 UI 的最终状态(如通过模板或渲染函数),无需手动操作 DOM。Vue 自动处理状态到视图的映射。
多次数据变化会被合并为一次虚拟 DOM 计算,避免开发者手动优化。4. 优化复杂场景
在频繁切换 v-if
或动态组件时,虚拟 DOM 复用已有节点,避免重复创建销毁 DOM。v-for
):
通过 key
属性帮助 Diff 算法识别节点身份,减少不必要的节点移动。虚拟 DOM 的优化策略(Vue 特有)
将静态内容(如纯文本)提取为常量,跳过 Diff 过程。// 静态节点在多次渲染中复用
const staticNode = createVNode('div', null, 'Static Content');
在编译阶段标记动态绑定的属性(如 :class
、v-model
),Diff 时仅检查标记部分。// 编译时标记动态 class
{ class: 'user', _patchFlag: 1 /* CLASS */ }
将模板划分为动态块(Block),仅追踪动态子树的更新,减少 Diff 范围。虚拟 DOM vs 直接操作 DOM
场景
直接操作 DOM
虚拟 DOM
性能
单次操作快,但频繁操作导致性能骤降
批量更新,适合频繁变化的复杂场景
开发复杂度
需手动优化,心智负担高
声明式编码,自动优化更新
跨平台支持
依赖浏览器环境
支持多平台渲染(SSR、Native 等)
适用规模
简单、小规模交互
中大型应用,尤其是动态内容多的项目
TS中的工具类型(Utility Types)有哪些?举例Partial和Pick的用途。
1.
Partial
:将类型属性变为可选T
的所有属性标记为可选(添加 ?
)。
场景:适用于需要部分更新对象的场景(如表单提交、Patch 请求)。
示例:interface User {
id: number;
name: string;
age: number;
}
// 所有属性变为可选
type PartialUser = Partial<User>;
/*
等效于:
{
id?: number;
name?: string;
age?: number;
}
*/
// 使用示例:允许只更新部分字段
function updateUser(id: number, fields: PartialUser) {
// 调用 API 更新用户信息
}
updateUser(1, { age: 26 }); // ✅ 只更新 age
2.
Pick
:选取指定属性T
中选取一组属性 K
构造新类型。
场景:提取对象的部分属性,避免重复定义。
示例:interface User {
id: number;
name: string;
age: number;
email: string;
}
// 仅选取 'id' 和 'name' 属性
type UserPreview = Pick<User, 'id' | 'name'>;
/*
等效于:
{
id: number;
name: string;
}
*/
// 使用示例:渲染用户简略信息
function renderPreview(user: UserPreview) {
console.log(`${user.id}: ${user.name}`);
}
其他常用工具类型
3.
Readonly
:所有属性变为只读type ReadonlyUser = Readonly<User>;
// 所有属性变为只读,不可修改
const user: ReadonlyUser = { id: 1, name: "Alice" };
user.id = 2; // ❌ 编译错误
4.
Record
:构造键类型为 K,值类型为 T 的对象type UserRoles = Record<string, 'admin' | 'user'>;
/*
等效于:
{
[key: string]: 'admin' | 'user';
}
*/
const roles: UserRoles = {
alice: 'admin',
bob: 'user'
};
5.
Omit
:排除指定属性type UserWithoutEmail = Omit<User, 'email'>;
/*
等效于:
{
id: number;
name: string;
age: number;
}
*/
6.
Required
:所有属性变为必填type RequiredUser = Required<PartialUser>;
// 将 PartialUser 的可选属性还原为必填
7.
Exclude
/ Extract
:集合操作
Exclude
:从 T
中排除可赋值给 U
的类型。type T = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
Extract
:从 T
中提取可赋值给 U
的类型。type T = Extract<"a" | "b" | 1, string>; // "a" | "b"
工具类型的底层原理
Partial
的实现可简化为:type Partial<T> = {
[P in keyof T]?: T[P];
};
如何为全局属性(如$axios)添加类型声明?
$axios
)添加类型声明,可以通过扩展 Vue 的 ComponentCustomProperties
接口来实现。以下是详细步骤:步骤 1:在项目中创建类型声明文件
src
目录下新建一个类型声明文件(如 src/types/vue.d.ts
),用于扩展 Vue 的类型:// src/types/vue.d.ts
import { ComponentCustomProperties } from 'vue';
import { AxiosInstance } from 'axios'; // 导入 axios 的类型
// 扩展全局属性类型
declare module 'vue' {
interface ComponentCustomProperties {
$axios: AxiosInstance; // 添加全局属性类型声明
// 其他全局属性示例:
// $translate: (key: string) => string;
}
}
步骤 2:挂载全局属性
src/main.ts
)中,将 $axios
挂载到 Vue 实例:// src/main.ts
import { createApp } from 'vue';
import axios from 'axios';
import App from './App.vue';
const app = createApp(App);
// 创建 axios 实例(可选:添加自定义配置)
const axiosInstance = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000,
});
// 挂载到全局属性
app.config.globalProperties.$axios = axiosInstance;
app.mount('#app');
步骤 3:在组件中使用(类型安全)
this
访问全局属性时,TypeScript 将自动识别类型:// 示例组件
export default {
methods: {
async fetchData() {
try {
const response = await this.$axios.get('/users');
console.log(response.data);
} catch (error) {
console.error('请求失败:', error);
}
},
},
};
扩展其他全局属性
$translate
),只需在声明文件中扩展接口:// src/types/vue.d.ts
declare module 'vue' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
$translate: (key: string) => string; // 新增全局方法
}
}
验证类型声明
this.$axios.
,IDE 会提示 get
、post
等 axios 方法。this.$axios.nonExistentMethod()
),TS 会报错。适配 Vue 2
Vue.prototype
扩展类型:// src/types/vue.d.ts
import Vue from 'vue';
import { AxiosInstance } from 'axios';
declare module 'vue/types/vue' {
interface Vue {
$axios: AxiosInstance;
}
}
如何优化Vue应用的性能?
一、代码层面的优化
1. 减少响应式依赖
使用 Object.freeze
避免 Vue 劫持大型静态数据。const staticData = Object.freeze({ list: [...] });
shallowRef
/ shallowReactive
:
仅对顶层属性进行响应式处理,减少深层监听开销。const shallowData = shallowReactive({ nested: { ... } }); // 仅监听顶层
2. 高效使用指令
v-if
vs v-show
:
频繁切换用 v-show
,运行时条件变化用 v-if
。v-for
优化:
始终提供唯一 key
,避免与 v-if
同用。
3. 计算属性和侦听器
用 computed
替代模板内复杂表达式,避免重复计算。
明确监听具体属性,减少 deep: true
的使用。watch(() => props.item.id, (newVal) => { ... });
4. 组件优化
动态加载非首屏组件,减少初始包体积。const HeavyComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'));
结合路由实现按需加载。{ path: '/detail', component: () => import('./Detail.vue') }
二、构建与打包优化
1. 代码分割
利用 Webpack/Vite 的代码分割能力。const module = await import('./module.js');
2. Tree Shaking
避免全量导入,如 Lodash 的 import { debounce } from 'lodash-es'
确保生产构建启用 Tree Shaking(Vite 默认开启)3. 压缩与优化
使用 vite-plugin-compression
生成 Gzip/Brotli 压缩文件
转 WebP 格式,使用 CDN 图片服务自动适配分辨率三、渲染性能优化
1. 虚拟滚动
使用 vue-virtual-scroller
或 @tanstack/vue-virtual
。2. 减少重渲染
v-once
静态内容:
标记永不更新的静态部分。shouldUpdateComponent
:
手动控制组件更新条件(Vue 3)。3. 优化 CSS
减少浏览器样式计算时间。
通过 contain: strict
隔离渲染区域。四、网络层优化
1. HTTP/2 与 CDN
利用多路复用降低延迟。
将图片、字体等上传至 CDN,提升全球访问速度。2. 预加载关键资源
:
提前加载首屏关键字体、图片。
Vue Router 自动预加载后续路由资源(默认开启)。3. 服务端优化
使用 Nuxt.js 实现服务端渲染或静态生成,提升首屏速度和 SEO。
配置 Nginx 缓存策略,减少服务器负载。五、监控与分析
1. 性能指标检测
通过 Google Lighthouse 测量 LCP、FID、CLS。
用 window.performance
API 监控关键操作耗时。2. Vue 专项工具
分析组件渲染时间,定位低效组件。
录制运行时性能,查找长任务和内存泄漏。六、进阶优化策略
1. Web Workers
将图像处理、大数据计算移至 Worker 线程。const worker = new Worker('./dataProcessor.js');
worker.postMessage(bigData);
如何处理Vue中的异步组件与TS类型?
1. 定义异步组件
defineAsyncComponent
创建异步组件,TypeScript会自动推断类型:import { defineAsyncComponent } from 'vue';
// 基础用法:自动推断类型
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
);
2. 显式指定组件类型
// 方式1:泛型(适用于明确组件类型)
const AsyncComponent = defineAsyncComponent<typeof AsyncComponentType>(
() => import('./AsyncComponent.vue')
);
// 方式2:类型断言
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue') as Promise<{ default: DefineComponent }>
);
3. 配置异步选项
import LoadingSpinner from './LoadingSpinner.vue';
import ErrorMessage from './ErrorMessage.vue';
const AsyncComponent = defineAsyncComponent({
loader: () => import('./AsyncComponent.vue'),
loadingComponent: LoadingSpinner, // 需为有效的组件类型
errorComponent: ErrorMessage,
delay: 200, // number类型
timeout: 3000 // number类型
});
4. 处理Props类型
子组件定义Props
// AsyncComponent.vue
import { defineComponent } from 'vue';
export default defineComponent({
props: {
message: {
type: String,
required: true
},
count: Number
}
});
父组件使用(类型安全)
// 父组件中
<template>
<AsyncComponent :message="greeting" :count="5" />
</template>
<script setup lang="ts">
const greeting = 'Hello, Vue!'; // 自动推断为string类型
</script>
5. 全局注册异步组件
注册组件
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.component(
'GlobalAsyncComponent',
defineAsyncComponent(() => import('./GlobalAsyncComponent.vue'))
);
扩展全局组件类型(可选)
// src/types/vue.d.ts
declare module 'vue' {
interface GlobalComponents {
GlobalAsyncComponent: typeof import('./GlobalAsyncComponent.vue')['default'];
}
}
6. 在组合式API中使用
ref
或 computed
处理异步组件引用:import { ref } from 'vue';
// 获取组件实例类型
const asyncComponentRef = ref<InstanceType<typeof AsyncComponent>>();
// 调用组件方法(假设组件暴露了refresh方法)
asyncComponentRef.value?.refresh();
7. 处理动态导入路径
// 动态路径示例(需确保模块存在)
const componentMap = {
home: defineAsyncComponent(() => import('./Home.vue')),
profile: defineAsyncComponent(() => import('./Profile.vue'))
};
type ComponentKey = keyof typeof componentMap;
function getComponent(key: ComponentKey) {
return componentMap[key]; // 类型安全
}
8. 类型错误排查
常见问题与解决
确保导入路径正确,必要时扩展 *.vue
模块类型:// src/types/shims-vue.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent;
export default component;
}
检查子组件Props定义,确保父组件传递类型一致。
确保 loadingComponent
、errorComponent
等选项为有效的组件类型。完整示例
// 父组件
<script setup lang="ts">
import { defineAsyncComponent } from 'vue';
// 定义异步组件(明确Props类型)
const AsyncUserList = defineAsyncComponent(() =>
import('./UserList.vue')
);
// 数据传递
const users = ref<User[]>([]);
</script>
<template>
<AsyncUserList :users="users" />
</template>
在TS中如何避免any类型滥用?
理解
any
的危害
any
会绕过 TypeScript 的类型检查,导致潜在运行时错误。替代
any
的解决方案优先使用
unknown
function parseData(data: unknown) {
if (typeof data === 'string') {
return JSON.parse(data); // data 在此分支被推断为 string
}
return data;
}
使用类型守卫(Type Guards)
function isUser(data: unknown): data is User {
return typeof data === 'object' && data !== null && 'name' in data;
}
if (isUser(input)) {
console.log(input.name); // input 被推断为 User
}
泛型(Generics)
function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>("text"); // result 类型为 string
工程化约束
(1) 启用严格类型检查
tsconfig.json
配置:{
"compilerOptions": {
"noImplicitAny": true, // 禁止隐式 any
"strict": true // 启用所有严格模式选项
}
}
(2) 代码规范工具
@typescript-eslint/no-explicit-any
限制显式 any
。{
"rules": {
"@typescript-eslint/no-explicit-any": "error"
}
}
如何实现一个带泛型的高阶组件(HOC)?
场景描述
loading
状态和 data
数据data
步骤 1:定义高阶组件类型
T
表示数据类型,P
表示原始组件的 Props 类型:// types/hoc.ts
import type { Component, ComponentPublicInstance } from 'vue';
// 高阶组件注入的额外 Props
interface InjectedProps<T> {
loading: boolean;
data: T | null;
}
// 高阶函数类型定义
export type HOC<T, P = {}> = (
WrappedComponent: Component<P & InjectedProps<T>>
) => Component<Omit<P, keyof InjectedProps<T>>>;
步骤 2:实现泛型高阶组件
withDataLoader
,接受数据加载逻辑并返回 HOC:// hoc/withDataLoader.ts
import { defineComponent, ref } from 'vue';
import type { HOC } from '@/types/hoc';
export function withDataLoader<T, P extends object>(
fetchData: () => Promise<T>
): HOC<T, P> {
return (WrappedComponent) => {
return defineComponent({
name: `WithDataLoader`,
props: WrappedComponent.props ?? {},
setup(props) {
const loading = ref(true);
const data = ref<T | null>(null);
fetchData()
.then((res) => (data.value = res))
.finally(() => (loading.value = false));
return () => (
<WrappedComponent
{...props}
loading={loading.value}
data={data.value}
/>
);
},
});
};
}
步骤 3:使用高阶组件
定义业务组件
应用高阶组件
类型安全验证
自动类型推断
fetchUsers
返回 Promise
,HOC 自动推断 T = User[]
EnhancedUserList
的 Props 类型为原始 UserList
的 Props 排除 loading
和 data
(由 HOC 自动注入)错误用法检测
高级用法:显式指定泛型类型
// 显式指定 User[] 类型
const EnhancedUserList = withDataLoader<User[]>(fetchUsers)(UserList);
实现原理分析
T
表示数据类型,通过 fetchData
的返回值类型自动推断,也可手动指定
使用 Omit
确保注入的 Props 不会与原始 Props 冲突
包装后的组件 Props 类型自动排除已注入的 loading
和 data
扩展:支持 Emits 和 Slots
// 扩展后的 HOC 类型
type AdvancedHOC<T, P = {}, E extends EmitsOptions = {}, S = {}> = (
WrappedComponent: Component<P & InjectedProps<T>, E, any, any, S>
) => Component<Omit<P, keyof InjectedProps<T>>, E, any, any, S>;
在大型Vue+TS项目中,如何组织代码结构?
核心架构原则
any
泛滥目录结构示例
src/
├── @types/ # 全局类型声明
│ ├── vite-env.d.ts # 环境变量类型
│ ├── vue-shims.d.ts # Vue单文件组件类型
│ └── api/ # API响应类型
├── assets/ # 静态资源
│ ├── styles/ # 全局样式体系
│ │ ├── _variables.scss # 设计系统变量
│ │ └── index.scss # 样式入口
│ └── images/ # 图片资源
├── core/ # 核心基础设施
│ ├── http/ # 请求层封装
│ │ ├── axios.ts # Axios实例配置
│ │ └── interceptors/ # 拦截器模块
│ ├── router/ # 路由系统
│ │ ├── guard/ # 路由守卫
│ │ └── index.ts # 路由配置入口
│ └── store/ # 状态管理(推荐Pinia)
├── modules/ # 业务功能模块 ★核心
│ ├── auth/ # 认证模块
│ │ ├── api/ # 模块专属API
│ │ ├── components/ # 模块内组件
│ │ ├── store/ # 模块状态管理
│ │ ├── types/ # 模块类型定义
│ │ └── views/ # 模块视图层
│ └── order/ # 订单模块(结构同上)
├── shared/ # 全局共享资源
│ ├── components/ # 全局通用组件
│ ├── composables/ # 组合式函数
│ ├── directives/ # 自定义指令
│ ├── layouts/ # 布局组件
│ └── utils/ # 工具函数库
├── App.vue # 应用根组件
└── main.ts # 应用入口
关键模块详解
1. 类型管理系统 (
@types/
)
// vue-shims.d.ts
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
// api/response.d.ts
declare namespace Api {
type Pagination<T> = {
data: T[]
total: number
}
}
2. 请求层封装 (
core/http/
)
// axios.ts
export const http = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 15000,
headers: { 'Content-Type': 'application/json' }
})
// interceptors/auth.ts
http.interceptors.request.use(config => {
const token = authStore.token
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
3. 业务模块化 (
modules/
)
modules/
└── product/
├── api/ # 产品相关API
├── components/ # 模块内复用组件
├── store/ # Pinia store
│ └── useProductStore.ts
├── types/ # 模块类型
│ └── product.d.ts
└── views/ # 页面级组件
└── ProductList.vue
// modules/product/api/product.ts
export const ProductService = {
getList: (params: ProductQuery) =>
http.get<Api.Pagination<Product>>('/products', { params }),
getDetail: (id: number) =>
http.get<Product>(`/products/${id}`)
}
4. 状态管理 (
core/store/
)
// modules/order/store/useOrderStore.ts
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [] as Order[],
loading: false
}),
actions: {
async fetchOrders() {
this.loading = true
try {
this.orders = await OrderService.getList()
} finally {
this.loading = false
}
}
}
})
5. 路由系统 (
core/router/
)
// router/index.ts
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/layouts/MainLayout.vue'),
children: [
// 动态导入模块路由
...productRoutes,
...orderRoutes
]
}
]
})
6. 样式架构 (
assets/styles/
)
// _variables.scss
$color-primary: #1890ff;
$breakpoint-md: 768px;
// index.scss
@use 'variables' as *;
@use 'mixins' as *;
开发规范建议
.d.ts
再实现逻辑
View
后缀命名 (ProductListView.vue
)Base
前缀命名 (BaseButton.vue
)README.md
工具链配置优化
// vite.config.ts
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'@modules': path.resolve(__dirname, 'src/modules')
}
}
.vscode/settings.json
性能优化策略
// router/product.ts
const ProductList = defineAsyncComponent(() =>
import('@modules/product/views/ProductList.vue')
)
vite.config.ts
的 build.rollupOptions
如何保证Vue组件在TS下的类型安全和可维护性?
一、类型安全核心策略
1. 严格的 Props 类型定义
// 使用泛型 defineProps(Vue 3.3+)
const props = defineProps<{
id: number
title: string
items: Array<{ label: string; value: number }>
disabled?: boolean
}>()
// 默认值处理(配合 withDefaults)
const props = withDefaults(defineProps<{
size?: 'small' | 'medium' | 'large'
}>(), {
size: 'medium'
})
2. 精确的 Emits 类型声明
// 声明事件类型和参数
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'submit', payload: FormData): Promise<boolean>
}>()
// 使用类型安全的触发方式
emit('submit', await validateForm()) // 自动校验参数类型
3. 组件实例类型导出
// 组件定义时导出实例类型
export type FormInstance = InstanceType<typeof FormComponent>
// 父组件中安全调用子组件方法
const formRef = ref<FormInstance>()
formRef.value?.validate()
二、可维护性架构设计
1. 模块化类型管理
src/
├── modules/
│ └── billing/
│ ├── components/ # 模块组件
│ ├── types/
│ │ ├── invoice.d.ts # 单据类型
│ │ └── api.d.ts # API 响应类型
│ └── stores/ # Pinia Store
2. 组合式函数类型规范
// composables/usePagination.ts
export interface PaginationOptions<T> {
fetch: (page: number) => Promise<T[]>
pageSize?: number
}
export function usePagination<T>(options: PaginationOptions<T>) {
const list = ref<T[]>([])
const currentPage = ref(1)
return {
list,
currentPage,
loadNext: () => { /* 类型安全的方法 */ }
}
}
3. 工具类型深度应用
// 复杂类型转换
type ApiResponse<T> = {
code: number
data: T
message: string
}
type User = {
id: number
name: string
}
// 提取 data 类型
type UserResponse = ApiResponse<User>['data'] // User
// 构造 API 函数类型
type ApiHandler<T> = (params: Record<string, any>) => Promise<ApiResponse<T>>
三、工程化保障体系
1. 类型检查工具链配置
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"types": ["vite/client", "unplugin-vue-components/client"]
}
}
// .eslintrc
{
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"vue/require-prop-types": "error"
}
}
2. 组件文档自动化
3. 类型测试策略
// tests/types/componentProps.test.ts
import type { PropType } from 'vue'
import MyComponent from '@/components/MyComponent.vue'
test('MyComponent props type safety', () => {
const propsType = MyComponent.props as Record<string, PropType<unknown>>
expect(propsType.id.required).toBe(true)
expect(propsType.id.type).toBe(Number)
expect(propsType.items.validator?.call(null, [{ label: 'test', value: 1 }]))
.toBe(true)
})
四、高级类型技巧
1. 递归类型处理复杂结构
type TreeNode<T> = {
value: T
children?: TreeNode<T>[]
}
const treeData: TreeNode<string> = {
value: 'root',
children: [
{ value: 'child1', children: [{ value: 'grandchild' }] }
]
}
2. 条件类型动态推导
type FormField<T> = T extends string
? { type: 'text'; maxLength?: number }
: T extends number
? { type: 'number'; min?: number; max?: number }
: never
type Config = FormField<string> // { type: 'text'; maxLength?: number }
3. 类型映射实现 DRY 原则
interface BaseEntity {
id: number
createdAt: Date
updatedAt: Date
}
type ReadonlyEntity<T> = {
readonly [P in keyof T]: T[P]
} & BaseEntity
type UserEntity = ReadonlyEntity<{
name: string
email: string
}>
五、性能与安全的平衡
// 安全断言函数
function safeCast<T>(value: unknown, guard: (v: unknown) => v is T): T {
if (guard(value)) return value
throw new Error('Type cast failed')
}
// 使用 Zod 进行运行时验证
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
name: z.string().min(3)
})
const safeUser = UserSchema.parse(apiResponse)
六、典型问题解决方案
1. 动态组件类型处理
const components: Record<string, Component> = {
home: defineAsyncComponent(() => import('./Home.vue')),
profile: defineAsyncComponent(() => import('./Profile.vue'))
}
const currentComponent = computed(() => components[route.name as keyof typeof components])
2. 高阶组件类型穿透
function withLogger<T extends ComponentOptions>(WrappedComponent: T) {
return defineComponent({
setup(_, { attrs, slots }) {
onMounted(() => console.log('Component mounted'))
return () => h(WrappedComponent, attrs, slots)
}
}) as T // 保持原始组件类型
}