【Vuejs】806- TypeScript Vue 3 上手教程

作者:TinssonTai
https://juejin.im/post/6875713523968802829
預(yù)估閱讀時(shí)間:10 分鐘
Vue3 TS 項(xiàng)目,支持 .vue 和 .tsx 寫法
項(xiàng)目地址:https://github.com/vincentzyc/vue3-demo.git
TypeScript?是JS的一個(gè)超集,主要提供了類型系統(tǒng)和對(duì)ES6的支持,使用?TypeScript?可以增加代碼的可讀性和可維護(hù)性,在?react?和?vue?社區(qū)中也越來(lái)越多人開始使用TypeScript。從最近發(fā)布的?Vue3?正式版本來(lái)看,?Vue3?的源碼就是用?TypeScript?編寫的,更好的?TypeScript?支持也是這一次升級(jí)的亮點(diǎn)。當(dāng)然,在實(shí)際開發(fā)中如何正確擁抱?TypeScript?也是遷移至?Vue3?的一個(gè)小痛點(diǎn),這里就針對(duì)?Vue3?和?TypeScript?展開一些交流。

96.8%的代碼都是TypeScript,支持的力度也是相當(dāng)大?
項(xiàng)目搭建
在官方倉(cāng)庫(kù)的 Quickstart 中推薦用兩種方式方式來(lái)構(gòu)建我們的?SPA?項(xiàng)目:
vite
npm?init?vite-app?sail-vue3?#?OR?yarn?create?vite-app?sail-vue3
vue-cli
npm?install?-g?@vue/cli?#?OR?yarn?global?add?@vue/cli
vue?create?sail-vue3
#?select?vue?3?preset
vite?是一個(gè)由原生ESM驅(qū)動(dòng)的Web開發(fā)構(gòu)建工具,打開?vite?依賴的?package.json?可以發(fā)現(xiàn)在?devDependencies?開發(fā)依賴?yán)锩嬉呀?jīng)引入了TypeScript?,甚至還有?vuex?,?vue-router?,?less?,?sass?這些本地開發(fā)經(jīng)常需要用到的工具。vite?輕量,開箱即用的特點(diǎn),滿足了大部分開發(fā)場(chǎng)景的需求,作為快速啟動(dòng)本地?Vue?項(xiàng)目來(lái)說(shuō),這是一個(gè)非常完美的工具。
后面的演示代碼也是用vite搭的
從?vue2.x?走過(guò)來(lái)的掘友肯定知道?vue-cli?這個(gè)官方腳手架,?vue3?的更新怎么能少得了?vue-cli?呢,?vue-cli?更強(qiáng)調(diào)的是用?cli?的方式進(jìn)行交互式的配置,選擇起來(lái)更加靈活可控。豐富的官方插件適配,GUI的創(chuàng)建管理界面,標(biāo)準(zhǔn)化開發(fā)流程,這些都是?vue-cli?的特點(diǎn)。
vue-cli ? TypeScript STEP1

vue-cli ? TypeScript STEP2

