Vue with TypeScript

最近嘗試了一下 TypeScript,試着把一個 Vue 項目改爲了 TypeScript 的,感受還不錯。 目前 Vue 和 TypeScript 的配合還不算很完美,Vuex 和 TypeScript 的配合挺糟糕的,嘗試須要謹慎,若是想體驗一下的話,強烈建議你用 vue-cli 3 直接生成項目目錄,這樣會少挺多配置,好比配 tsconfig 什麼的,在用 vue-cli 的時候選擇 TypeScript 就好。html

若是想本身體驗從 0 開始配置請參考這個文檔 TypeScript-Vue-Startervue

這裏不提初始配置,由於 vue-cli 已經默認配置好了,在根目錄下,有個 tsconfig.json 的文件,在 src 的文件夾下面,有 shims-tsx.d.ts, shims-vue.d.ts 文件git

shims-vue.d.ts 這個文件,主要用於 TypeScript 識別.vue 文件,Ts 默認並不支持導入 vue 文件,這個文件告訴 ts 導入.vue 文件都按VueConstructor<Vue>處理,所以導入 vue 文件必須寫.vue 後綴,可是這樣一樣的也會形成,就算你寫的導入的 .vue 文件的路徑就算是錯的,靜態檢測也不會檢測到錯誤,若是你把鼠標放上面你會看到錯誤的路徑就是指向這個文件,由於你定義了這個模塊是全部 .vue 後綴的導入都會指向到這個文件,可是若是你的路徑是對的,ts 能讀出正確的 module。 說那麼多可能沒用,看一下下面兩張圖就懂了github

Alt 導入路徑是正確的
導入路徑是正確的
Alt 導入路徑是錯誤的
導入路徑是錯誤的

shims-tsx.d.ts 文件,這個文件主要是方便你使用在 ts 中使用 jsx 語法的,若是不使用 jsx 語法,能夠無視這個,可是強烈建議使用 jsx 語法,畢竟模板是無法得到靜態類型提示的,固然,若是你境界高的話,直接用 vue render function。想要使用 jsx 語法的話,配合 babel-plugin-jsx-v-model,這個插件,體驗更佳,這個插件在 jsx 語法中實現了 v-model。vue-cli

以上的東西瞭解一下就行了,基本不用變的

編輯器支持

推薦使用 Visual Studio Code 和 Vetur 插件,若是用的是 IDE,推薦使用 WebStormtypescript

使用 TypeScript 編寫 Vue 的時候,主要有兩種方法Vue.extend()vue-class-component

  • Vue.extend():使用基礎 Vue 構造器,建立一個「子類」。 這種方式最接近 Vue 的單文件組件的寫法,若是一個完善 Vue 項目從 JS 改爲 TS,用這種方法很快,只要加上 lang=ts和一些必要的變量類型就行了,而後用Vue.extend()包裹就好。請看下面的例子:
    JavaScript
    json

    Alt JavaScript
    JavaScript

    TypeScript
    Alt TypeScript
    TypeScript

    能夠看到基本沒什麼大的變化

  • vue-class-component:一般和vue-property-decorator一塊兒搭配使用,實際使用只使用vue-property-decorator就行了,vue-property-decorator是在vue-class-component上擴展來的,而且提供了不少修飾器好比 @Prop@Watch等等,使用這個能夠編寫類式組件,可是若是你是完善的項目 JS 改 TS 的話,須要改的地方不少。看一下下面的例子:
    JavaScript
    segmentfault

    Alt JavaScript
    JavaScript

    TypeScript
    Alt TypeScript
    TypeScript

    能夠看出變化真的很大

這兩種編寫方式風格有很大的不一樣,下面具體說一下兩種方式的具體實現

  • 組件 props數組

    • Vue.extend()實現 props 其實和 JavaScript 沒有任何差異,可是若是你須要有 Object 變量類型提示,那就有點不同了babel

    • 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 {}
      複製代碼

      這樣寫不只不會報錯,並且編輯器還有提示

      Alt

      這張圖能夠看出,setWatch 是 BookingWatcher 裏面的方法,實際上,Test 這個類並無自身的屬性,都是從 Vue,BookingWatcher 還有 TestMixins 繼承過來的

  • 函數式組件

    這個只能用 Vue.extends(),Vue-class-component 無能爲力,能夠看看這個 Issue
    這裏提供一個函數式組件TypeScript的寫法

這部分資料參考

在 Vue 中使用 TypeScript 的一些思考(實踐)
vue 官網 TypeScript 支持

開發中遇到的一些問題總結

  • $refs 報錯

    $refs 報錯這個問題相信基本都遇到,除非你真沒用到這個,如圖:

    Alt

    報錯信息是 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!: {}

    Alt

    這樣編輯器還會提示組件裏面有什麼方法,當你打出 this.$refs.header.時候,編輯器有提示 Header 這個組件裏面的屬性和方法

相關文章
相關標籤/搜索