Vue & TypeScript 初體驗

背景介紹

項目使用的是Vue全家桶系列(vue, vuex, vue-router)構建的, 項目代碼量和業務複雜度仍是有一些. 剛開始人少時, 代碼寫起來仍是沒有問題的, 慢慢的, 隨着人員的增多, 會發現你們的代碼"風格"各異:html

  • 有jQuery風格的
  • 有使用ID選擇器更新DOM的
  • 有將vue實例掛載到window下方便使用的
  • 有函數形參都使用對象/數組傳入的
  • etc...

凡此種種, 想起以前看過的一段話"欠的債, 早晚要還的". 有沒有辦法能夠約束下這些"風格"各異的代碼, 而且對當前工程代碼影響不是很大的? => TypeScript, 也許能夠試試.前端

Vue & TypeScript

TypeScript 具備類型系統,且是 JavaScript 的超集,TypeScript 在 2018年 勢頭迅猛,可謂遍地開花。vue

Vue3.0 將使用 TS 重寫,重寫後的 Vue3.0 將更好的支持 TS。 2019 年 TypeScript 將會更加普及,可以熟練掌握 TS,並使用 TS 開發過項目,將更加成爲前端開發者的優點。node

所以, 這個技能必需要學會, 因此也就邊學邊實踐, 並逐步引用到項目中實戰. 預計在12月的版本中, 將其中一個小的vue項目中所有改用TypeScript.git

由於公司是內網環境, 不可訪問外網. So, 只能回來再將代碼複寫一回了. 估計更新會比較慢.github

練手項目地址: vue-typescript-skillsvue-router

工程建立

使用@vue/cli 3.0建立typescript工程.vuex

D:\vueProjects>vue create vue-typescript
Vue CLI v3.9.2
┌───────────────────────────┐
│  Update available: 4.0.5  │
└───────────────────────────┘
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, TS, Router, Vuex, CSS Pre-processors, Linter, Unit, E2E
? Use class-style component syntax? Yes
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)Lint on save
? Pick a unit testing solution: Jest
? Pick a E2E testing solution: Cypress
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? Yes
? Save preset as: vue-typescript
複製代碼

安裝成功後, 便可運行本地開發環境了.vue-cli

目錄結構

安裝成功後, 會生成以下目錄 :typescript

這裏咱們重點關注下4個文件:

  • .eslintrc.js
  • src/shims-tsx.d.ts
  • src/shims-vue.d.ts
  • tsconfig.json

.eslintrc.js

eslint示例和解釋可參考: .eslintrc 文件示例和解釋

在對支持typescript, 可能會新增或修改2個配置,

  1. extends 添加對vue typescript的支持
'extends': [
    'plugin:vue/essential',
    '@vue/standard',
    '@vue/typescript'
  ]
複製代碼
  1. overrides
overrides: [
    {
      files: [
        '**/__tests__/*.{j,t}s?(x)',
        '**/tests/unit/**/*.spec.{j,t}s?(x)'
      ],
      env: {
        jest: true
      }
    }
  ]
複製代碼

這主要用於複寫eslint配置配置, 此處是針對__tests__, 以及tests/units目錄下的js/ts/jsx/ts文件, 修改配置規則, 此處是修改envjest:true

xx.d.ts

ts的語言服務須要.d.ts文件來識別類型,這樣才能作到相應的語法檢查和智能提示. 咱們本身編寫的.d.ts文件直接放在項目的目錄下,ts本身會去識別,不用咱們作什麼操做,更加詳細的資料能夠看一下TypeScript-聲明文件

  • src/shims-tsx.d.ts, 聲明相關的 tsx 模塊
  • src/shims-vue.d.ts, 聲明相關的 vue 模塊

可參考TypeScript-全局變量聲明示例

因爲 TypeScript 默認並不支持 *.vue 後綴的文件,因此在 vue 項目中引入的時候須要建立一個 vue-shim.d.ts 文件,放在項目項目對應使用目錄下,例如 src/vue-shim.d.ts

tsconfig

