前端【TS】06-typescript【基础】【搭建Vite+Vue3+TS项目】【为ref、reactive、computed、事件处理函数、模版引用、组件的props、组件的emits 标注类型】【类型声明文件】【自定义声明文件】
前置
基于Vite创建Vue3 + TS环境
vite官方文档:https://cn.vitejs.dev/guide/
vite除了支持基础阶段的纯TS环境之外,还支持 Vue + TS开发环境的快速创建, 命令如下:
1 npm create vite@latest vue-ts-project -- --template vue-ts 2 3 // 说明: 4 1. npm create vite@latest 基于最新版本的vite进行项目创建 5 2. vue-ts-pro 项目名称 6 3. -- --template vue-ts 选择Vue + TS的开发模板
和.vue文件TS环境相关的工具职责说明
开发阶段1. Volar工具对.vue文件进行实时的类型错误反馈
2. TypeScript Vue Plugin 工具用于支持在 TS 中 import *.vue 文件
打包阶段
vue-tsc工具负责打包时最终的类型检查
vue3中的标注类型
为ref标注类型
好处
为ref标注类型之后,既可以在给ref对象的value赋值时校验数据类型,同时在使用value的时候可以获得代码提示
如何标注类型
1 <script setup lang="ts"> 2 import { ref } from 'vue' 3 type ListItem = { 4 id: number 5 name: string 6 } 7 8 const list = ref<ListItem[]>([]) 9 list.value = [ 10 { 11 id: 1, 12 name: '张三' 13 } 14 ] 15 </script> 16 17 <template> 18 <ul> 19 <li v-for="item in list" :key="item.id">{{ item.name }}</li> 20 </ul> 21 </template> 22 23 <style scoped></style>
为reactive标注类型
场景和好处
为reactive标注类型之后,既可以在响应式对象在修改属性值的时候约束类型,也可以在使用时获得代码提示
如何标注类型
1 <script setup lang="ts"> 2 import { reactive } from 'vue' 3 4 // 1、自动推导, 根据默认值推导出来的类型 5 const form = reactive({ 6 username: '', 7 password: '' 8 }) 9 form.username = 'zhangsan' // 如果赋值的类型与推导的类型不一致,则会报错提示 10 11 // 2、显示注解变量的类型,推导不出来我们想要的类型 12 type Form = { 13 username: string 14 password: string 15 isAgree?: boolean 16 } 17 18 const form2: Form = reactive({ 19 username: '张三', 20 password: '123' 21 }) 22 </script> 23 24 <template> 25 <div> 26 <div>{{ form.username }}</div> 27 <div>{{ form2.username }}</div> 28 <div>{{ form2.password }}</div> 29 </div> 30 </template> 31 32 <style scoped></style>
为computed标注类型
计算属性通常由已知的响应式数据计算得到,所以依赖的数据类型一旦确定通过自动推导就可以知道计算属性的类型 另外根据最佳实践,计算属性多数情况下是只读的,不做修改,所以配合TS一般只做代码提示
需求:给ref函数标注类型,接收后端返回的对象列表,然后使用计算属性做过滤计算,计算得到单价大于500的商品
1 <script setup lang="ts"> 2 import { computed, ref } from 'vue' 3 4 // 1、ref标注类型 5 type Item = { 6 id: number 7 name: string 8 price: number 9 } 10 const list = ref<Item[]>([]) 11 12 // 2、基于List筛选出价格大于400的商品 13 const filterList = computed(() => list.value.filter(item => item.price > 500)) 14 </script> 15 16 <template> 17 <div> 18 <ul> 19 <li v-for="item in filterList" :key="item.id">{{ item.name }}</li> 20 </ul> 21 </div> 22 </template> 23 24 <style scoped></style>
为事件处理函数标注类型
原生dom事件处理函数的参数默认会自动标注为any类型,没有任何类型提示,为了获得良好的类型提示,需要手动标注类型
事件处理函数的类型标注主要做俩个事
1. 给事件对象形参 e 标注为Event类型,可以获得事件对象的相关类型提示
2. 如果需要更加精确的DOM类型提示可以使用断言(as)进行操作
1 <script setup lang="ts"> 2 import { computed, ref } from 'vue' 3 4 // 这么写会直接推导为any类型 5 // Parameter 'e' implicitly has an 'any' type 6 const inputChange = e => {} 7 8 // 1、给事件对象形参 e 标注为Event类型,可以获得事件对象的相关类型提示 9 const inputChange2 = (e: Event) => { 10 // 这里在点的时候就能提示所有事件对象的方法 11 console.log(e.target) 12 } 13 14 // 2. 如果需要更加精确的DOM类型提示可以使用断言(as)进行操作 15 const inputChange3 = (e: Event) => { 16 // 这里在点的时候就能提示所有事件对象的方法 17 // 但是e.target不知道具体是什么类型 18 // 那么如果这个target如果知道一定是input那么就断言类型为HTMLInputElement 19 // 这样就可以通过. 获取到input这个dom元素的所有方法 20 console.log((e.target as HTMLInputElement).value) 21 } 22 </script> 23 24 <template> 25 <div> 26 <input type="text" @change="inputChange" /> 27 </div> 28 </template> 29 30 <style scoped></style>
为模版引用标注类型
给模版引用标注类型,本质上是给ref对象的value属性添加了类型约束,约定value属性中存放的是特定类型的DOM对 象,从而在使用的时候获得相应的代码提示
1 <script setup lang="ts"> 2 import { onMounted, ref } from 'vue' 3 4 // 使用联合类型,标注其为HTMLInputElement或null 5 const inputRef = ref<HTMLInputElement | null>(null) 6 7 onMounted(() => { 8 // 当对象的属性可能是 null 或 undefined 的时候,称之为“空值”,尝试访问空值身上的属性或者方法会发生类型错误 9 inputRef.value.focus() 10 // 解决方式1:可选链 (使用?.) 表示当value不为空才调用focus方法 11 inputRef.value?.focus() 12 // 解决方式2:逻辑判断方案 13 if (inputRef.value) { 14 inputRef.value?.focus() 15 } 16 // 解决方式3:非空断言方案, 不推荐使用 17 // 非空断言(!)是指我们开发者明确知道当前的值一定不是null或者undefined,让TS通过类型校验 18 inputRef.value!.focus() 19 }) 20 </script> 21 22 <template> 23 <div> 24 <input type="text" ref="inputRef" /> 25 </div> 26 </template> 27 28 <style scoped></style>
为组件的props标注类型
为什么给props标注类型
1. 确保给组件传递的prop是类型安全的
2. 在组件内部使用props和为组件传递prop属性的时候会有良好的代码提示
语法:通过defineProps宏函数对组件props进行类型标注
父传子,子组件定义参数类型
需求:按钮组件有俩个prop参数,color类型为string且为必填,size类型为string且为可选,怎么定义类型?
说明:按钮组件传递prop属性的时候必须满足color是必传项且类型为string, size为可选属性,类型为string
父组件
1 <script setup lang="ts"> 2 import List from './components/List.vue' 3 </script> 4 5 <template> 6 <div> 7 <!-- 通过自定义属性给子组件传值 --> 8 <!-- --> 9 <List color="red" size="100"></List> 10 11 <!-- 如果传的类型不正确,就会提示报错 --> 12 <!-- Type 'number' is not assignable to type 'string' --> 13 <List :color="100" size="100"></List> 14 </div> 15 </template> 16 17 <style scoped></style>
子组件
1 <script setup lang="ts"> 2 // 接收父组件传值 3 type Props = { 4 color: string 5 size?: string 6 } 7 8 const props = defineProps<Props>() 9 </script> 10 <template> 11 <div>{{ props.color }}</div> 12 </template> 13 14 <style scoped></style>
props默认值设置
场景:Props中的可选参数通常除了指定类型之外还需要提供默认值,可以使用withDefaults宏函数来进行设置
需求:按钮组件的size属性的默认值设置为 middle
说明:如果用户传递了size属性,按照传递的数据来,如果没有传递,则size值为 ’middle’
1 <script setup lang="ts"> 2 // 接收父组件传值 3 type Props = { 4 color: string 5 size?: string // 可选参数 6 } 7 8 // 使用withDefaults宏函数给可选参数size设置默认值 9 // 如果父组件没有传size,那么size默认为middle 10 const props = withDefaults(defineProps<Props>(), { 11 size: 'middle' 12 }) 13 </script> 14 <template> 15 <div>{{ props.color }}</div> 16 </template> 17 18 <style scoped></style>
为组件的emits标注类型
子传父,子组件定义事件名称和参数类型
作用:可以约束事件名称并给出自动提示,确保不会拼写错误,同时约束传参类型,不会发生参数类型错误
语法:通过 defineEmits 宏函数进行类型标注
需求:子组件触发一个名称为 ’get-msg‘ 的事件,并且传递一个类型为string的参数
父组件
1 <script setup lang="ts"> 2 import List from './components/List.vue' 3 4 const getMessage = (msg: string) => { 5 console.log('接收到子组件传的msg: ', msg) 6 } 7 8 const getSize = (size: number) => { 9 console.log('接收到子组件传的msg: ', size) 10 } 11 </script> 12 13 <template> 14 <div> 15 <!-- 通过监听子组件定义的事件,拿到子组件传的值 --> 16 <List @get-msg="getMessage" @get-size="getSize"></List> 17 </div> 18 </template> 19 20 <style scoped></style>
子组件
1 <script setup lang="ts"> 2 type Emits = { 3 // 定义的第一个事件 4 (e: 'get-msg', msg: string): void 5 // 定义第二个事件 6 (e: 'get-size', size: number): void 7 } 8 // 1、定义事件类型emit 9 const emit = defineEmits<Emits>() 10 11 // 2、定义触发事件的函数,调用对应的事件 12 const clickHandle = () => { 13 // 触发事件1 14 emit('get-msg', '我是儿子') 15 // 触发事件2 16 emit('get-size', 200) 17 } 18 </script> 19 <template> 20 <button @click="clickHandle">点击传值给父组件</button> 21 </template> 22 23 <style scoped></style>
类型声明文件
概念:在TS中以d.ts为后缀的文件就是类型声明文件,主要作用是为js模块提供类型信息支持,从而获得类型提示
说明:
1. d.ts是如何生效的?
在使用js某些模块的时候,TS会自动导入模块对应的d.ts文件,以提供类型提示
2. d.ts是怎么来的?
库如果本身是使用TS编写的,在打包的时候经过配置自动生成对应的d.ts文件(axios本身就是TS编写的)
使用 DefinitelyTyped 提供类型声明文件
场景:有些库本身并不是采用TS编写的,无法直接生成配套的d.ts文件,但是也想获得类型提示,此时需要 Definitely Typed 提供类型声明文件
DefinitelyTyped是一个TS类型定义的仓库,专门为JS编写的库可以提供类型声明,比如可以安装 @types/jquery 为 jquery提供类型提示
TS内置类型声明文件
TS为JS运行时可用的所有标准化内置API都提供了声明文件,这些文件既不需要编译生成,也不需要三方提供
说明:这里的lib.es5.d.ts以及lib.dom.d.ts都是内置的类型声明文件,为原生js和浏览器API提供类型提示
自定义类型声明文件
d.ts文件在项目中是可以进行自定义创建的,通常有俩种作用,第一个是共享TS类型(重要),第二种是给js文件提供 类型(了解)
场景一:共享TS类型
相当于原本类型和变量写在一起,然后这里将类型全部提取到d.ts结尾的文件中,然后通过export导出,在使用到的文件中就导入并且导入的时候通过type来标识,然后使用类型
说明:哪个业务组件需要用到类型导入即可,为了区分普通模块,可以加上type关键词
场景二:给JS文件提供类型
当JS只书写逻辑时,其他地方用到这个JS文件,使用其中的方法或者变量的时候,是没有提示的,此时如果希望有提示,那么需要与原js文件同名,创建一个d.ts结尾的文件,然后将js中的类型定义到其中,然后使用declare关键字声明对应类型,然后导出此时其他地方在使用到这个js,导入之后就有完美的提示
说明:通过declare关键词可以为js文件中的变量声明对应类型,这样js导出的模块在使用的时候也会获得类型提示
.ts文件和d.ts文件对比
TS中有俩种文件类型:
一种是.ts文件,
一种是.d.ts文件
.ts文件
1. 既可以包含类型信息也可以写逻辑代码
2. 可以被编译为js文件
.d.ts文件
1. 只能包含类型信息不可以写逻辑代码
2. 不会被编译为js文件,仅做类型校验检查
综合案例
下载代码
1 git clone http://git.itcast.cn/heimaqianduan/vue3-ts.git // 代码中页面结构已经写好
List.vue
1 <script setup lang="ts"> 2 import axios from 'axios' 3 import { onMounted, ref } from 'vue' 4 5 import type { ArticleItem, ArticleResData } from '../types/data' 6 // 定义组件props类型 7 8 type Props = { 9 channelId: number 10 } 11 const props = defineProps<Props>() 12 13 14 15 // 1. 定义响应式列表数据 16 const list = ref<ArticleItem[]>() 17 18 // 2. axios.request获取数据 19 const getList = async () => { 20 const res = await axios.request<ArticleResData>({ 21 url: 'http://geek.itheima.net/v1_0/articles', 22 method: 'GET', 23 params: { 24 channel_id: props.channelId, 25 timestamp: Date.now() 26 } 27 }) 28 list.value = res.data.data.results 29 } 30 onMounted(() => getList()) 31 32 </script> 33 34 <template> 35 <div class="list-box"> 36 <!-- 列表 --> 37 <van-cell v-for="item in list" :key="item.art_id"> 38 <!-- 标题区域的插槽 --> 39 <template #title> 40 <!-- 无图模式 --> 41 <div class="title-box" v-if="item.cover.type === 0"> 42 <!-- 标题 --> 43 <span> {{ item.title }} </span> 44 </div> 45 46 <!-- 单图模式 --> 47 <div class="title-box" v-if="item.cover.type === 1"> 48 <span>{{ item.title }}</span> 49 <img class="thumb" :src="item.cover.images[0]" /> 50 </div> 51 52 <!-- 三图模式 --> 53 <div class="thumb-box" v-if="item.cover.type === 3"> 54 <img class="thumb" v-for="img in item.cover.images" :src="img" :key="img" /> 55 </div> 56 </template> 57 <!-- label 区域的插槽 --> 58 <template #label> 59 <div class="label-box"> 60 <div> 61 <span>{{ item.aut_name }}</span> 62 <span>{{ item.comm_count }}评论</span> 63 <span>{{ item.pubdate }}</span> 64 </div> 65 </div> 66 </template> 67 </van-cell> 68 69 </div> 70 </template> 71 72 73 <style scoped lang="less"> 74 .list-box { 75 position: fixed; 76 top: 50px; 77 bottom: 0; 78 width: 100%; 79 overflow-y: auto; 80 } 81 82 /* 标题样式 */ 83 .title-box { 84 display: flex; 85 justify-content: space-between; 86 align-items: flex-start; 87 } 88 89 /* label描述样式 */ 90 .label-box { 91 display: flex; 92 justify-content: space-between; 93 align-items: center; 94 } 95 96 /* 文章信息span */ 97 .label-box span { 98 margin: 0 3px; 99 100 &:first-child { 101 margin-left: 0; 102 } 103 } 104 105 /* 图片的样式, 矩形黄金比例:0.618 */ 106 .thumb { 107 width: 113px; 108 height: 70px; 109 background-color: #f8f8f8; 110 object-fit: cover; 111 } 112 113 /* 三图, 图片容器 */ 114 .thumb-box { 115 display: flex; 116 justify-content: space-between; 117 } 118 </style>
类型定义
1 // 频道相关类型 2 // 泛型定义 3 4 type ResType<T> = { 5 message: string 6 data: T 7 } 8 9 export type ChannelItem = { 10 id: number 11 name: string 12 } 13 14 // export type ChannelRes = { 15 // data: { 16 // channels: ChannelItem[] 17 // } 18 // message: string 19 // } 20 export type ChannelRes = ResType<{ 21 channels: ChannelItem[] 22 }> 23 24 // 文章列表相关类型 25 26 // 文章对象类型 27 export type ArticleItem = { 28 art_id: string 29 aut_id: string 30 aut_name: string 31 comm_count: number 32 cover: { 33 type: number 34 images: string[] 35 } 36 is_top: number 37 pubdate: string 38 title: string 39 } 40 41 // 文章接口响应数据类型 42 // export type ArticleResData = { 43 // data: { 44 // pre_timestamp: string 45 // results: ArticleItem[] 46 // } 47 // message: string 48 // } 49 50 export type ArticleResData = ResType<{ 51 pre_timestamp: string 52 results: ArticleItem[] 53 }>
App.vue
1 <script setup lang="ts"> 2 import { onMounted, ref } from 'vue' 3 import List from './components/List.vue' 4 import axios from 'axios' 5 6 import type { ChannelItem, ChannelRes } from './types/data' 7 8 // 核心实现步骤 9 10 // 1. 声明响应式列表数据 (ref + TS) 11 const channelList = ref<ChannelItem[]>([]) 12 13 // 2. axios获取后端数据 (axios.request<类型>) 14 const getList = async () => { 15 const res = await axios.request<ChannelRes>({ 16 url: 'http://geek.itheima.net/v1_0/channels', 17 method: 'GET' 18 }) 19 // 3. 后端数据赋值给响应式列表 (类型自然匹配) 20 channelList.value = res.data.data.channels 21 } 22 23 onMounted(() => getList()) 24 25 // 4. 响应式列表渲染到模板 (类型提示) 26 27 // 频道id 28 const channelId = ref(0) 29 const tabChange = (id: number) => { 30 console.log(id) 31 channelId.value = id 32 } 33 </script> 34 35 <template> 36 <!-- tab切换 --> 37 <van-tabs @change="tabChange"> 38 <van-tab v-for="item in channelList" :key="item.id" :title="item.name"> 39 <!-- 文章列表 --> 40 <List :channel-id="channelId" /> 41 </van-tab> 42 </van-tabs> 43 </template>