Vue3,Pinia与TypeScript面试题总结

在Pinia中如何定义带有类型推断的store?

使用 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的区别)

Vue的响应式原理是其数据驱动视图的核心机制,Vue 2和Vue 3在实现方式上有显著差异,主要体现在底层依赖的API和对数据变化的检测能力上:


Vue 2 的响应式原理

实现方式:
Vue 2 使用 Object.defineProperty 对对象的属性进行劫持,将其转化为 gettersetter

  1. 依赖收集(Getter):
    当组件渲染时访问数据属性,触发 getter,将当前的 Watcher(依赖)收集到依赖列表中(Dep)。

  2. 触发更新(Setter):
    当数据被修改时,触发 setter,通知所有依赖的 Watcher 更新视图。

局限性:

  • 无法检测对象属性的新增或删除
    必须通过 Vue.setVue.delete 显式操作才能触发响应式更新。
  • 对数组的监听受限
    直接通过索引修改数组(如 arr[0] = 1)或修改数组长度(如 arr.length = 0)无法触发更新。
    Vue 2 通过重写数组的 pushpopsplice 等变异方法(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 的响应式原理

实现方式:
Vue 3 改用 Proxy 代理整个对象,结合 Reflect 实现更全面的拦截能力。

  1. Proxy 的拦截操作
    Proxy 可以拦截对象的 getsetdeleteProperty 等操作,无需逐个劫持属性。

  2. 深层响应式
    通过递归代理嵌套对象,实现深层次的响应式跟踪。

优势:

  • 支持动态新增/删除属性
    直接操作 obj.newKey = valuedelete obj.key 也能触发响应式。
  • 原生支持数组和复杂数据结构
    无需重写数组方法,直接通过索引修改或调用 arr.length 即可触发更新。
  • 性能优化
    Proxy 仅在访问属性时递归代理,避免初始化时深度遍历所有属性。

示例:

// 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 的区别是什么?

核心区别

特性 v-if v-show
实现方式 动态添加/移除DOM元素 通过CSS display切换显示状态
初始渲染 条件为false不渲染元素 无论条件如何,始终渲染元素
切换性能 较高(涉及DOM操作) 较低(仅修改CSS属性)
适用场景 条件不频繁切换且可能为false 条件频繁切换
生命周期 触发组件的创建/销毁生命周期钩子 不触发生命周期,仅切换显示
v-else配合 支持逻辑分支(v-else/v-else-if 不支持
性能影响
  • v-if

    • 初始渲染开销低:若初始条件为false,无需渲染元素。
    • 切换开销高:频繁切换时,频繁的DOM操作会降低性能。
  • v-show

    • 初始渲染开销高:无论条件如何,元素都会被渲染。
    • 切换开销低:仅修改CSS属性,适合高频切换场景(如选项卡)。
生命周期行为
  • v-if
    条件首次为true时触发组件的createdmounted钩子;条件变为false时触发unmounted钩子。

  • v-show
    无论条件如何变化,组件的生命周期钩子不会触发,元素始终存在。


Vue组件的生命周期钩子有哪些?哪个阶段适合发起数据请求?

Vue 生命周期钩子(以 Vue 2 为主,兼容 Vue 3)

1. 创建阶段(Initialization)
  • beforeCreate

    • 触发时机:实例初始化后,数据观测(data)和事件配置(methods)之前。
    • 用途:通常用于插件初始化(如 Vuex action 订阅),但此时无法访问组件数据。
  • created

    • 触发时机:实例创建完成,数据观测、计算属性、方法已配置,但 DOM 未生成。
    • 用途适合发起异步请求(如 API 调用)、初始化非 DOM 相关数据。
2. 挂载阶段(Mounting)
  • beforeMount

    • 触发时机:模板编译完成,但尚未将虚拟 DOM 渲染为真实 DOM。
    • 用途:极少使用,适用于需要在挂载前修改模板的场景。
  • mounted

    • 触发时机:DOM 已挂载,可以访问 this.$el
    • 用途适合操作 DOM(如初始化图表库)、依赖 DOM 的第三方库初始化。
3. 更新阶段(Updating)
  • beforeUpdate

    • 触发时机:数据变化后,虚拟 DOM 重新渲染前。
    • 用途:获取更新前的 DOM 状态(如滚动位置)。
  • updated

    • 触发时机:虚拟 DOM 重新渲染并应用到真实 DOM 后。
    • 用途:执行依赖最新 DOM 的操作(如调整元素尺寸),但避免在此处修改数据(可能导致无限循环)。
4. 销毁阶段
  • beforeDestroy(Vue 2)/ beforeUnmount(Vue 3)

    • 触发时机:实例销毁前,组件仍完全可用。
    • 用途清理定时器、取消事件监听、释放外部资源(如 WebSocket 连接)。
  • destroyed(Vue 2)/ unmounted(Vue 3)

    • 触发时机:实例销毁后,所有子组件也已被销毁。
    • 用途:极少使用,适用于最终清理逻辑。
5. 缓存组件生命周期(Keep-Alive)
  • activated

    • 触发时机:被 缓存的组件重新激活时(如切换回该组件)。
    • 用途:恢复组件状态(如重新请求数据)。
  • deactivated

    • 触发时机:被 缓存的组件停用时(如切换到其他组件)。
    • 用途:保存组件状态(如表单输入内容)。

适合发起数据请求的阶段

1. created 阶段
  • 推荐场景
    • 需要尽早获取数据(减少用户等待时间)。
    • 不依赖 DOM 的操作(如纯数据初始化)。
  • 示例
    created() {
      this.fetchUserData(); // 初始化用户数据
    }
    
2. mounted 阶段
  • 推荐场景
    • 需要操作 DOM 或依赖 DOM 的库(如地图、图表初始化)。
    • 需要获取元素尺寸或位置。
  • 示例
    mounted() {
      this.initChart(); // 依赖 DOM 的图表库初始化
    }
    
3. activated 阶段(配合
  • 推荐场景
    • 缓存组件需要动态更新数据(如切换回页面时刷新列表)。
  • 示例
    activated() {
      this.refreshData(); // 重新请求最新数据
    }
    

组件间通信方式有哪些?

  • 注意:Vue 3 中需改用第三方库(如 mitt)。

** 复杂场景通信**

方式:状态管理库(Vuex/Pinia)
  • Vuex(Vue 2 官方方案):

    • 集中式状态管理,通过 statemutationsactions 管理数据流。
    • 示例
      // 组件中获取状态
      this.$store.state.count;
      
      // 触发 Action
      this.$store.dispatch('fetchData');
      
  • Pinia(Vue 3 推荐方案):

    • 更简洁的 API,支持 TypeScript。
    • 示例
      // 定义 Store
      export const useStore = defineStore('main', {
        state: () => ({ count: 0 }),
        actions: { increment() { this.count++ } }
      });
      
      // 组件中使用
      const store = useStore();
      store.count++;
      

** 直接访问组件实例**

方式:$parent / $children
  • 用法:通过组件实例链直接访问父子组件。
  • 缺点:增加耦合,不利于维护,慎用。
方式:$refs
  • 用法:父组件通过 ref 属性获取子组件实例。
  • 示例
    
    
    
    

** 其他方式**

方式:浏览器存储(LocalStorage/SessionStorage)
  • 通过浏览器存储共享数据,需手动监听 storage 事件。
  • 适用场景:持久化数据(如用户登录状态)。
方式:作用域插槽(Scoped Slots)
  • 父组件通过插槽向子组件传递模板,子组件通过插槽prop暴露数据。
  • 示例
    
    
    
    
    
      
    
    

如何选择通信方式?

场景 推荐方式
父子组件简单通信 Props / $emit
兄弟组件通信 事件总线 / 状态管理库
跨层级组件共享数据 Provide/Inject
复杂应用状态管理 Vuex(Vue 2) / Pinia(Vue 3)
需要直接操作子组件 $refs
非响应式数据或简单全局配置 事件总线 / 浏览器存储

interface 和 type 的区别是什么?

核心区别

特性 interface type(类型别名)
声明合并 ✅ 支持(同名接口自动合并) ❌ 不支持
扩展方式 通过 extends 继承 通过 &(交叉类型)组合
适用类型范围 主要用于对象类型 支持更广泛的类型(联合、元组、字面量等)
实现(implements) 类可以直接实现接口 类不能直接实现类型别名(除非是对象类型)
工具类型兼容性 完全支持(如 Partial 完全支持(如 Partial
声明合并(Declaration Merging)
  • 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) { ... }

TS中的泛型是什么?举例说明在函数中的应用。

在函数中使用泛型的例子

假设我们想要编写一个函数,该函数返回传入的参数本身。如果我们不使用泛型,我们可能需要为每种类型都写一个这样的函数,或者让函数接受并返回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)和非空断言(!)的使用场景及风险

** 类型断言(as)**

使用场景
  1. 明确知道值的实际类型
    当开发者比编译器更清楚值的类型时,强制指定类型。

    // 从外部数据源获取的值
    const data = JSON.parse('{ "name": "Alice" }') as { name: string };
    
  2. 处理联合类型的窄化
    在无法通过类型守卫(typeof/instanceof)缩小类型范围时,手动断言。

    const element = document.getElementById("input") as HTMLInputElement;
    
  3. 兼容旧代码或第三方库
    处理类型定义不完整的第三方库返回值。

    const value = (window as any).externalValue as string;
    
风险
  • 掩盖类型错误
    若断言类型与实际类型不符,编译器不会报错,但运行时可能崩溃。
    const num = "123" as number; // ❌ 错误断言,但编译通过
    console.log(num.toFixed(2)); // 运行时报错
    
替代方案
  • 类型守卫(Type Guards)
    通过逻辑判断缩小类型范围。
    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");
    }
    

** 非空断言(!)**

使用场景
  1. 明确变量不为 null/undefined
    当开发者确定某个可能为空的变量在特定上下文中一定有值。

    // React Ref 访问
    const inputRef = useRef<HTMLInputElement>(null);
    inputRef.current!.focus(); // 确认组件已挂载
    
  2. 访问可选属性或方法
    强制跳过编译器的空值检查。

    type User = { name?: string };
    const user: User = { name: "Alice" };
    console.log(user.name!.toUpperCase()); // 确保 name 存在
    
  3. 初始化延迟赋值的变量
    结合 ! 声明变量,稍后赋值。

    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 + 类型声明文件 优先完善类型定义文件

TS的“类型守卫”是什么?如何实现?

类型守卫的作用

  • 缩小类型范围:将联合类型或宽泛类型的具体类型确定下来。
  • 避免类型断言:减少使用 as! 等可能引发运行时错误的手段。
  • 增强代码可读性:通过显式类型检查逻辑提高代码可维护性。

类型守卫的实现方式

typeof 类型守卫

适用类型:基本类型(stringnumberbooleansymbolundefinedobjectfunction)。
示例

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
  }
}