若是一個目錄下存在一個tsconfig.json文件,那麼它意味着這個目錄是TypeScript項目的根目錄。 tsconfig.json文件中指定了用來編譯這個項目的根文件和編譯選項。 一個項目能夠經過如下方式之一來編譯:

  • 不帶任何輸入文件的狀況下調用tsc,編譯器會從當前目錄開始去查找tsconfig.json文件,逐級向上搜索父目錄。
  • 不帶任何輸入文件的狀況下調用tsc,且使用命令行參數--project(或-p)指定一個包含tsconfig.json文件的目錄。 當命令行上指定了輸入文件時,tsconfig.json文件會被忽略。

tsconfig.json中詳細配置項及說明, 請移步至: TypeScript-項目配置-tsconfig.json

package.json

  1. @vue/eslint-config-typescript - github, vue/cli typescript eslint插件

此規則集是Vue-TypeScript項目的基本配置。除了設置解析器和插件選項外,它還會關閉規則集中的一些衝突規則eslint:recommended。所以,當與其餘可共享配置一塊兒使用時,此配置應放在extends數組的末尾。 例如:

// .eslintrc.js:
module.exports = {
  extends: [
    'plugin:vue/essential',
    'eslint:recommended',
    '@vue/typescript'
  ]
}
複製代碼
  1. @vue/cli-plugin-typescript - github, vue/cli typescript 插件
  2. vue-class-component - github, 強化 Vue 組件,使用 TypeScript/裝飾器 加強 Vue 組件
  3. vue-property-decorator - github, 在 vue-class-component 上加強更多的結合 Vue 特性的裝飾器

Vue組件寫法的變化

在此以前, 咱們可能須要先了解下ES7裝飾器(Decorator)在Javascript中的使用

  1. vue-class-component , 對 Vue 組件進行了一層封裝,讓 Vue 組件語法在結合了 TypeScript 語法更加貼近面向對象編程. 並提供一個工具函數一個裝飾器:
  • @Component
  • @mixins
  1. vue-property-decorator, 在 vue-class-component 上加強更多的結合 Vue 特性的裝飾器, 新增了這 7 個裝飾器:
  • @Watch
  • @Model
  • @Prop
  • @PropSync
  • @Emit
  • @Provide & @Inject
  • @Ref

所以, 會有如下寫法上的改變

1. @Component

@Component(options) options 中須要配置 decorator 庫不支持的屬性, 如: components, filters, directives等

示例:

<template>
 <div>
     <input-demo :demo="demo"></input-demo>
   </div>
 </div>
</template>

<script lang="ts">
import Component from 'vue-class-component'
import { Emit, Inject, Model, Prop, Provide, Ref, Vue, Watch, PropSync } from 'vue-property-decorator'

import InputDemo from './InputDemo.vue'

@Component({
  components: {
    InputDemo
  }
})
export default class Demo extends Vue {
  // data
  count = 0
  demo = '123'

  mounted () {
    window.console.log('bar=> ', this.bar)
    window.console.log('foo=> ', this.foo)
    window.console.log('optional=> ', this.optional)
  }
}
</script>
複製代碼

2. mixins

在使用Vue進行開發時咱們可能須要用到混合,在TypeScript中, 咱們能夠這麼寫

在如下示例中mixins/index.ts中, 咱們在data中添加了一個屬性mixinVal, 值爲: 'Hello Mixin'

// 定義要混合的類 mixins/index.ts
import Vue from 'vue'
import Component from 'vue-class-component'

@Component
// 必定要用Component修飾
export default class myMixins extends Vue {
  mixinVal: string = 'Hello Mixin'
}
複製代碼

而後, 在其餘組件中使用它

<template>
  <div>
    <hello-world msg='hello world'></hello-world>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component, { mixins } from 'vue-class-component'
import { Emit, Inject, Model, Prop, Provide, Watch, PropSync, Ref } from 'vue-property-decorator'

import HelloWorld from '../components/HelloWorld.vue'
import mixinDemo from './mixin'

@Component({
  components: {
    HelloWorld // 組件注入
  }
})
export default class App extends mixins(mixinDemo) {
  // data
  message = 'hello'
  mounted () {
    // 此時, 就可使用this.mixinVal
    window.console.log('mixinVal => ', this.mixinVal) // 輸出: 'Hello Mixin'
  }
}
</script>
複製代碼

