前置
基於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>