类型守卫的高级用法

联合类型的全面检查(Exhaustiveness Check)

使用 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
  }
}

如何在Vue组件中为props定义类型?

1. Vue 3 + Composition API(推荐)

使用 defineProps 和泛型

直接在

应用高阶组件

在父组件中使用 HOC 包装业务组件:




类型安全验证

自动类型推断
  1. fetchUsers 返回 Promise,HOC 自动推断 T = User[]
  2. EnhancedUserList 的 Props 类型为原始 UserList 的 Props 排除 loadingdata(由 HOC 自动注入)
错误用法检测

如果错误传递 Props,TypeScript 会报错:





高级用法:显式指定泛型类型

当需要手动指定数据类型时,可以显式传递泛型参数:

// 显式指定 User[] 类型
const EnhancedUserList = withDataLoader<User[]>(fetchUsers)(UserList);

实现原理分析

  1. 泛型传递
    T 表示数据类型,通过 fetchData 的返回值类型自动推断,也可手动指定
  2. Props 合并
    使用 Omit> 确保注入的 Props 不会与原始 Props 冲突
  3. 类型安全
    包装后的组件 Props 类型自动排除已注入的 loadingdata

扩展:支持 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项目中,如何组织代码结构?

核心架构原则

  1. 功能模块化 - 按业务领域划分独立模块
  2. 分层解耦 - 分离视图、逻辑、数据层
  3. 类型驱动 - 全链路类型安全,拒绝 any 泛滥
  4. 工具统一 - 规范化请求库、状态管理、工具函数
  5. 可测性设计 - 内置测试友好结构