想要預(yù)裝TypeScript,就需要選擇手動(dòng)配置,并check好TypeScript
忘記使用選擇?TypeScript?也沒(méi)事,加一行cli命令就行了
vue?add?typescript
最后,別忘了在?.vue?代碼中,給?script?標(biāo)簽加上?lang="ts"
<script?lang="ts">
Option API風(fēng)格
在?Vue2.x?使用過(guò)?TypeScript?的掘友肯定知道引入?TypeScript?不是一件簡(jiǎn)單的事情:
要用? vue-class-component?強(qiáng)化?vue?組件,讓?Script?支持?TypeScript?裝飾器用? vue-property-decorator?來(lái)增加更多結(jié)合?Vue?特性的裝飾器引入? ts-loader?讓?webpack?識(shí)別?.ts?.tsx?文件.....
然后出來(lái)的代碼風(fēng)格是這樣的:
@Component({
????components:{?componentA,?componentB},
})
export?default?class?Parent?extends?Vue{
??@Prop(Number)?readonly?propA!:?number?|?undefined
??@Prop({?default:?'default?value'?})?readonly?propB!:?string
??@Prop([String,?Boolean])?readonly?propC!:?string?|?boolean?|?undefined
??//?data信息
??message?=?'Vue2?code?style'
??//?計(jì)算屬性
??private?get?reversedMessage?():?string[]?{
??????return?this.message.split('?').reverse().join('')
??}
??//?method
??public?changeMessage?():?void?{
????this.message?=?'Good?bye'
??}
}
class?風(fēng)格的組件,各種裝飾器穿插在代碼中,有點(diǎn)感覺(jué)自己不是在寫?vue?,些許凌亂?,所以這種曲線救國(guó)的方案在?vue3?里面肯定是行不通的。
在?vue3?中可以直接這么寫:
import?{?defineComponent,?PropType?}?from?'vue'
interface?Student?{
??name:?string
??class:?string
??age:?number
}
const?Component?=?defineComponent({
??props:?{
????success:?{?type:?String?},
????callback:?{
??????type:?Function?as?PropType<()?=>?void>
????},
????student:?{
??????type:?Object?as?PropType,
??????required:?true
????}
??},
??data()?{
?????return?{
????????message:?'Vue3?code?style'
????}
??},
??computed:?{
????reversedMessage():?string?{
??????return?this.message.split('?').reverse().join('')
????}
??}
})
vue?對(duì)?props?進(jìn)行復(fù)雜類型驗(yàn)證的時(shí)候,就直接用?PropType?進(jìn)行強(qiáng)制轉(zhuǎn)換,?data?中返回的數(shù)據(jù)也能在不顯式定義類型的時(shí)候推斷出大多類型,?computed?也只用返回類型的計(jì)算屬性即可,代碼清晰,邏輯簡(jiǎn)單,同時(shí)也保證了?vue?結(jié)構(gòu)的完整性。
Composition API風(fēng)格
在?vue3?的?Composition API?代碼風(fēng)格中,比較有代表性的api就是?ref?和?reactive?,我們看看這兩個(gè)是如何做類型聲明的:
ref
import?{?defineComponent,?ref?}?from?'vue'
const?Component?=?defineComponent({
setup()?{
??const?year?=?ref(2020)
??const?month?=?ref('9')
??month.value?=?9?//?OK
??const?result?=?year.value.split('')?//?=>?Property?'split'?does?not?exist?on?type?'number'
?}
})
分析上面的代碼,可以發(fā)現(xiàn)如果我們不給定?ref?定義的類型的話,?vue3?也能根據(jù)初始值來(lái)進(jìn)行類型推導(dǎo),然后需要指定復(fù)雜類型的時(shí)候簡(jiǎn)單傳遞一個(gè)泛型即可。
Tips:如果只有setup方法的話,可以直接在defineComponent中傳入setup函數(shù)
const?Component?=?defineComponent(()?=>?{
????const?year?=?ref(2020)
????const?month?=?ref('9')
????month.value?=?9?//?OK
????const?result?=?year.value.split('')?//?=>?Property?'split'?does?not?exist?on?type?'number'
})
reactive
import?{?defineComponent,?reactive?}?from?'vue'
interface?Student?{
??name:?string
??class?:?string
??age:?number
}
export?default?defineComponent({
??name:?'HelloWorld',
??setup()?{
????const?student?=?reactive({?name:?'阿勇',?age:?16?})
????//?or
????const?student:?Student?=?reactive({?name:?'阿勇',?age:?16?})
????//?or
????const?student?=?reactive({?name:?'阿勇',?age:?16,?class:?'cs'?})?as?Student
??}
})
聲明?reactive?的時(shí)候就很推薦使用接口了,然后怎么使用類型斷言就有很多種選擇了,這是?TypeScript?的語(yǔ)法糖,本質(zhì)上都是一樣的。
自定義Hooks
vue3?借鑒?react hooks?開發(fā)出了?Composition API?,那么也就意味著?Composition API?也能進(jìn)行自定義封裝?hooks?,接下來(lái)我們就用?TypeScript?風(fēng)格封裝一個(gè)計(jì)數(shù)器邏輯的?hooks?(?useCount?):
首先來(lái)看看這個(gè)?hooks?怎么使用:
import?{?ref?}?from?'/@modules/vue'
import??useCount?from?'./useCount'
export?default?{
??name:?'CountDemo',
??props:?{
????msg:?String
??},
??setup()?{
????const?{?current:?count,?inc,?dec,?set,?reset?}?=?useCount(2,?{
??????min:?1,
??????max:?15
????})
????const?msg?=?ref('Demo?useCount')
????return?{
??????count,
??????inc,
??????dec,
??????set,
??????reset,
??????msg
????}
??}
}
出來(lái)的效果就是:

