vue中引入typescript開發使用vue.extend()和vue.class-component
最近嘗試了一下 TypeScript,試著把一個 Vue 專案改成了 TypeScript 的,感覺還不錯
目前 Vue 和 TypeScript 的配合還不算很完美,Vuex 和 TypeScript 的配合挺糟糕的,嘗試需要謹慎
如果想體驗一下的話,強烈建議你用 vue-cli 3 直接生成專案目錄,這樣會少挺多配置,比如配 tsconfig 什麼的,在用 vue-cli 的時候選擇 TypeScript 就好
如果想自己體驗從 0 開始配置請參考這個文件 TypeScript-Vue-Starter
初始配置
這裡不提初始配置,因為 vue-cli 已經預設配置好了,在根目錄下,有個 tsconfig.json 的檔案,在 src 的資料夾下面,有 shims-tsx.d.ts, shims-vue.d.ts 檔案
shims-vue.d.ts 這個檔案,主要用於 TypeScript 識別.vue 檔案,Ts 預設並不支援匯入 vue 檔案,這個檔案告訴 ts 匯入.vue 檔案都按VueConstructor<Vue>
處理,因此匯入 vue 檔案必須寫.vue 字尾,但是這樣同樣的也會造成,就算你寫的匯入的 .vue 檔案的路徑就算是錯的,靜態檢測也不會檢測到錯誤,如果你把滑鼠放上面你會看到錯誤的路徑就是指向這個檔案,因為你定義了這個模組是所有 .vue 字尾的匯入都會指向到這個檔案,但是如果你的路徑是對的,ts 能讀出正確的 module。
說那麼多可能沒用,看一下下面兩張圖就懂了
匯入地址是正確的
匯入地址是錯誤的
shims-tsx.d.ts 檔案,這個檔案主要是方便你使用在 ts 中使用 jsx 語法的,如果不使用 jsx 語法,可以無視這個,但是強烈建議使用 jsx 語法,畢竟模板是沒法獲得靜態型別提示的,當然,如果你境界高的話,直接用 vue render function。想要使用 jsx 語法的話,配合 babel-plugin-jsx-v-model,這個外掛,體驗更佳,這個外掛在 jsx 語法中實現了 v-model。
編輯器支援
推薦使用 Visual Studio Code 和 Vetur 外掛,如果用的是 IDE,推薦使用 WebStorm
使用 TypeScript 編寫 Vue 的時候,主要有兩種方法Vue.extend()
和 vue-class-component
Vue.extend()
:使用基礎 Vue 構造器,建立一個“子類”。 這種方式最接近 Vue 的單檔案元件的寫法,如果一個完善 Vue 專案從 JS 改成 TS,用這種方法很快,只要加上lang=ts
和一些必要的變數型別就好了,然後用Vue.extend()
包裹就好。請看下面的例子:
JavaScript
TypeScript
vue-class-component
:通常和vue-property-decorator
一起搭配使用,實際使用只使用vue-property-decorator
就好了,vue-property-decorator
是在vue-class-component
上擴充套件來的,並且提供了很多修飾器比如@Prop
和@Watch
等等,使用這個可以編寫類式元件,但是如果你是完善的專案 JS 改 TS 的話,需要改的地方很多。看一下下面的例子:
JavaScript
TypeScript
可以看出變化真的很大
這兩種編寫方式風格有很大的不同,下面具體說一下兩種方式的具體實現
-
元件 props
Vue.extend()
實現 props 其實和 JavaScript 沒有任何差別,但是如果你需要有 Object 變數型別提示,那就有點不一樣了vue-class-component
實現 props, 需要從 vue-property-decorator 引入 Prop 這個修飾符,使用起來也非常方便,還是用上面的例子
// JavaScript
props: ['isVisible', 'title', 'item', 'count', 'items']
// 如果加入了prop驗證,這樣寫
props: {
isVisible: {
type: Boolean,
required: true
},
title: {
type: [String, Number]
},
item: {
type: Object
},
items: {
type: Array,
}
count: {
count: Number
}
}
// TypeScript
/* 這種寫法沒有任何改變,但是這樣沒有任何型別提示,這些變數能被TS識別,但是會全部被識別成any,和沒型別檢查一樣 */
props: ['isVisible', 'title', 'item', 'count', 'items']
/* 當加入prop驗證之後,TS就會提示prop型別了,如果是物件的話,還能有物件的成員提示,寫法和JS寫法差不多,只是物件型別(包括物件,陣列和函式)的有點差別,這樣寫的話。*/
// 假設item物件的結構是
interface Item {
key: string
val: string
num: number
}
props: {
isVisible: {
type: Boolean,
required: true
},
title: {
type: [String, Number]
},
item: {
// 注意這裡不是
// Object as Item
type: Object as () => Item
},
itmes: {
// 注意這裡不是
// Array as Array<Item>
type: Array as () => Array<Item>
}
count: {
count: Number
}
}
// vue-class-component方式
import { Vue, Component, Prop } from 'vue-property-decorator'
// 注意要加非空斷言符 ! 不然會報,當然,你定義成any型別當我沒說
/* [non-null-assertion-operator](https://github.com/Microsoft/TypeScript/wiki
/What's-new-in-TypeScript#non-null-assertion-operator) 關於非空斷言可以參考這個 */
@Prop({
required: true
}) isVisible!: boolean
@Prop() title!: string | number
@Prop() item!: Item
@Prop() items!: Array<Item>
@Prop() count!: number
-
元件 data computed methods watch
Vue.extend()
實現 data 其實和 JavaScript 沒有任何差別,computed 的話,也沒什麼大的改變,但是有 this 參與運算的必須標明返回值型別,不然會報錯, methods 的處理方式和 computed 的一樣,有 this 參與運算的必須標明返回值型別,watch 也是一樣vue-class-component
實現 data 的話,直接在類裡面寫變數就好,computed 的話,寫法類似 getter 和 setter,methods 處理方式就是直接在裡面寫方法,watch 需要從 vue-property-decorator 引入 Watch 這個修飾符
//一個簡單的例子
// Vue.extend()
import Vue from 'vue'
export default Vue.extend({
data() {
return {
count: 1,
item: {
c: '',
n: ''
}
}
},
computed: {
// 需要標註有 `this` 參與運算的返回值型別
num(): number {
return this.count
},
name: {
// 需要標註有 `this` 參與運算的返回值型別
get(): string {
return this.item.n
},
set(val: string) {
this.item.n = val
}
}
},
watch: {
count(newVal: number, oldVal: number): void {
console.log(newVal)
},
'item.n'(newVal: string, oldVal: string): void {
console.log(newVal)
},
item: {
handler(newV, oldVal) {
console.log(oldVal)
},
deep: true
}
},
methods: {
reset(): void {
this.$emit('reset')
},
getKey(): string {
return this.item.c
}
}
})
// vue-class-component
import { Vue, Component, Watch } from 'vue-property-decorator'
interface KeyValue {
c: string
n: string
}
@Component
export default class Test extends Vue {
// data
count: number = 1
item: KeyValue = {
c: '',
n: ''
}
// computed
get num(): number {
return this.count
}
get name(): string {
return this.item.n
}
// 注意,這裡不能標返回值型別,就算寫void也不行
set name(val: string) {
this.item.n = val
}
// watch
@Watch('count')
watchCount(newVal: number, oldVal: number): void {
console.log(newVal)
}
@Watch('item.n')
watchName(newVal: string, oldVal: string): void {
console.log(newVal)
}
@Watch('item', { deep: true })
watchItem(newVal: KeyValue, oldVal: KeyValue): void {
console.log(newVal)
}
// methods
reset(): void {
this.$emit('reset')
},
getKey(): string {
return this.item.c
}
}
-
元件 components
Vue.extend()
components 和 JavaScript 寫法完全一致vue-class-component
需要把匯入的元件寫在修飾器@Components({})裡面
// Vue.extend
import Vue from 'vue'
import MainHeader from './header.vue'
import MainContent from './content.vue'
export default Vue.extend({
components: {
MainHeader,
MainContent
}
})
// vue-class-component
import { Vue, Component } from 'vue-property-decorator'
import MainHeader from './header.vue'
import MainContent from './content.vue'
@Component({
components: {
MainHeader,
MainContent
}
})
export default class extends Vue {}
-
元件 mixins
-
Vue.extend()
並不能完全實現 mixins 多混入的效果,只能混入一個。不推薦混入用這種方式寫,無法實現多繼承。如果你非要嘗試這種寫法,可以看看這個Issue,我沒有嘗試過這種寫法,不過有人寫了個例子,可以作為參考,但是我嘗試了沒成功// ExampleMixin.vue export default Vue.extend({ data () { return { testValue: 'test' } } }) // other.vue export default Vue.extend({ mixins: [ExampleMixin], created () { this.testValue // error, testValue 不存在! } }) 我們需要稍作修改: // other.vue export default ExampleMixin.extend({ mixins: [ExampleMixin], created () { this.testValue // 編譯通過 } })
-
vue-class-component
能夠實現多混入,寫法類似類繼承// mixin1.ts import Vue from 'vue' export default Vue.extend({ data () { return { valFromMixin1: 'test' } } }) // 不能是 // 這種寫法會報 Mixin1 is not a constructor function type export default { data () { return { valFromMixin1: 'test' } } } // mixin2.ts import { Component, Vue } from 'vue-property-decorator' @Component export default class Mixin2 extends Vue { methodFromMixin2() {} } // test.ts import Mixin1 from './mixin1' import Mixin2 from './mixin2' import { Component, Mixins } from 'vue-property-decorator' export default class Test extends Mixins(Mixin1, Mixin2) { test() { this.methodFromMixin2() console.log(this.valFromMixin1) } } // 如果只混入一個的話,可以這樣寫 export default class Test extends Mixin1 {} export default class Test extends Mixin2 {}
這樣寫不僅不會報錯,而且編輯器還有提示
這張圖可以看出,setWatch 是 BookingWatcher 裡面的方法,實際上,Test 這個類並沒有自身的屬性,都是從 Vue,BookingWatcher 還有 TestMixins 繼承過來的
-
- 函式式元件
這個只能用 Vue.extends(),Vue-class-component 無能為力,可以看看這個 Issue
這裡提供一個函式式元件的例子
這部分資料參考
在 Vue 中使用 TypeScript 的一些思考(實踐)
vue 官網 TypeScript 支援
開發中遇到的一些問題總結
-
$refs 報錯
$refs 報錯這個問題相信基本都遇到,除非你真沒用到這個,如圖:
報錯資訊是
Property 'blur' does not exist on type 'Vue | Element | Vue[] | Element[]'.
Property 'blur' does not exist on type 'Vue'.解決方案:把上圖報錯部分改成
// 把這個變數改成定義成HTMLInputElement就好,這裡需要用到型別斷言 test() { let inputBox: HTMLInputElement = this.$refs.inputBox as HTMLInputElement inputBox.blur() }
如果引用的比較多,並且引用裡面有自己的元件的話,可以這樣寫:
$refs: {}
這樣編輯器還會提示元件裡面有什麼方法,當你打出 this.$refs.header.時候,編輯器有提示 Header 這個元件裡面的屬性