目录结构示例

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、第三方库、环境变量类型
    // 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 实例化:统一配置 baseURL、超时时间
    // 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
    
  • API 服务化:模块 API 按功能聚合
    // 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/)
  • Pinia 模块化:每个业务模块维护自己的 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
          }
        }
      }
    })
    
  • 类型安全存储:Store 定义与模块类型严格对应
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 *;
    
  • CSS 模块化:组件样式使用

开发规范建议

  1. 类型优先原则:先定义 .d.ts 再实现逻辑
  2. 模块通信:跨模块交互通过 Store 或 Event Bus
  3. 组件规范
    • 视图组件:以 View 后缀命名 (ProductListView.vue)
    • 通用组件:以 Base 前缀命名 (BaseButton.vue)
  4. 提交规范:遵循 Conventional Commits
  5. 文档驱动:模块根目录维护 README.md

工具链配置优化

  1. 路径别名:简化模块导入
    // vite.config.ts
    resolve: {
      alias: {
        '@': path.resolve(__dirname, 'src'),
        '@modules': path.resolve(__dirname, 'src/modules')
      }
    }
    
  2. IDE 配置:统一 VSCode 的 .vscode/settings.json
  3. Husky + Lint-Staged:提交前自动化校验

性能优化策略

  1. 模块懒加载:动态导入非核心模块
    // router/product.ts
    const ProductList = defineAsyncComponent(() =>
      import('@modules/product/views/ProductList.vue')
    )
    
  2. 构建分块:配置 vite.config.tsbuild.rollupOptions
  3. 类型检查加速:启用 Volar Takeover 模式

如何保证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
}>

五、性能与安全的平衡

  1. 类型断言边界控制
    // 安全断言函数
    function safeCast<T>(value: unknown, guard: (v: unknown) => v is T): T {
      if (guard(value)) return value
      throw new Error('Type cast failed')
    }
    
  2. 轻量级运行时类型校验
    // 使用 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 // 保持原始组件类型
}

你可能感兴趣的:(typescript,vue.js,javascript)