TypeScript推出已經很長時間了,在Angular項目中開發比較普遍,隨着Vue 3.0的即將推出,TypeScript在Vue項目中使用也即將成為很大的趨勢,筆者也是最近才開始研究如何在Vue項目中使用TypeScript進行項目的開發。
准備
閱讀博客前希望讀者能夠掌握如下技能,文章中也會相對應的講解一些對於TypeScript基礎進行講解。本篇博客基於Vue cli 3.0實踐,若讀者使用的是Vue cli 2.0的話,需要對其webpack配置進行更改,本文不進行講解,請讀者自行百度,並進行更改。
Vue cli 3.0環境Vue基礎應用
讀者完全不用擔心不懂TypeScript基礎,本文會一一對其基礎進行簡單的講解。
TypeScript基礎
創建完項目之后,接下來就可以對項目進行開發,在使用TypeScript開發與使用JavaScript還是有很大的區別,打開HelloWorld.vue文件內容如下:
<template>
<div></div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
}
</script>
<style lang="scss">
</style>
讀者若經常開發Vue項目會發現與之間的.vue有了一些變化,主要是在<script>標簽部分。使用ES6開發項目時樣文件格式如下:
<template>
<div></div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped></style>
對比一下兩者之間還是有很大的區別的。原有寫法是直接使用export default導出一個對象,然而TypeScript而是使用class如果對React的同學應該很熟悉,有點類似於React創建的組件的寫法了。對於template的使用其實與之前的寫法是一樣的。唯一改變得就是對於<script>部分,如果想在項目中使用TypeScript需要在<script>添加標識:<script lang="ts">告知解析器需要使用TypeScript進行解析。
在Vue中使用TypeScript編寫項目,需要依賴於Vue提供的包vue-property-decorator以達到對TypeScript的支持。需要使用@修飾器其中Vue功能才能正常使用。無論在頁面還是組件時,一定要使用@Component對導出類進行修飾,否則無法正常使用某些功能(如:雙向綁定...)。接下來就來實現如何在模板中渲染數據,這里將不再使用data而是直接在class中寫入變量。
關於vue-property-decorator的用法會在下面詳細介紹。
<template>
<div>
<h1>{{message}}</h1>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
private message:string = "Hello,TypeScript";
}
</script>
仔細觀察一下message的聲明,與其之前使用data時已經完全不一樣了有沒有,除了沒有使用data以外,還有很大的區別:
修飾符 變量名:數據類型 = 數據
private message:string = "Hello,TypeScript";
修飾符
如果沒有接觸過強類型語言的話對於修飾符可能會感覺到一絲絲的陌生,在es6的class中已經有了修飾符的概念,在es6的class中只有一個修飾符static修飾符,表示該類的靜態方法,但是在TypeScript中添加了很多修飾符:
- public:所有定義成
public的屬性和方法都可以在任何地方進行訪問(默認值) - private:所有定義成
private的屬性和方法都只能在類定義內部進行訪問 - protected:多有定義成
protected的屬性和方法可以從類定義內部訪問,也可以從子類中訪問。 - readonly:
readonly關鍵字將屬性設置為只讀的。只讀屬性必須在聲明時或構造函數里被初始化。
修飾符不但可以修飾屬性,還可以用來修飾方法,若在寫入屬性和方法時沒有使用修飾符對其進行修飾的話默認則會是public。
class A {
public message1:string = "Aaron";
private message2:string = "Angie";
protected message3:string = "Tom";
readonly message4:string = "Jerry";
message5:string = "Sony";
}
class B extends A {
constructor(){
// 這里因為受修飾器影響無法讀取到`message2`
// 若寫入 message2 在編碼工具中則會顯示紅色波浪線提示錯誤
// Property 'message2' is private and only accessible within class 'A'.
let {message1,message3,message4,message5} = this;
console.log(message1,message3,message4,message5);
// 若去更改 message4 的數據則會拋出下面的錯誤
// Cannot assign to 'message4' because it is a read-only property.
// this.message4 = "Sun";
}
}
數據類型
在聲明變量或屬性時需要規定其對應的數據類型是什么,這樣一來就可以對其變量以及屬性進行更加嚴格的管理了。
TypeScript中提供了一下基本類型:
- string: 字符串
- number:數字
- boolean:布爾值
- array:數組
- enum:枚舉,個人理解枚舉類型並不陌生,它能夠給一系列數值集合提供友好的名稱,也就是說枚舉表示的是一個命名元素的集合
- any:任意類型
- void:沒有任何類型,通常用於函數沒有返回值時使用
// 定義number型變量
let test:number = 1;
// 定義number型數組
let arr:[]number = [];
let arr1:Array<number> = [];
// 定義對象數據,且對象只能有name字段
let a:{name:string}[] = [];
在進行變量或屬性聲明的時候,一旦使用了數據類型限定的話,如果試圖想要對其數據類型進行更改的話,就會提示一個錯誤,如果類型是多種情況,可以使用|進行分割。若不確定使用哪種類型則可以使用any。
let a:string = "";
// Type '1' is not assignable to type 'string'.
a = 1;
let b:(string|number) = "";
b = 1;
注意:在聲明變量時不一定要使用數據限定,如果沒有使用數據限定,TypeScript則會根據默認值進行數據類型推論作為其變量的數據類型。
let a = 1;
// Type '"Aaron"' is not assignable to type 'number'.
a = "Aaron";
函數
對數據已經有了一定了解,之后就是對於函數的說明,在項目開發過程中唯一不可缺少的就是函數,同樣的是在class中寫入的方法,同樣也需要使用修飾符,其用法與屬性一致。
在函數后面添加了void標識符,標識當前函數沒有函數返回值,可以根據當前函數的返回值,更改其返回類型。一旦規定了函數的返回值類型,就無法再返回其他的數據的類型,如果強行返回其他類型的話,則會拋出錯誤。
<template>
<div>
<h1>{{message}}</h1>
<el-button @click="clickMe"></el-button>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
private message:string = "Hello,TypeScript";
private clickMe():void{
alert("Aaron!")
// Type '"Angie"' is not assignable to type 'void'.
// return "";
}
}
</script>
有的時候函數會帶有一些參數,該如何去處理這些參數呢?同樣當在接收參數(形參)處同樣也要規定其數據類型,一旦寫入參數在調用該方法時就必須傳入該參數,某些參數若不是必填的,則要在定義其類型前使用?,這樣該參數就不是必填項,可以有可無,?與默認值不能共存只能使用一個,一旦使用默認值的話,改參數就是必定存在的了,不會出現不傳入的情況了。
const fn = (name:string,age?:number) => {
console.log(`my name is ${name}, age ${age || 18}`)
}
fn("Aaron",3); // my name is Aaron, age 3
fn("Angie"); // my name is Angie, age 18
fn(); // Expected 1-2 arguments, but got 0.
開發過程中難免會遇到一些特殊的函數,函數內部無法確定其參數個數,但是傳入的類型都是統一類型的,在JavaScript中提供了arguments屬性,對於TypeScript有其他處理方式:
const fn = (...foo:number):number => {
let res:number = 0;
// 如果不使用foo,可以替換成arguments也是一樣的
for(let i = 0;i<foo.length;i++){
res += foo[0];
}
return res;
}
fn(1,2,3,4,5,6,8,7,9) // 45
筆者也嘗試使用過arguments,但是如果使用arguments時會拋出一個錯誤Cannot find name 'arguments'.
函數的重載,在沒有接觸過強語言的話可能是很陌生的,什么是重載?重載就是函數或者方法有相同的名稱,但是參數列表不相同的情形,這樣的同名不同參數的函數或者方法之間,互相稱之為重載函數或者方法。(節選自百度百科)
function abs(name:string):string;
function abs(name:{name:string}):string;
function abs(name:string|{name:string}):string{
if(typeof name === "object"){
return name.name;
}
return name;
}
abs("Aaron");
abs({name:"Angie"});
上述中前兩個都屬於抽象函數,最后一個函數為對於抽象函數的實現,實現簽名必須兼容所有的重載簽名,總是在參數列表的最后,接受一個any類型或聯合類型的參數作為他的參數。如果沒有按照實現所傳入參數則會拋出錯誤。
泛型
泛型是程序設計語言的一種特性。允許程序員在強類型程序設計語言中編寫代碼時定義一些可變部分,那些部分在使用前必須作出指明。各種程序設計語言和其編譯器、運行環境對泛型的支持均不一樣。將類型參數化以達到代碼復用提高軟件開發工作效率的一種數據類型。泛型類是引用類型,是堆對象,主要是引入了類型參數這個概念。
筆者在最開始接觸泛型的時候也是一臉懵逼,因為一直都在聽后端的同學說,泛型什么什么的...一直都只是知道有泛型這個東西,但是泛型應該怎么用,想問卻又不敢...因為是不知道從何開始問起。
我對於泛型的理解就是去規定一種數據類型,無論是函數接收參數,還是返回結果,或者一種類型的數組,對象,等等等都可以使用泛型進行約束,泛型就是在定義方法不確定需要返回什么樣類型的數值,但是當調用函數值時需要去限定返回該類型。
使用泛型時分為兩種情況(只會提及TypeScript的泛型),一種是接口泛型,一種是類泛型其實兩種方法是類似的。
// 類泛型
class User {
name:string = "";
age:number = 0;
}
// 接口泛型
interface User {
name:string,
age:number
}
// 獲取數據
function getData(){
// ... axios操作等
return [{}];
};
// T 泛型的形參
// T 可以自定義
function getUsers<T> ():T[]{
return getData();
}
let users:User[] = getUsers<User>();
上面代碼中使用class和接口實現了兩種泛型,兩種都是可用的,一般不推薦使用去使用泛型,因為在使用類去做泛型的時候需要對其中的屬性進行初始化,否則會拋出錯誤。
接口
上面提到了接口,對於接口也是前端沒有涉及的一部分,接口泛指實體把自己提供給外界的一種抽象化物(可以為另一實體),用以由內部操作分離出外部溝通方法,使其能被內部修改而不影響外界其他實體與其交互的方式。
對於接口來說,去定義一些抽象的方法或屬性,在使用時對其進行實現,使用implements關鍵字對其接口進行實現,可以同時實現多個接口以,分割。
接口可以繼承類,但是類不可以繼承接口,只能實現接口,如果接口繼承類的話,不會繼承類的實現只會繼承類的簽名規范。
class Friend {
public friends:object[] = [];
getFriends(){
return this.friends;
}
}
interface U extends Friend {
name:string,
age:number,
seyHi(name:string):string
}
interface S {
job:string
}
class User implements U,S {
public friends:object[] = [];
constructor(
public name:string,
public age:number,
public job:string){}
seyHi(name:string):string{
return `Hi ${name} ~,my name is ${this.name}`;
}
getFriends(){
return this.friends;
}
}
new User("Aaron",3,"Code");
類
有關於TypeScript的類,與es6中的類是一樣的,這里着重說一下抽象類。抽象類是對其屬性方法進行規定,在繼承時對其進行實現。
abstract class Animal{
public name:string;
constructor(name:string){
this.name=name;
}
// 抽象方法 ,不包含具體實現,要求子類中必須實現此方法
abstract eat():any;
// 非抽象方法,無需要求子類實現、重寫
run(){
console.log('非抽象方法,不要子類實現、重寫');
}
}
class Dog extends Animal{
// 子類中必須實現父類抽象方法,否則ts編譯報錯
eat(){
return this.name+"吃肉";
}
}
在類中需要對其內部的屬性進行初始化賦值,可以寫入默認值,同樣也可以根據傳入的值進行賦值,也就是在進行實例化的時候傳入參數對其屬性進行初始化。以下三種方法都可以為類中的屬性進行初始化。
// 第一種方法
class A {
public name:string = "Aaron";
}
// 第二種方法
class A {
public name:string;
constructor(name:string){
this.name = name;
}
}
// 第三種方法
class A {
constructor(public name:string){}
}
Vue中使用TypeScript
上面已經對TypeScript的基礎做了一些簡單的講解,在開發中是沒有問題的了,接下來就開始在Vue項目中開始實戰。
對於創建項目就不做過多贅述了,只需要在創建項目時選中TypeScript即可,其他配置項讀者可以根據項目需求自行選擇。
當Vue中使用TypeScript編寫項目以后,很多東西發生了變化,上面已經提到了事件與數據,對一些常用的方法進行簡單的說明。
組件傳值
父組件傳入子組件的值通過vue-property-decorator中Prop進行接收,傳入的方式與Vue中的使用是相同的。
import { Component, Prop, Vue } from 'vue-property-decorator';
@Component
export default class HelloWorld extends Vue {
@Prop() private msg!: string;
}
組件掛載
組件掛載則是在Component中進行掛載,除了位置發生了變化,其他並沒有任何不同。
import {Component, Vue } from 'vue-property-decorator';
import Search from '@/components/business/Search.vue';
@Component({
components: {
Search
}
})
export default class CreateItem extends Vue {}
通過上述代碼已經可以正常使用組件了<Search/>。
事件消息
事件消息使用Emit進行返回事件
注意:
- 當使用
Emit時執行事件函數所返回的值,則作為在函數中傳給父組件的值。 - Emit中可以接收參數,第一個參數可以自定義事件名稱,參數什么樣,在父組件綁定的事件名稱必須一致,否則事件不會執行,如果不傳入事件名稱,則會默認使用子組件中觸發事件的函數名稱,在父組件調用時則使用
烤串形式進行拼接。
import {Component, Vue, Ref, Emit} from 'vue-property-decorator';
@Component
export default class CreateItem extends Vue {
@Emit("clickMe") // 執行事件 @clickMe
private async onClickMe():string{
return "onClick";
}
@Emit // 執行事件 @on-click-me
private async onClickMe():string{
return "onClick";
}
}
Ref
<template>
<div>
<button ref="btn"><button/>
</div>
</template>
<script lang="ts">
import {Component, Vue, Ref} from 'vue-property-decorator';
@Component
export default class CreateItem extends Vue {
// 定義類型可以根據當前為什么元素寫入對應的名稱的element
// 如果不確定元素可以直接寫 HTMLElement
@Ref("btn") readonly formEle!:HTMLButtonElement;
};
</script>
mixins
對於混入的話有兩種混入方法,可以依賴於vue-class-component模塊中的混入,也可以在Component中進行混入。
第一種方法:
// 混入文件
import { Vue, Component} from 'vue-property-decorator';
@Component
export default class myMixins extends Vue {
values: string = 'Hello'
created() {
console.log("混入第二種方法!!!")
}
}
// 混入
import HomeMixins1 from "@/mixins/Home1";
@Component({
mixins:[HomeMixins1]
})
export default class Home extends Vue {
}
第二種方法:
// 混入文件
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default class HomeMixin extends Vue {
valueq: string = "Hello"
created() {
console.log("混入第一種方法!!")
}
}
// 混入
import Component,{mixins} from 'vue-class-component';
import HomeMixins from "@/mixins/Home";
export default class Home extends mixins(HomeMixins) {}
兩種混入方法都是可行的,同樣都可以混入多種方法,官網推薦使用第二種方法進行混入。
過濾器
過濾器分文兩種,一種全局過濾器,一種局部過濾器,對於全局過濾器幾乎沒有發生變化。
全局過濾器
全局過濾器寫在main.ts中
Vue.filter('capitalize', function (value:string) {
if (!value) return ''
return value.charAt(0).toUpperCase() + value.slice(1)
})
局部過濾器
局部過濾器寫在各個組件@Component中。
import {Component, Vue} from 'vue-property-decorator';
@Component({
filters:{
thumb(url:string){
return url += "/abcd";
}
}
})
export default class Home extends Vue {}
項目結構
簡單說一下筆者在使用Vue開發項目式使用的項目結構,可能不是最好的,但是對於項目的可維護性確實有了很大的提高。在項目開始時需要創建項目結構,Vue項目中為了更好的結構化,需要將項目進行分層處理,以達到高度維護的目的。
目錄結構:
├─api // 數據請求
├─assets // 資源
├─components // 組件
│ ├─basis // 基礎組件
│ └─business // 業務組件
├─domain // 業務
├─interface // 接口
├─middleware // 中間件
├─mixins // 混入
├─style // 樣式
├─store // 狀態管理
├─router // 路由
└─views // 視圖
上面對其項目進行了項目結構進行了划分,若對工程化不太了解的同學可能不太能理解每一層的目的到底是為了什么?他們相互之間又應該如何搭配使用?簡單對每一層進行簡單的描述。
- api:其中封裝的是請求后端接口數據的請求函數
- assets:靜態資源
- components:組件,在文件中分為了兩個文件夾,業務組件和基礎組件
- domain:項目中的業務邏輯
- interface:用戶TypeScript限制接口數據格式
- middleware:Vue項目中使用的中間件
- mixins:需要混入的內容
- style:樣式
- store:全局狀態管理
- router:路由
這里需要說明一點,在創建項目時如果讀者選擇了vue-router和vuex,創建項目后會自動生成router.ts和store.ts筆者這里並沒有使用原有的文件,而是單獨對其進行處理。考慮到若項目業務較多可以使用使用文件或文件夾再對其路由和狀態管理根據其業務對其進行文件划分,單獨維護,這樣更加的有利於項目的可維護性。
總結
文章篇幅過長,用了過長的篇幅講解了TypeScript簡單應用,對於Vue中的使用也只是簡單的講解了一下。與其之前是很類似的。
文章些許潦草,但是很感謝大家能夠堅持讀完本篇文章。若文章中出現錯誤請大家在評論區提出,我會盡快做出改正。
