Vue.extend or vue-class-component
使用 TypeScript 寫 Vue 組件時,有兩種推薦形式:
- Vue.extend():使用基礎 Vue 構造器,創建一個“子類”。此種寫法與 Vue 單文件組件標準形式最爲接近,唯一不同僅是組件選項需要被包裹在 Vue.extend() 中。
- vue-class-component:通常與 vue-property-decorator 一起使用,提供一系列裝飾器,能讓我們書寫類風格的 Vue 組件。
兩種形式輸出結果一致,同是創建一個 Vue 子類,但在書寫組件選項如 props,mixin 時,有些不同。特別是當你使用 Vue.extend() 時,爲了讓 TypeScript 正確推斷類型,你將不得不做一些額外的處理。接下來,我們來聊一聊它們的細節差異。
Prop
由於組件實例的作用域是孤立的,當從父組件傳遞數據到子組件時,我們通常使用 Prop 選項。同時,爲了確保 Prop 的類型安全,我們會給 Prop 添加指定類型驗證,形式如下:
export default {
props: {
someProp: {
type: Object,
required: true,
default: () => ({ message: 'test' })
}
}
}
我們定義了一個 someProp,它的類型是 Object。
使用 JavaScript 時,這並沒有什麼不對的地方,但當你使用 TypeScript 時,這有點不足,我們並不能得到有關於 someProp 更多有用的信息(比如它含有某些屬性),甚至在 TypeScript 看來,這將會是一個 any 類型:
這意味着我們可以使用 someProp 上的任意屬性(存在或者是不存在的)都可以通過編譯。爲了防止此種情況的發生,我們將會給 Prop 添加類型註釋。
Vue.extend()
使用 Vue.extend() 方法添加類型註釋時,需要給 type 斷言:
import Vue from 'vue'
interface User {
name: string,
age: number
}
export default Vue.extend({
props: {
testProps: {
type: Object as () => User
}
}
})
當組件內訪問 testProps 時,便能得到相關提示:
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。
然而,你必須以函數返回值的形式斷言,並不能直接斷言:
export default Vue.extend({
props: {
testProps: {
type: Object as User
}
}
})
它會給出錯誤警告,User 接口並沒有實現原生 Object 構造函數所執行的方法:
Type 'ObjectConstructor' cannot be converted to type 'User'. Property 'id' is missing in type 'ObjectConstructor'.
實際上,我們可從 Prop type declaration :
export type Prop<T> = { (): T } | { new (...args: any[]): T & object }
export type PropValidator<T> = PropOptions<T> | Prop<T> | Prop<T>[];
export interface PropOptions<T=any> {
type?: Prop<T> | Prop<T>[];
required?: boolean;
default?: T | null | undefined | (() => object);
validator?(value: T): boolean;
}
可知 Prop type 可以以兩種不同方式出現:
- 含有一個調用簽名的範型 type,該簽名返回 T;
- 一個範型構造函數簽名,該函數創建指定類型 T 對象 (返回值 T & object 用於降低優先級,當兩種方式同時滿足時取第一種,其次它還可以用於標記構造函數不應該返回原始類型)。
當我們指定 type 類型爲 String/Number/Boolean/Array/Object/Date/Function/Symbol 等原生構造函數時,Prop<T> 會返回它們各自簽名的返回值。
當 type 類型爲 String 構造函數時,它的調用簽名返回爲 string:
// lib.es5.d.ts
interface StringConstructor {
new(value?: any): String;
(value?: any): string;
readonly prototype: String;
fromCharCode(...codes: number[]): string;
}
而這也是上文中,當指定 type 類型爲 Object 構造函數時,經過 Vue 的聲明文件處理,TypeScript 推斷出爲 any 類型的原因:
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
// 其它屬性 ....
}
類似的,當我們使用關鍵字 as 斷言 Object 爲 () => User 時,它能推斷出爲 User 。
從 type 第二部分可知,除傳入原生構造函數外,我們還可傳入自定義類:
此外,這裏有個 PR 暴露一個更直觀的類型( Vue 2.6 版本纔可以用):
props: {
testProp: Object as PropTypes<{ test: boolean }>
}
vue-class-component
得益於 vue-propperty-decorator Prop 修飾器,當給 Prop 增加類型推斷時,這些將變得簡單:
import { Component, Vue, Prop } from 'vue-property-decorator'
@Component
export default class Test extends Vue {
@Prop({ type: Object })
private test: { value: string }
}
當我們在組件內訪問 test 時,便能獲取它正確的類型信息。
mixins
mixins 是一種分發 Vue 組件中可複用功能的一種方式。當在 TypeScript 中使用它時,我們希望得到有關於 mixins 的類型信息。
當你使用 Vue.extends() 時,這有點困難,它並不能推斷出 mixins 裏的類型:
// ExampleMixin.vue
export default Vue.extend({
data () {
return {
testValue: 'test'
}
}
})
// other.vue
export default Vue.extend({
mixins: [ExampleMixin],
created () {
this.testValue // error, testValue 不存在!
}
})
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。
我們需要稍作修改:
// other.vue
export default ExampleMixin.extend({
mixins: [ExampleMixin],
created () {
this.testValue // 編譯通過
}
})
但這會存在一個問題,當使用多個 mixins 且推斷出類型時,這將無法工作。而在這個 Issuse 中官方也明確表示,這無法被修改。
使用 vue-class-component 這會方便很多:
// ExampleMixin.vue
import Vue from 'vue'
import Component from 'vue-class-component'
@Component
export class ExampleMixin extends Vue {
public testValue = 'test'
}
// other.vue
import Component, { mixins } from 'vue-class-component'
import ExampleMixin from 'ExampleMixin.vue'
@Component({
components: {
ExampleMixin
}
})
export class MyComp extends mixins(ExampleMixin) {
created () {
console.log(this.testValue) // 編譯通過
}
}
也支持可以傳入多個 mixins。
一些其它
做爲 Vue 中最正統的方法(與標準形式最爲接近),Vue.extends() 有着自己的優勢,在 VScode Vetur 插件輔助下,它能正確提示子組件上的 Props:
而類做爲 TypeScript 特殊的存在(它既可以作爲類型,也可以作爲值),當我們使用 vue-class-component 並通過 $refs 綁定爲子類組件時,便能獲取子組件上暴露的類型信息:
導入 .vue 時,爲什麼會報錯?
當你在 Vue 中使用 TypeScript 時,所遇到的第一個問題即是在 ts 文件中找不到 .vue 文件,即使你所寫的路徑並沒有問題:
在 TypeScript 中,它僅識別 js/ts/jsx/tsx 文件,爲了讓它識別 .vue 文件,我們需要顯式告訴 TypeScript,vue 文件存在,並且指定導出 VueConstructor:
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
前端全棧學習交流圈:866109386,面向1-3經驗年前端開發人員,幫助突破技術瓶頸,提升思維能力,羣內有大量PDF可供自取,更有乾貨實戰項目視頻進羣免費領取。
但是,這引起了另一個問題,當我們導入一個並不存在的 .vue 文件時,也能通過編譯:
是的,這在情理之中。
當我嘗試在 .vue 文件中導入已存在或者不存在的 .vue 文件時,卻得到不同的結果:
文件不存在時:
文件存在時:
文件不存在時,引用 Vue 的聲明文件。文件存在時,引用正確的文件定義。
這讓人很困惑,而這些都是 Vetur 的功勞。
最後
爲了幫助大家讓學習變得輕鬆、高效,給大家免費分享一大批資料,幫助大家在成爲全棧工程師,乃至架構師的路上披荊斬棘。在這裏給大家推薦一個前端全棧學習交流圈:866109386.歡迎大家進羣交流討論,學習交流,共同進步。
當真正開始學習的時候難免不知道從哪入手,導致效率低下影響繼續學習的信心。
但最重要的是不知道哪些技術需要重點掌握,學習時頻繁踩坑,最終浪費大量時間,所以有有效資源還是很有必要的。
最後祝福所有遇到瓶疾且不知道怎麼辦的前端程序員們,祝福大家在往後的工作與面試中一切順利。