3. data

export default class App extends Vue {
  // data
  message = 'hello'
  name = 'dmax'
  child: number | string = 'james'
}
複製代碼

等價於:

export default {
    name: 'App',
    data () {
        return {
            message: 'hello',
            name: 'dmax',
            child: 'james'
        }
    }
}
複製代碼

4. computed

// 計算屬性
  get msg () {
    return 'computed ' + this.message
  }
複製代碼

等價於:

computed: {
    msg(){
        return 'computed ' + this.message
    }
}
複製代碼

5. watch

@Watch(path: string, options: WatchOptions = {})

@Watch 裝飾器接收兩個參數:

  • path: string 被偵聽的屬性名;
  • options?: WatchOptions={} options能夠包含兩個屬性 :
    • immediate?:boolean 偵聽開始以後是否當即調用該回調函數;
    • deep?:boolean 被偵聽的對象的屬性被改變時,是否調用該回調函數;
@Watch('child')
onChildChanged (val: string, oldVal: string) {
    if (val !== oldVal) {
    window.console.log(val)
    }
}
複製代碼

等價於:

watch: {
    'child': {
        handler: 'onChildChanged',
        immediate: false,
        deep: false 
    }
},
method: {
    onChildChanged(val, oldVal) {
        if (val !== oldVal) {
          window.console.log(val)
        }
    }
}
複製代碼

也能夠寫成: @Watch('child', { immediate: true, deep: true }), 等價於:

watch: {
    'child': {
        handler: 'onChildChanged',
        immediate: true,
        deep: true 
    }
},
method: {
    onChildChanged(val, oldVal) {
        if (val !== oldVal) {
          window.console.log(val)
        }
    }
}
複製代碼

6. model

@Model Vue組件提供model: {prop?: string, event?: string} 讓咱們能夠定製prop和event. 默認狀況下, 一個組件上的v-model會:

  • value用做 prop
  • input用做 event,可是一些輸入類型好比單選框和複選框按鈕可能想使用 value prop來達到不一樣的目的。使用model選項能夠迴避這些狀況產生的衝突。

下面是Vue官網的例子

Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    // this allows using the `value` prop for a different purpose
    value: String,
    // use `checked` as the prop which take the place of `value`
    checked: {
      type: Number,
      default: 0
    }
  },
  // ...
})
<my-checkbox v-model="foo" value="some value"></my-checkbox>
複製代碼

上述代碼至關於:

<my-checkbox
  :checked="foo"
  @change="val => { foo = val }"
  value="some value">
</my-checkbox>
複製代碼

即foo雙向綁定的是組件的checke, 觸發雙向綁定數值的事件是change

使用vue-property-decorator提供的@Model改造上面的例子.

Parent.vue

<template>
 <div>
   <child v-model="price"></child>
   <div>
     v-model(price) => {{price}}
   </div>
 </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from 'vue-property-decorator'

import Child from './Child.vue'

@Component({
  components: {
    Child
  }
})
export default class Parent extends Vue {
  price = 'hello price'
}
</script>
複製代碼

Child.vue

<template>
 <div>
   <input type="text" :value="value" @input="changed"/>
 </div>
</template>

<script lang="ts">
import { Vue, Component, Prop, Model, Emit } from 'vue-property-decorator'

@Component
export default class Child extends Vue {
  @Model('input') value!: boolean

  @Emit('input')
  changed (ev:any) {
    return ev.target.value
  }
}
</script>
複製代碼

最終效果可能爲:

也能夠經過clone git庫 vue-typescript-skills, 運行本地服務後進入http://localhost:8080/#/model, 看到效果.

7. props

@Prop(options: (PropOptions | Constructor[] | Constructor) = {})

@Prop裝飾器接收一個參數,這個參數能夠有三種寫法:

  • Constructor,例如String,Number,Boolean等,指定 prop 的類型;
  • Constructor[],指定 prop 的可選類型;
  • PropOptions,可使用如下選項:type,default,required,validator。

示例:

@Component
export default class Hello extends Vue {
  // child, 必傳, child! => 表示不須要構建器進行初始化
  @Prop({ type: [String, Number], required: true }) readonly child!: string | number