貼上?useCount?的源碼:
import?{?ref,?Ref,?watch?}?from?'vue'
interface?Range?{
??min?:?number,
??max?:?number
}
interface?Result?{
??current:?Ref,
??inc:?(delta?:?number)?=>?void,
??dec:?(delta?:?number)?=>?void,
??set:?(value:?number)?=>?void,
??reset:?()?=>?void
}
export?default?function?useCount(initialVal:?number,?range?:?Range):?Result?{
??const?current?=?ref(initialVal)
??const?inc?=?(delta?:?number):?void?=>?{
????if?(typeof?delta?===?'number')?{
??????current.value?+=?delta
????}?else?{
??????current.value?+=?1
????}
??}
??const?dec?=?(delta?:?number):?void?=>?{
????if?(typeof?delta?===?'number')?{
??????current.value?-=?delta
????}?else?{
??????current.value?-=?1
????}
??}
??const?set?=?(value:?number):?void?=>?{
????current.value?=?value
??}
??const?reset?=?()?=>?{
????current.value?=?initialVal
??}
??watch(current,?(newVal:?number,?oldVal:?number)?=>?{
????if?(newVal?===?oldVal)?return
????if?(range?&&?range.min?&&?newVal???????current.value?=?range.min
????}?else?if?(range?&&?range.max?&&?newVal?>?range.max)?{
??????current.value?=?range.max
????}
??})
??return?{
????current,
????inc,
????dec,
????set,
????reset
??}
}
分析源碼
這里首先是對(duì)?hooks?函數(shù)的入?yún)㈩愋秃头祷仡愋瓦M(jìn)行了定義,入?yún)⒌?Range?和返回的?Result?分別用一個(gè)接口來(lái)指定,這樣做了以后,最大的好處就是在使用?useCount?函數(shù)的時(shí)候,ide就會(huì)自動(dòng)提示哪些參數(shù)是必填項(xiàng),各個(gè)參數(shù)的類型是什么,防止業(yè)務(wù)邏輯出錯(cuò)。

接下來(lái),在增加?inc?和減少?dec?的兩個(gè)函數(shù)中增加了?typeo?類型守衛(wèi)檢查,因?yàn)閭魅氲?delta?類型值在某些特定場(chǎng)景下不是很確定,比如在?template?中調(diào)用方法的話,類型檢查可能會(huì)失效,傳入的類型就是一個(gè)原生的?Event?。
關(guān)于?ref?類型值,這里并沒(méi)有特別聲明類型,因?yàn)?vue3?會(huì)進(jìn)行自動(dòng)類型推導(dǎo),但如果是復(fù)雜類型的話可以采用類型斷言的方式:ref(initObj) as Ref
小建議 ?
AnyScript
在初期使用?TypeScript?的時(shí)候,很多掘友都很喜歡使用?any?類型,硬生生把TypeScript?寫成了?AnyScript?,雖然使用起來(lái)很方便,但是這就失去了?TypeScript?的類型檢查意義了,當(dāng)然寫類型的習(xí)慣是需要慢慢去養(yǎng)成的,不用急于一時(shí)。
Vetur
vetur?代碼檢查工具在寫vue代碼的時(shí)候會(huì)非常有用,就像構(gòu)建?vue?項(xiàng)目少不了?vue-cli?一樣,vetur?提供了?vscode?的插件支持,趕著升級(jí)?vue3?這一波工作,順帶也把?vetur?也帶上吧。

