首页
直播
壁纸
友链
搜索
1
微信小程序 Skyline 渲染引擎实战:worklet 动画从原理到落地
327 阅读
2
微信小程序支付全链路实战:JSAPI 下单、调起支付、回调验签与退款
308 阅读
3
微信小程序云开发实战:云函数、云数据库与云存储的正确使用姿势
280 阅读
4
小程序接口安全攻防:签名、防重放、防刷与数据加密实战
276 阅读
5
微信小程序分包进阶:独立分包、预下载与分包异步化实战
271 阅读
服务器运维
后端技术
前端技术
梯子
数据库
小程序
登录
搜索
标签搜索
fastadmin
Redis
微信小程序
前端开发
RabbitMQ
Go
Golang
服务器
codex
buildadmin
小程序
mysql
Nginx
Docker
Vue3
Node.js
MySQL优化
Linux
TypeScript
JWT
沿途的风景
累计撰写
82
篇文章
累计收到
0
条评论
首页
栏目
服务器运维
后端技术
前端技术
梯子
数据库
小程序
页面
直播
壁纸
友链
搜索到
2
篇与
» TypeScript
的结果
2026-07-11
Vue 3 + TypeScript 类型化开发实战:从 Props 到泛型组件
Vue 3 是用 TS 重写的,类型支持是其核心卖点。但很多项目只是"用了 TS",模板里的类型断言满天飞、组件 Props 无类型、第三方库全是 any。本文覆盖 Vue 3 + TS 的核心类型工具,从基础用法一路讲到泛型组件,帮你把类型系统真正用起来。一、Props 类型化的三个层次层次一:运行时校验(纯 JS 风格)defineProps({ id: [Number, String], items: { type: Array, required: true } })有运行时警告,但没有编辑器类型推导,TS 项目不推荐。层次二:类型声明(类型推导最佳)<script setup lang="ts"> interface Order { id: number title: string status: 'pending' | 'paid' | 'closed' } const props = defineProps<{ order: Order showActions?: boolean }>() // props.order.status 自动推导为联合类型 // 模板中尝试比较错误状态会有类型提示 </script>层次三:withDefaults 提供默认值类型声明语法不支持直接给默认值,需要 withDefaults:const props = withDefaults( defineProps<{ items: Order[] pageSize?: number labels?: Record<string, string> }>(), { pageSize: 20, labels: () => ({}) // 对象/数组默认值必须用工厂函数 } )从接口自动生成 Props(进阶)interface Props { userId: number; compact?: boolean } // 响应式解包后仍是响应式的 const props = defineProps<Props>()props 是被 reactive 包装的对象,解构会丢失响应性。需要解构时用 Vue 3.5+ 的响应式 Props 解构:const { userId, compact = false } = defineProps<Props>() // 3.5+ 编译器自动保持响应性二、emit 与 ref 的类型化emit 类型const emit = defineEmits<{ (e: 'change', value: string): void (e: 'select', id: number, item: Order): void }>() // Vue 3.3+ 更简洁的具名元组语法 const emit2 = defineEmits<{ change: [value: string] select: [id: number, item: Order] }>()父组件在模板上监听时,回调参数类型自动校验。ref 的类型// 基础:自动推导 const count = ref(0) // Ref<number> // 初始值为 null 时必须显式标注 const el = ref<HTMLInputElement | null>(null) onMounted(() => el.value?.focus()) // 复杂对象推荐接口先行 interface FormData { name: string tags: string[] } const form = ref<FormData>({ name: '', tags: [] })模板引用组件实例import FormModal from './FormModal.vue' const modalRef = ref<InstanceType<typeof FormModal> | null>(null) onMounted(() => { modalRef.value?.open() // expose 的方法有完整类型 })InstanceType<typeof Component> 会读取 defineExpose 暴露的成员类型,这是父组件调用子组件方法的类型安全姿势。三、computed 与 watch 的类型const orderList = ref<Order[]>([]) const pendingCount = computed(() => orderList.value.filter(o => o.status === 'pending').length ) // 自动推导 ComputedRef<number> // watch 回调参数类型自动对应数据源 watch( () => props.order.status, (newStatus, oldStatus) => { // newStatus: 'pending' | 'paid' | 'closed' console.log(`${oldStatus} → ${newStatus}`) } ) // watchEffect 不需要指定类型,内部自动收集依赖四、provide / inject 的类型安全// symbols/keys.ts —— 集中管理 InjectionKey import type { InjectionKey, Ref } from 'vue' export interface UserContext { user: Ref<{ id: number; name: string }> refresh: () => Promise<void> } export const UserKey: InjectionKey<UserContext> = Symbol('user')// 祖先组件 provide(UserKey, { user, refresh }) // 后代组件:完整类型推导 + 缺省兜底 const ctx = inject(UserKey) if (!ctx) throw new Error('UserKey 未在祖先组件提供') ctx.user.value.name // string,自动推导五、泛型组件:类型跟着数据走普通组件的 Props 类型是固定的,泛型组件让类型由调用方决定。最典型的场景是列表组件和选择器组件。普通写法的困境// ❌ items 只能声明成 any[],丢失元素类型 defineProps<{ items: any[]; modelValue: any }>()泛型组件写法<!-- SelectList.vue --> <script setup lang="ts" generic="T extends { id: number }"> defineProps<{ items: T[] modelValue: T['id'] | null labelField?: keyof T }>() const emit = defineEmits<{ 'update:modelValue': [id: T['id'] | null] select: [item: T] }>() </script> <template> <ul class="select-list"> <li v-for="item in items" :key="item.id" :class="{ active: item.id === modelValue }" @click="emit('update:modelValue', item.id); emit('select', item)" > <slot :item="item">{{ item[labelField ?? 'id'] }}</slot> </li> </ul> </template>generic="T extends { id: number }" 声明泛型参数,调用方传 Order[] 时所有相关类型自动实例化为 Order:<SelectList v-model="selectedOrderId" :items="orders" label-field="title" @select="(order) => console.log(order.status)" // order: Order,类型完整 />泛型 composable:类型化的 useListexport function useList<T>(initial: T[] = []) { const list = ref<T[]>([...initial]) function add(item: T) { list.value.push(item) } function remove(predicate: (item: T) => boolean) { list.value = list.value.filter(i => !predicate(i)) } function find(predicate: (item: T) => boolean): T | undefined { return list.value.find(predicate) } return { list, add, remove, find } } // 使用:所有方法参数和返回值都有精确类型 const { list, add, find } = useList<Order>([]) add({ id: 1, title: 'x', status: 'pending' }) // OK add({ id: 2, title: 'y' }) // 报错:缺 status六、SFC 与 TS 工程细节defineComponent 与组件类型<script setup> 的组件是匿名的,需要显式 name 时(keep-alive include、devtools 显示):<script setup lang="ts"> defineOptions({ name: 'OrderList' }) </script>外部类型文件的组织src/ ├── types/ │ ├── api.d.ts # 后端接口类型(可由 OpenAPI 生成) │ ├── models.ts # 业务模型 Order、User 等 │ └── global.d.ts # 全局类型扩展 ├── components/api.d.ts 建议用工具从后端 Swagger/OpenAPI 规范生成,杜绝手抄接口字段。常用工具类型速查import type { Ref, ComputedRef, MaybeRef, UnwrapRef } from 'vue' // MaybeRef<T>:参数既可以是 T 也可以是 Ref<T> function useX(source: MaybeRef<string>) { /* ... */ } // UnwrapRef<T>:ref 的解包类型 const state = ref({ nested: { count: 0 } }) // state.value.nested.count 类型是 number(自动解包) // ExtractPropTypes:从运行时 props 选项提取类型 import type { ExtractPropTypes } from 'vue' const propsSchema = { title: String, count: { type: Number, default: 0 } } type Props = ExtractPropTypes<typeof propsSchema>vue-tsc 做模板类型检查vue-tsc 能检查模板中的表达式类型,接入 CI:// package.json { "scripts": { "type-check": "vue-tsc --noEmit" } }模板里 {{ order.statu }} 这类错误会在构建前暴露,而不是上线后白屏。七、别过度类型化类型是工具不是目的,几个克制的建议:联合类型优先于枚举:'pending' | 'paid' 比 enum 更利于 tree-shaking后端接口类型交给代码生成,手写注定跟不上变化第三方库无类型时的最小兜底:declare module 'xxx',而不是到处 any断言 as 只用于"我确信类型系统不知道的事",当作逃生舱而非常规操作总结Props 用类型声明语法,默认值交给 withDefaultsdefineEmits 用具名元组语法,参数类型双向校验InstanceType<typeof Comp> 是引用子组件的规范类型InjectionKey<T> 让 provide/inject 摆脱字符串裸奔泛型组件 generic="T" 是封装列表/选择器类组件的杀手锏vue-tsc 进 CI,模板类型错误提前拦截类型系统用到位后,重构敢下手、接口变更立刻报错、新人看类型就能懂用法——这 defensive 能力正是中级向高级进阶的分水岭。
2026年07月11日
22 阅读
0 评论
0 点赞
2026-03-05
Vue3 Composition API 深度解析:从 ref 到自定义 Hook
前言Vue3 的 Composition API 是对 Options API 的范式升级。它解决了大型组件中逻辑复用和组织的问题,让代码更灵活、更可维护。一、响应式基础1.1 ref vs reactiveimport { ref, reactive, toRefs } from "vue"; // ref:适用于基本类型 const count = ref(0); count.value++; // reactive:适用于对象 const state = reactive({ name: "张三", age: 25, }); // reactive 的陷阱:解构会失去响应性 const { name } = state; // name 不再是响应式 // 解决方案:toRefs const { name: reactiveName } = toRefs(state);1.2 computedimport { ref, computed } from "vue"; const firstName = ref("张"); const lastName = ref("三"); // 只读计算属性 const fullName = computed(() => `${firstName.value}${lastName.value}`); // 可写计算属性 const fullNameWritable = computed({ get() { return `${firstName.value}${lastName.value}`; }, set(newValue: string) { firstName.value = newValue[0]; lastName.value = newValue.slice(1); }, });二、watch 和 watchEffect2.1 watchimport { ref, watch } from "vue"; const keyword = ref(""); // 监听单个 ref watch(keyword, (newVal, oldVal) => { console.log(`关键词从 ${oldVal} 变为 ${newVal}`); }); // 监听多个源 watch([keyword, category], ([newKeyword, newCategory]) => { search(newKeyword, newCategory); }); // 深度监听对象 watch( () => state, (newState) => { saveToStorage(newState); }, { deep: true } );2.2 watchEffectimport { ref, watchEffect } from "vue"; const userId = ref(1); // 自动追踪依赖,立即执行 watchEffect(() => { fetchUser(userId.value); // userId 变化时自动重新执行 });三、自定义 Hook(组合式函数)3.1 useMousePosition// composables/useMousePosition.ts import { ref, onMounted, onUnmounted } from "vue"; export function useMousePosition() { const x = ref(0); const y = ref(0); function update(e: MouseEvent) { x.value = e.pageX; y.value = e.pageY; } onMounted(() => { window.addEventListener("mousemove", update); }); onUnmounted(() => { window.removeEventListener("mousemove", update); }); return { x, y }; }使用:<script setup lang="ts"> import { useMousePosition } from "@/composables/useMousePosition"; const { x, y } = useMousePosition(); </script> <template> <p>鼠标位置: {{ x }}, {{ y }}</p> </template>3.2 useFetch// composables/useFetch.ts import { ref, watchEffect } from "vue"; export function useFetch<T>(url: () => string) { const data = ref<T | null>(null); const error = ref<string | null>(null); const loading = ref(false); watchEffect(async () => { loading.value = true; error.value = null; try { const res = await fetch(url()); if (!res.ok) throw new Error(`HTTP ${res.status}`); data.value = await res.json() as T; } catch (e) { error.value = (e as Error).message; } finally { loading.value = false; } }); return { data, error, loading }; }四、生命周期钩子import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from "vue"; onBeforeMount(() => { /* 组件挂载前 */ }); onMounted(() => { /* DOM 已挂载 */ }); onBeforeUpdate(() => { /* 响应式数据变更,DOM 更新前 */ }); onUpdated(() => { /* DOM 更新后 */ }); onBeforeUnmount(() => { /* 组件卸载前 */ }); onUnmounted(() => { /* 组件已卸载 */ });五、总结Composition API 的核心优势在于逻辑复用和代码组织。通过自定义 Hook,可以将组件逻辑提取为可复用的函数,告别 Options API 时代 mixin 的命名冲突和数据来源不透明问题。建议在项目中优先使用 <script setup> 语法,配合 TypeScript 获得最佳开发体验。
2026年03月05日
29 阅读
0 评论
0 点赞
0:00