  // propA, 非必傳, 類型能夠是number | undefined
  @Prop(Number) readonly propA: number | undefined

  // propB, 非必傳, 類型能夠是number | undefined, propB! => 表示不須要構建器進行初始化
  @Prop({ default: 'default value' }) readonly propB!: string

  // propC, 非必傳, 構建器能夠是String|Boolean, 值類型能夠爲: string | boolean | undefined
  @Prop([String, Boolean]) readonly propC: string | boolean | undefined
}
複製代碼

等價於:

export default {
    name: 'Hello',
    props: {
        child: {
            required: true,
            type: [String, Number]
        },
        propA: {
            type: Number
        },
        propB: {
            required: false,
            type: String,
            default: 'default value'
        },
        propC: {
            type: [String, Boolean]
        }
    }
}
複製代碼

注意

  1. 屬性的ts類型後面須要加上undefined類型;
  2. 或者在屬性名後面加上!,表示非null 和 非undefined的斷言,不然編譯器會給出錯誤提示;

8. prop.sync

@PropSync(propName: string, options: (PropOptions | Constructor[] | Constructor) = {})

@PropSync裝飾器與@prop用法相似,兩者的區別在於: @PropSync 裝飾器接收兩個參數:

  • propName: string 表示父組件傳遞過來的屬性名;
  • options: Constructor | Constructor[] | PropOptions 與@Prop的第一個參數一致;

@PropSync 會生成一個新的計算屬性。 示例:

import { Vue, Component, PropSync } from 'vue-property-decorator'

@Component
export default class MyComponent extends Vue {
  @PropSync('name', { type: String }) syncedName!: string
}
複製代碼

等價於

props: {
  name: {
    type: String
  }
},
computed: {
  syncedName: {
    get() {
      return this.name
    },
    set(value) {
      this.$emit('update:name', value)
    }
  }
}
複製代碼

注意

@PropSync須要配合父組件的.sync修飾符使用

9. $emit

@Emit(event?: string)

  • 接受一個參數 event?: string, 若是沒有的話會自動將 camelCase 轉爲 dash-case 做爲事件名.
  • 會將函數的返回值做爲回調函數的第二個參數, 若是是 Promise 對象,則回調函數會等 Promise resolve 掉以後觸發.
  • 若是$emit 還有別的參數, 好比點擊事件的 event , 會在返回值以後, 也就是第三個參數.
import { Vue, Component, Emit } from 'vue-property-decorator'

@Component
export default class MyComponent extends Vue {
  count = 0
  @Emit('reset')
  public resetCount() {
    this.count = 0
  }
  @Emit()
  public addToCount (n: number) {
    this.count += n
  }
  @Emit()
  public returnValue () {
    return 10
  }
  @Emit()
  public onInputChange (e:any) {
    return e.target.value
  }
  @Emit()
  public promise () {
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(20)
      }, 0)
    })
  }
}
複製代碼

等價於

export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    addToCount(n) {
      this.count += n
      this.$emit('add-to-count', n)
    },
    resetCount() {
      this.count = 0
      this.$emit('reset')
    },
    returnValue() {
      this.$emit('return-value', 10)
    },
    onInputChange(e) {
      this.$emit('on-input-change', e.target.value, e)
    },
    promise() {
      const promise = new Promise(resolve => {
        setTimeout(() => {
          resolve(20)
        }, 0)
      })
      promise.then(value => {
        this.$emit('promise', value)
      })
    }
  }
}
複製代碼

10. provide & inject

  1. @Provide(key?: string | symbol)

@Provide接收一個參數:

  • key, 值能夠爲String 或 symbol類型

若是爲了不命名衝突, 可使用 ES6 的 Symbol 特性做爲 key

  1. @Inject(options?: { from?: InjectKey, default?: any } | InjectKey) decorator

@Inject 裝飾器一個參數, 該參數有兩種要能:

  • 若爲String類型, 即爲接收(inject)的key名稱
  • 若爲對象, 則可能須要傳入兩個值:
    • from, 接收(inject)的key名稱
    • default, 若祖先沒有provide此key, 則使用默認值

示例:

import { Component, Inject, Provide, Vue } from 'vue-property-decorator'