一個(gè)完整的Vue3+ts項(xiàng)目
├─public
│??????favicon.ico
│??????index.html
└─src
????│??App.vue
????│??main.ts
????│??shims-vue.d.ts
????├─assets
????│??│??logo.png
????│??└─css
????│??????????base.css
????│??????????main.styl
????├─components
????│??│??HelloWorld.vue
????│??└─base
????│??????????Button.vue
????│??????????index.ts
????│??????????Select.vue
????├─directive
????│??????focus.ts
????│??????index.ts
????│??????pin.ts
????├─router
????│??????index.ts
????├─store
????│??????index.ts
????├─utils
????│??│??cookie.ts
????│??│??deep-clone.ts
????│??│??index.ts
????│??│??storage.ts
????│??└─validate
????│??????????date.ts
????│??????????email.ts
????│??????????mobile.ts
????│??????????number.ts
????│??????????system.ts
????└─views
????????│??About.vue
????????│??Home.vue
????????│??LuckDraw.vue
????????│??TodoList.vue
????????└─address
????????????????AddressEdit.tsx
????????????????AddressList.tsx
.vue寫法
??...
???...
tsx寫法
import?{?ref,?reactive?}?from?"vue";
import?{?AddressList,?NavBar,?Toast,?Popup?}?from?"vant";
import?AddressEdit?from?'./AddressEdit'
import?router?from?'@/router'
export?default?{
??setup()?{
????const?chosenAddressId?=?ref('1')
????const?showEdit?=?ref(false)
????const?list?=?reactive([
??????{
????????id:?'1',
????????name:?'張三',
????????tel:?'13000000000',
????????address:?'浙江省杭州市西湖區(qū)文三路?138?號(hào)東方通信大廈?7?樓?501?室',
????????isDefault:?true,
??????},
??????{
????????id:?'2',
????????name:?'李四',
????????tel:?'1310000000',
????????address:?'浙江省杭州市拱墅區(qū)莫干山路?50?號(hào)',
??????},
????])
????const?disabledList?=?reactive([
??????{
????????id:?'3',
????????name:?'王五',
????????tel:?'1320000000',
????????address:?'浙江省杭州市濱江區(qū)江南大道?15?號(hào)',
??????},
????])
????const?onAdd?=?()?=>?{
??????showEdit.value?=?true
????}
????const?onEdit?=?(item:?any,?index:?string)?=>?{
??????Toast('編輯地址:'?+?index);
????}
????const?onClickLeft?=?()?=>?{
??????router.back()
????}
????const?onClickRight?=?()?=>?{
??????router.push('/todoList')
????}
????return?()?=>?{
??????return?(
????????
??????????????????????title="地址管理"
????????????left-text="返回"
????????????right-text="Todo"
????????????left-arrow
????????????onClick-left={onClickLeft}
????????????onClick-right={onClickRight}
??????????/>
??????????????????????vModel={chosenAddressId.value}
????????????list={list}
????????????disabledList={disabledList}
????????????disabledText="以下地址超出配送范圍"
????????????defaultTagText="默認(rèn)"
????????????onAdd={onAdd}
????????????onEdit={onEdit}
??????????/>
??????????
????????????
??????????
????????
??????);
????};
??}
};
結(jié)束
不知不覺(jué),?Vue?都到3的One Piece時(shí)代了,?Vue3?的新特性讓擁抱?TypeScript?的姿勢(shì)更加從容優(yōu)雅,?Vue?面向大型項(xiàng)目開發(fā)也更加有底氣了,點(diǎn)擊查看更多。

【面試】803- 66 條 JavaScript 面試知識(shí)點(diǎn)

【Vuejs】802- 如何區(qū)別 Vue2 和 Vue3 ?

【面試】801- 聊下你對(duì) Vue.js 框架的理解
回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
