一個(gè)完整的Vue3+Ts項(xiàng)目,支持.vue和.tsx寫法 項(xiàng)目地址:https://github.com/vincentzyc/vue3-demo.git
TypeScript
是JS的一個(gè)超集,,主要提供了類型系統(tǒng)和對ES6的支持,,使用 TypeScript
可以增加代碼的可讀性和可維護(hù)性,,在 react
和 vue
社區(qū)中也越來越多人開始使用TypeScript
。從最近發(fā)布的 Vue3
正式版本來看, Vue3
的源碼就是用 TypeScript
編寫的,,更好的 TypeScript
支持也是這一次升級的亮點(diǎn),。當(dāng)然,在實(shí)際開發(fā)中如何正確擁抱 TypeScript
也是遷移至 Vue3
的一個(gè)小痛點(diǎn),,這里就針對 Vue3
和 TypeScript
展開一些交流,。
96.8%的代碼都是TypeScript,支持的力度也是相當(dāng)大??
項(xiàng)目搭建 在官方倉庫的 Quickstart 中推薦用兩種方式方式來構(gòu)建我們的 SPA
項(xiàng)目:
npm init vite-app sail-vue3 # OR yarn create vite-app sail-vue3
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ā)場景的需求,作為快速啟動(dòng)本地 Vue
項(xiàng)目來說,,這是一個(gè)非常完美的工具,。
后面的演示代碼也是用vite搭的
從 vue2.x
走過來的掘友肯定知道 vue-cli
這個(gè)官方腳手架, vue3
的更新怎么能少得了 vue-cli
呢,, vue-cli
更強(qiáng)調(diào)的是用 cli
的方式進(jìn)行交互式的配置,,選擇起來更加靈活可控。豐富的官方插件適配,,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
也沒事,,加一行cli命令就行了
vue add typescript
最后,,別忘了在 .vue
代碼中,給 script
標(biāo)簽加上 lang='ts'
<script lang ='ts' >
Option API風(fēng)格 在 Vue2.x
使用過 TypeScript
的掘友肯定知道引入 TypeScript
不是一件簡單的事情:
要用 vue-class-component
強(qiáng)化 vue
組件,,讓 Script
支持 TypeScript
裝飾器 用 vue-property-decorator
來增加更多結(jié)合 Vue
特性的裝飾器 引入 ts-loader
讓 webpack
識(shí)別 .ts
.tsx
文件 然后出來的代碼風(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)感覺自己不是在寫 vue
,,些許凌亂??,,所以這種曲線救國的方案在 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<Student>, required : true } }, data() { return { message : 'Vue3 code style' } }, computed : { reversedMessage(): string { return this .message.split(' ' ).reverse().join('' ) } } })
vue
對 props
進(jìn)行復(fù)雜類型驗(yàn)證的時(shí)候,,就直接用 PropType
進(jìn)行強(qiáng)制轉(zhuǎn)換,, data
中返回的數(shù)據(jù)也能在不顯式定義類型的時(shí)候推斷出大多類型, computed
也只用返回類型的計(jì)算屬性即可,,代碼清晰,邏輯簡單,,同時(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<string | number>('9' ) month.value = 9 // OK const result = year.value.split('' ) // => Property 'split' does not exist on type 'number' } })
分析上面的代碼,,可以發(fā)現(xiàn)如果我們不給定 ref
定義的類型的話,, vue3
也能根據(jù)初始值來進(jìn)行類型推導(dǎo),然后需要指定復(fù)雜類型的時(shí)候簡單傳遞一個(gè)泛型即可,。
Tips:如果只有setup方法的話,,可以直接在defineComponent中傳入setup函數(shù)
const Component = defineComponent(() => { const year = ref(2020 ) const month = ref<string | number>('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<Student>({ 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
的語法糖,,本質(zhì)上都是一樣的。
自定義Hooks vue3
借鑒 react hooks
開發(fā)出了 Composition API
,,那么也就意味著 Composition API
也能進(jìn)行自定義封裝 hooks
,,接下來我們就用 TypeScript
風(fēng)格封裝一個(gè)計(jì)數(shù)器邏輯的 hooks
( useCount
):
首先來看看這個(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 } } }
出來的效果就是:
貼上 useCount
的源碼:
import { ref, Ref, watch } from 'vue' interface Range { min?: number, max?: number } interface Result { current : Ref<number>, 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 < range.min) { current.value = range.min } else if (range && range.max && newVal > range.max) { current.value = range.max } }) return { current, inc, dec, set , reset } }
分析源碼
這里首先是對 hooks
函數(shù)的入?yún)㈩愋秃头祷仡愋瓦M(jìn)行了定義,入?yún)⒌?nbsp;Range
和返回的 Result
分別用一個(gè)接口來指定,,這樣做了以后,,最大的好處就是在使用 useCount
函數(shù)的時(shí)候,ide就會(huì)自動(dòng)提示哪些參數(shù)是必填項(xiàng),,各個(gè)參數(shù)的類型是什么,,防止業(yè)務(wù)邏輯出錯(cuò)。
接下來,,在增加 inc
和減少 dec
的兩個(gè)函數(shù)中增加了 typeo
類型守衛(wèi)檢查,,因?yàn)閭魅氲?nbsp;delta
類型值在某些特定場景下不是很確定,比如在 template
中調(diào)用方法的話,,類型檢查可能會(huì)失效,,傳入的類型就是一個(gè)原生的 Event
。
關(guān)于 ref
類型值,,這里并沒有特別聲明類型,,因?yàn)?nbsp;vue3
會(huì)進(jìn)行自動(dòng)類型推導(dǎo),但如果是復(fù)雜類型的話可以采用類型斷言的方式:ref(initObj) as Ref<ObjType>
小建議 ?? AnyScript
在初期使用 TypeScript
的時(shí)候,,很多掘友都很喜歡使用 any
類型,,硬生生把TypeScript
寫成了 AnyScript
,雖然使用起來很方便,,但是這就失去了 TypeScript
的類型檢查意義了,,當(dāng)然寫類型的習(xí)慣是需要慢慢去養(yǎng)成的,不用急于一時(shí),。
Vetur
vetur
代碼檢查工具在寫vue代碼的時(shí)候會(huì)非常有用,,就像構(gòu)建 vue
項(xiàng)目少不了 vue-cli
一樣,vetur
提供了 vscode
的插件支持,趕著升級 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
<template> ... </template> <script lang='ts' > import dayjs from 'dayjs' ; import { ref, reactive, onMounted } from 'vue' ; import { Button, Step, Steps, NoticeBar } from 'vant' ;export default { components: { Button, Step, Steps, NoticeBar, }, setup () { const nameinput = ref(); const selectionStart = ref(0); const twoNow = dayjs().subtract(2, 'day' ).format('YYYY-MM-DD HH:mm:ss' ); const now = dayjs().format('YYYY-MM-DD HH:mm:ss' ); const now2 = dayjs().add(2, 'day' ).format('YYYY-MM-DD HH:mm:ss' ); const formData = reactive({ name: '' , phone: '' , code: '' , }); onMounted(() => { (nameinput.value as HTMLInputElement).focus(); }); const insertName = () => { const index = (nameinput.value as HTMLInputElement).selectionStart; if (typeof index !== 'number' ) return ; formData.name = formData.name.slice(0, index) + '哈哈' + formData.name.slice(index); }; return { nameinput, formData, insertName, selectionStart, twoNow, now, now2, }; }, }; </script>
<template> ... </template> <script lang='ts' > import dayjs from 'dayjs' ; import { defineComponent } from 'vue' ; import HelloWorld from '@/components/HelloWorld.vue' ; // @ is an alias to /src import { Button, Dialog, Toast } from 'vant' ;export default defineComponent({ name: 'Home' , components: { HelloWorld, Button, }, data () { return { direction: 'top' , pinPadding: 0, time: '' , timer: 0, color: 'red' , }; }, methods: { showToast () { Toast('字體顏色已改藍(lán)色' ); this.color = 'blue' ; }, handleClick () { Dialog({ title: '標(biāo)題' , message: '這是一個(gè)全局按鈕組件' , }); }, initTime () { this.time = dayjs().format('YYYY-MM-DD HH:mm:ss' ); this.timer = setInterval(() => { this.time = dayjs().format('YYYY-MM-DD HH:mm:ss' ); }, 1000); }, }, created () { this.initTime(); }, beforeUnmount () { clearInterval(this.timer); }, }); </script> <style vars='{ color }' > .text-color { color: var(--color); } </style>
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 號東方通信大廈 7 樓 501 室' , isDefault : true , }, { id : '2' , name : '李四' , tel : '1310000000' , address : '浙江省杭州市拱墅區(qū)莫干山路 50 號' , }, ]) const disabledList = reactive([ { id : '3' , name : '王五' , tel : '1320000000' , address : '浙江省杭州市濱江區(qū)江南大道 15 號' , }, ]) const onAdd = () => { showEdit.value = true } const onEdit = (item: any, index: string ) => { Toast('編輯地址:' + index); } const onClickLeft = () => { router.back() } const onClickRight = () => { router.push('/todoList' ) } return () => { return ( <div style='background:#f7f8fa'> <NavBar title='地址管理' left-text='返回' right-text='Todo' left-arrow onClick-left={onClickLeft} onClick-right={onClickRight} /> <AddressList vModel={chosenAddressId.value} list={list} disabledList={disabledList} disabledText='以下地址超出配送范圍' defaultTagText='默認(rèn)' onAdd={onAdd} onEdit={onEdit} /> <Popup vModel={[showEdit.value, 'show']} position='bottom' round style='height: 80%' > <AddressEdit /> </Popup> </div > ); }; } };
結(jié)束 不知不覺, Vue
都到3的One Piece時(shí)代了,, Vue3
的新特性讓擁抱 TypeScript
的姿勢更加從容優(yōu)雅,, Vue
面向大型項(xiàng)目開發(fā)也更加有底氣了。