const symbol = Symbol('baz')

@Component
export class MyComponent extends Vue {
  @Inject() readonly foo!: string
  @Inject('bar') readonly bar!: string
  @Inject({ from: 'optional', default: 'default' }) readonly optional!: string
  @Inject(symbol) readonly baz!: string

  @Provide() foo = 'foo'
  @Provide('bar') baz = 'bar'
}
複製代碼

等價於:

const symbol = Symbol('baz')

export const MyComponent = Vue.extend({
  inject: {
    foo: 'foo',
    bar: 'bar',
    optional: { from: 'optional', default: 'default' },
    [symbol]: symbol
  },
  data() {
    return {
      foo: 'foo',
      baz: 'bar'
    }
  },
  provide() {
    return {
      foo: this.foo,
      bar: this.baz
    }
  }
})
複製代碼

11. @ProvideReactive/@InjectReactive

顧名思義就是響應式的注入, 會同步更新到子組件中. 好比下例能夠實如今 input 中的輸入實時注入到子組件中. 示例: Parent.vue

<template>
  <div>
    <input type="text" v-model="bar">
    <Child />
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, ProvideReactive } from 'vue-property-decorator'

import Child from './Child.vue'

@Component({
  components: {
    Child
  }
})
export default class Parent extends Vue {
  @ProvideReactive() private bar = 'deeper lorry'
}
</script>

複製代碼

Child.vue

<template>
  <div >
    InjectReactive: {{bar}}
  </div>
</template>
<script lang="ts">
import { Vue, Component, Prop, InjectReactive } from 'vue-property-decorator'

@Component
export default class Child extends Vue {
  @InjectReactive() private bar!: string
}
</script>
複製代碼

最終效果可能以下:

也能夠經過clone git庫 vue-typescript-skills, 運行本地服務後進入http://localhost:8080/#/provide, 看到效果.

12. ref

@Ref(refKey?: string)

@Ref裝飾器接收一個可選參數:

  • refKey, 值能夠爲String, 若是省略傳輸參數, 那麼會自動將屬性名做爲參數, 注意與@Emit的區別, @Emit在不傳參數的狀況下會轉爲 dash-case, 而 @Ref不會轉, 爲原屬性名
<template>
  <div>
    <span>Name:</span>
    <input type="text" v-model="value" ref='name' />
  </div>
</template>

<script lang="ts">
@Component
export default class RefComponent extends Vue {
  @Ref('name') readonly name!: string;
  value = 'lorry'
  mounted() {
    window.console.log(this.inputName); // <input type="text">
  }
}
</script>
複製代碼

等價於:

<template>
  <div>
    <span>Name:</span>
    <input type="text" v-model="value" ref='name' />
  </div>
</template>

<script lang="ts">
@Component
export default {
  data(){
      return {
          value: 'lorry'
      }
  },
  computed: {
    inputName(){
        return this.$refs.name
    }
  },
  mounted() {
    window.console.log(this.inputName); // <input type="text">
  }
}
</script>
複製代碼

13. directives

directives 具體的介紹能夠看 Vue 的官方介紹.

示例:

<template>
  <span v-demo:foo.a="1+1">test</span>
</template>

<script lang="ts">
@Component({
  directives: {
    demo: {
      bind(el:any, binding:any, vnode:any) {
        var s = JSON.stringify
        el.innerHTML =
          'name: '       + s(binding.name) + '<br>' +
          'value: '      + s(binding.value) + '<br>' +
          'expression: ' + s(binding.expression) + '<br>' +
          'argument: '   + s(binding.arg) + '<br>' +
          'modifiers: '  + s(binding.modifiers) + '<br>' +
          'vnode keys: ' + Object.keys(vnode).join(', ')
      },
    }
  },
})
export default class App extends Vue {}
</script>
複製代碼

總結

經過這幾天的嘗試和試驗, 整體來講, 有一點吸引力的, 畢竟vue的寫法也很隨意, 多加入一些強制性的校驗, 項目代碼的健壯性應該會加強很多. 後續會慢慢在項目中推行, 也會慢慢進入踩坑中, 後續再持續更新, 敬請關注!

相關連接

相關文章
相關標籤/搜索