小邵教你玩轉Typescript、ts版React全家桶腳手架

邵威儒發表於2018-12-07

前言:大家好,我叫邵威儒,大家都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程式猿的坑,從大學買的第一本vb和自學vb,我就與程式設計結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟體,至今進入了前端領域,看到不少朋友都寫文章分享,自己也弄一個玩玩,以下文章純屬個人理解,便於記錄學習,肯定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享個人對技術的通俗理解,共同成長!

後續我會陸陸續續更新javascript方面,儘量把javascript這個學習路徑體系都寫一下
包括前端所常用的es6、angular、react、vue、nodejs、koa、express、公眾號等等
都會從淺到深,從入門開始逐步寫,希望能讓大家有所收穫,也希望大家關注我~

文章列表:juejin.im/user/5a84f8…

Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/


在本文我將和大家一起玩轉Typescript,目前angular、deno已經開始使用typescript,並且我們熟知的vue,在3.0也即將會使用typescript,可以說,前端領域,typescript會逐漸變為必備的技能,那麼,為什麼typescript變得越來越火呢?

網上有各種typescript和javascript的對比,那麼在我的角度的理解,javascript是解釋型(動態)語言,可以說是從上到下執行,在我們開發過程中,比如有語法錯誤等等,需要執行到這一行程式碼才能知道,而typescript則像寫易語言那樣生成exe時,需要靜態編譯,而靜態編譯這個過程,會把程式碼都檢查一遍,看是否通過檢測,最終才生成exe,typescript最終是也是編譯成javascript原生程式碼的,只是在這個生成過程中,會進行各種檢測,來檢查程式碼是否符合語法啊規則啊,符合的話最終再編譯成javascript,規範了我們程式碼的編寫,同時也提高了程式碼的複用以及元件化,在runtime階段為我們提前找到錯誤。

小邵教你玩轉Typescript、ts版React全家桶腳手架

typescript支援es5/es6的語法,並且擴充套件了javascript語法,更像java、c#、swift這種語言了。

在前端nodejs很火,但是為什麼在後端卻不火,很大程度也是因為nodejs也是解釋型(動態)語言,優勢就是解釋型語言比較靈活,但是缺點也很明顯,用node開發後臺程式,開發一直爽,重構火葬場=。=!一旦重構了,就會出現很多問題,像Java、c#這類語言,非常嚴謹,型別檢查等非常嚴謹,而javascript呢,一般是靠我們用肉眼去排查,很麻煩,typescript就是解決這一類問題的。

總而言之,typescript是未來的趨勢,也是谷歌推薦的框架,我也是剛學typescript,很多都是站在前輩的肩膀總結的,廢話不多說,我們開始進入正題吧!


一.typescript 安裝

首先我們全域性安裝

npm i typescript -g

全域性安裝完成後,我們新建一個hello.ts的ts檔案

// hello.ts內容
let a = "邵威儒"
複製程式碼

接下來我們在命令列輸入tsc hello.ts來編譯這個ts檔案,然後會在同級目錄生成一個編譯好了的hello.js檔案

// hello.js內容
var = "邵威儒"
複製程式碼

那麼我們每次都要輸tsc hello.ts命令來編譯,這樣很麻煩,能否讓它自動編譯?答案是可以的,我平時使用vscode來開發,需要配置一下vscode就可以。

首先我們在命令列執行tsc --init來生成配置檔案,然後我們在目錄下看到生成了一個tsconfig.json檔案

小邵教你玩轉Typescript、ts版React全家桶腳手架

這個json檔案裡有很多選項

  • target是選擇編譯到什麼語法
  • module則是模組型別
  • outDir則是輸出目錄,可以指定這個引數到指定目錄

接下來我們需要開啟監控了,在vscode工作列中

小邵教你玩轉Typescript、ts版React全家桶腳手架

小邵教你玩轉Typescript、ts版React全家桶腳手架

小邵教你玩轉Typescript、ts版React全家桶腳手架

此時就會開啟監控了,會監聽ts的變化,然後自動去編譯。


二、資料型別

java、c#是強型別語言,而js是弱型別語言,強弱類語言有什麼區別呢?typescript最大的優點就是型別檢查,可以幫你檢查你定義的型別和賦值的型別。

布林型別boolean

// 在js中,定義isFlag為true,為布林型別boolean
let isFlag = true;
// 但是我們也可以重新給它賦值為字串
isFlag = "hello swr";

// 在ts中,定義isFlag為true,為布林型別boolean
// 在變數名後加冒號和型別,如  :boolean
let isFlag:boolean = true
// 重新賦值到字串型別會報錯
isFlag = "hello swr" 

// 在java中,一般是這樣定義,要寫變數名也要寫型別名
// int a = 10; 
// string name = "邵威儒"
複製程式碼

數字型別number

let age:number = 28;
age = 29;
複製程式碼

字串型別string

let name:string = "邵威儒"
name = "iamswr"
複製程式碼

以上boolean、number、string型別有個共性,就是可以通過typeof來獲取到是什麼型別,是基本資料型別。

那麼複雜的資料型別是怎麼處理的呢?

陣列 Array

// 在js中
let pets = ["旺財","小黑"];

// 在ts中
// 需要注意的是,這個是一個字串型別的陣列
// 只能往裡面寫字串,寫別的型別會報錯
let pets:string[] = ["旺財","小黑"];

// 另外一種ts寫法
let pets:Array<string> = ["旺財","小黑"];

// 那麼如果想在陣列裡放物件呢?
let pets:Array<object> = [{name:"旺財"},{name:"小黑"}];

// 那麼怎樣在一個陣列中,隨意放string、number、boolean型別呢?
// 這裡的 | 相當於 或 的意思
let arr:Array<string|number|boolean> = ["hello swr",28];

// 想在陣列中放任意型別
let arr:Array<any> = ["hello swr",28,true]
複製程式碼

元組型別tuple

什麼是元組型別?其實元組是陣列的一種。

let person:[string,number] = ['邵威儒',28]
複製程式碼

有點類似解構賦值,但是又不完全是解構賦值,比如元組型別必須一一對應上,多了少了或者型別不對都會報錯。

元組型別是一個不可變的陣列,長度、型別是不可變的。

列舉型別enum

列舉在java中是從6.0才引入的一種型別,在java和ts中的關鍵字都是enum

什麼是列舉?列舉有點類似一一列舉,一個一個數出來,在易語言中,我們會經常列舉視窗,來找到自己想要的,一般用於值是某幾個固定的值,比如生肖(有12種)、星座(有12種)、性別(男女)等,這些值是固定的,可以一個一個數出來。

為什麼我們要用列舉呢?我們可以定義一些值,定義完了後可以直接拿來用了,用的時候也不會賦錯值。

比如我們普通賦值

// 我們給性別賦值一個boy,但是我們有時候手誤,可能輸成boy1、boy2了
// 這樣就會導致我們賦值錯誤了
let sex = "boy"
複製程式碼

既然這樣容易導致手誤賦錯值,那麼我們可以定義一個列舉

// 定義一個列舉型別的值
enum sex {
  BOY,
  GIRL
}
console.log(sex)
console.log(`邵威儒是${sex.BOY}`)
複製程式碼

我們看看轉為es5語法是怎樣的

// 轉為es5語法
"use strict";
var sex;
(function (sex) {
    sex[sex["BOY"] = 0] = "BOY";
    sex[sex["GIRL"] = 1] = "GIRL";
})(sex || (sex = {}));
console.log(sex); // 列印輸出{ '0': 'BOY', '1': 'GIRL', BOY: 0, GIRL: 1 }
console.log("\u90B5\u5A01\u5112\u662F" + sex.BOY); // 列印輸出 邵威儒是0
複製程式碼

是不是感覺有點像給物件新增各種屬性,然後這個屬性又有點像常量,然後通過物件去取這個屬性?
上面這樣寫,不是很友好,那麼我們還可以給BOY GIRL賦值

enum sex{
    BOY="男",
    GIRL="女"
}
複製程式碼
// 轉化為es5語法
// 我們順便看看實現的原理
"use strict";
var sex;
// 首先這裡是一個自執行函式
// 並且把sex定義為物件,傳參進給自執行函式
// 然後給sex物件新增屬性並且賦值
(function (sex) {
    sex["BOY"] = "\u7537";
    sex["GIRL"] = "\u5973";
})(sex || (sex = {}));
console.log(sex); // 列印輸出 { BOY: '男', GIRL: '女' }
console.log("\u90B5\u5A01\u5112\u662F" + sex.BOY); // 列印輸出 邵威儒是男
複製程式碼

比如我們實際專案中,特別是商城類,訂單會存在很多狀態流轉,那麼非常適合用列舉

enum orderStatus {
    WAIT_FOR_PAY = "待支付",
    UNDELIVERED = "完成支付,待發貨",
    DELIVERED = "已發貨",
    COMPLETED = "已確認收貨"
}
複製程式碼

到這裡,我們會有一個疑慮,為什麼我們不這樣寫呢?

let orderStatus2 = {
    WAIT_FOR_PAY : "待支付",
    ...
}
複製程式碼

如果我們直接寫物件的鍵值對方式,是可以在外部修改這個值的,而我們通過enum則不能修改定義好的值了,更加嚴謹。

任意型別 any

any有好處也有壞處,特別是前端,很多時候寫型別的時候,幾乎分不清楚型別,任意去寫,寫起來很爽,但是對於後續的重構、迭代等是非常不友好的,會暴露出很多問題,某種程度來說,any型別就是放棄了型別檢查了。。。

比如我們有這樣一個場景,就是需要獲取某一個dom節點

let btn = document.getElementById('btn');
btn.style.color = "blue";
複製程式碼

此時我們發現在ts中會報錯

小邵教你玩轉Typescript、ts版React全家桶腳手架

因為我們取這個dom節點,有可能取到,也有可能沒取到,當沒取到的時候,相當於是null,是沒有style這個屬性的。

那麼我們可以給它新增一個型別為any

// 新增一個any型別,此時就不會報錯了,但是也相當於放棄了型別檢查了
let btn:any = document.getElementById('btn');
btn.style.color = "blue";

// 當然也有粗暴一些的方式,利用 ! 強制斷言
let btn = document.getElementById("btn");
btn!.style!.color = "blue";

// 可以賦值任何型別的值
// 跟以前我們var let宣告的一模一樣的
let person:any = "邵威儒"
person = 28
複製程式碼

null undefined型別

這個也沒什麼好說的,不過可以看下下面的例子

// (string | number | null | undefined) 相當於這幾種型別
// 是 string 或 number 或 null 或 undefined
let str:(string | number | null | undefined)
str = "hello swr"
str = 28
str = null
str = undefined
複製程式碼

void型別

void表示沒有任何型別,一般是定義函式沒有返回值。

// ts寫法
function say(name:string):void {
  console.log("hello",name)
}
say("swr")
複製程式碼
// 轉為es5
"use strict";
function say(name) {
    console.log("hello", name);
}
say("swr");
複製程式碼

怎麼理解叫沒有返回值呢?此時我們給函式return一個值

function say(name:string):void {
  console.log("hello",name)
  // return "ok" 會報錯
  return "ok"
  // return undefined 不會報錯
  // return 不會報錯
}
say("swr")
複製程式碼

那麼此時我們希望這個函式返回一個字串型別怎麼辦?

function say(name:string):string {
  console.log("hello",name)
  return "ok"
}
say("swr")
複製程式碼

never型別

這個用得很少,一般是用於丟擲異常。

let xx:never;
function error(message: string): never {
  throw new Error(message);
}

error("error")
複製程式碼

我們要搞明白any、never、void

  • any是任意的值
  • void是不能有任何值
  • never永遠不會有返回值

any比較好理解,就是任何值都可以

let str:any = "hello swr"
str = 28
str = true
複製程式碼

void不能有任何值(返回值)

function say():void {
  
}
複製程式碼

never則不好理解,什麼叫永遠不會有返回值?

// 除了上面舉例的丟擲異常以外,我們看一下這個例子
// 這個loop函式,一旦開始執行,就永遠不會結束
// 可以看出在while中,是死迴圈,永遠都不會有返回值,包括undefined

function loop():never {
    while(true){
        console.log("陷入死迴圈啦")
    }
}

loop()

// 包括比如JSON.parse也是使用這種 never | any
function parse(str:string):(never | any){
    return JSON.parse(str)
}
// 首先在正常情況下,我們傳一個JSON格式的字串,是可以正常得到一個JSON物件的
let json = parse('{"name":"邵威儒"}')
// 但是有時候,傳進去的不一定是JSON格式的字串,那麼就會丟擲異常
// 此時就需要never了
let json = parse("iamswr")
複製程式碼

也就是說,當一個函式執行的時候,被丟擲異常打斷了,導致沒有返回值或者該函式是一個死迴圈,永遠沒有返回值,這樣叫做永遠不會有返回值。

實際開發中,是never和聯合型別來一起用,比如

function say():(never | string) {
  return "ok"
}
複製程式碼

三.函式

函式是這樣定義的

function say(name:string):void {
  console.log("hello",name)
}
say("邵威儒")
複製程式碼

形參和實參要完全一樣,如想不一樣,則需要配置可選引數,可選引數放在後面

// 形參和實參一一對應,完全一樣
function say(name:string,age:number):void {
  console.log("hello",name,age)
}
say("邵威儒",28)
複製程式碼
// 可選引數,用 ? 處理,只能放在後面
function say(name:string,age?:number):void {
  console.log("hello",name,age)
}
say("邵威儒")
複製程式碼

那麼如何設定預設引數呢?

// 在js中我們是這樣寫的
function ajax(url,method="get"){
    console.log(url,method)
}

// 在ts中我們是這樣寫的
function ajax(url:string,method:string = "GET") {
  console.log(url,method)
}
複製程式碼

那麼如何設定剩餘引數呢?可以利用擴充套件運算子

function sum(...args:Array<number>):number {
  return eval(args.join("+"))
}
let total:number = sum(1,2,3,4,5)
console.log(total)
複製程式碼

那麼如何實現函式過載呢?函式過載是java中非常有名的,在java中函式的過載,是指兩個或者兩個以上的同名函式,引數的個數和型別不一樣

// 比如說我們現在有2個同名函式
function say(name:string){
    
}
function say(name:string,age:number){
    
}
// 那麼我想達到一個效果
// 當我傳引數name時,執行name:string這個函式
// 當我傳引數name和age時,執行name:string,age:number這個函式
// 此時該怎麼辦?
複製程式碼

接下來看一下typescript中的函式過載

// 首先宣告兩個函式名一樣的函式
function say(val: string): void; // 函式的宣告
function say(val: number): void; // 函式的宣告
// 函式的實現,注意是在這裡是有函式體的
// 其實下面的say()無論怎麼執行,實際上就是執行下面的函式
function say(val: any):void {
  console.log(val)
}

say("hello swr")
say(28)
複製程式碼

在typescript中主要體現是同一個同名函式提供多個函式型別定義,函式實際上就只有一個,就是擁有函式體那個,如果想根據傳入值型別的不一樣執行不同邏輯,則需要在這個函式裡面進行一個型別判斷。

那麼這個函式過載有什麼作用呢?其實在ts中,函式過載只是用來限制引數的個數和型別,用來檢查型別的,而且過載不能拆開幾個函式,這一點和java的處理是不一樣的,需要注意。


四、類

如何定義一個類?

// ts寫法
// 其實跟es6非常像,沒太大的區別
class Person{
  // 這裡宣告的變數,是例項上的屬性
  name:string
  age:number
  constructor(name:string,age:number){
    // this.name和this.age必須在前面先宣告好型別
    // name:string   age:number
    this.name = name
    this.age = age
  }
  // 原型方法
  say():string{
    return "hello swr"
  }
}

let p = new Person("邵威儒",28)
複製程式碼
// 那麼轉為es5呢?
"use strict";
var Person = /** @class */ (function () {
    function Person(name, age) {
        this.name = name;
        this.age = age;
    }
    Person.prototype.say = function () {
        return "hello swr";
    };
    return Person;
}());
var p = new Person("邵威儒", 28);
複製程式碼

可以發現,其實跟我們es6的class是非常像的,那麼類的繼承是怎樣實現呢?

// 類的繼承和es6也是差不多
class Parent{
  // 這裡宣告的變數,是例項上的屬性
  name:string
  age:number
  constructor(name:string,age:number){
    // this.name和this.age必須在前面先宣告好型別
    // name:string   age:number
    this.name = name
    this.age = age
  }
  // 原型方法
  say():string{
    return "hello swr"
  }
}

class Child extends Parent{
  childName:string
  constructor(name:string,age:number,childName:string){
    super(name,age)
    this.childName = childName
  }
  childSay():string{
    return this.childName
  }
}

let child = new Child("邵威儒",28,"bb")
console.log(child)
複製程式碼

類的修飾符

  • public公開的,可以供自己、子類以及其它類訪問
  • protected受保護的,可以供自己、子類訪問,但是其他就訪問不了
  • private私有的,只有自己訪問,而子類、其他都訪問不了
class Parent{
  public name:string
  protected age:number
  private money:number

  /**
   * 也可以簡寫為
   * constructor(public name:string,protected age:number,private money:number)
   */

  constructor(name:string,age:number,money:number){
    this.name = name
    this.age = age
    this.money = money
  }
  getName():string{
    return this.name
  }
  getAge():number{
    return this.age
  }
  getMoney():number{
    return this.money
  }
}

let p = new Parent("邵威儒",28,10)
console.log(p.name)
console.log(p.age) // 報錯
console.log(p.money) // 報錯
複製程式碼

靜態屬性、靜態方法,跟es6差不多

class Person{
    // 這是類的靜態屬性
    static name = "邵威儒"
    // 這是類的靜態方法,需要通過這個類去呼叫
    static say(){
        console.log("hello swr")
    }
}
let p = new Person()
Person.say() // hello swr
p.say() // 報錯
複製程式碼

抽象類

抽象類和方法,有點類似抽取共性出來,但是又不是具體化,比如說,世界上的動物都需要吃東西,那麼會把吃東西這個行為,抽象出來。

如果子類繼承的是一個抽象類,子類必須實現父類裡的抽象方法,不然的話不能例項化,會報錯。

// 關鍵字 abstract 抽象的意思
// 首先定義個抽象類Animal
// Animal類有一個抽象方法eat
abstract class Animal{
    // 實際上是使用了public修飾符
    // 如果新增private修飾符則會報錯
    abstract eat():void;
}

// 需要注意的是,這個Animal類是不能例項化的
let animal = new Animal() // 報錯

// 抽象類的抽象方法,意思就是,需要在繼承這個抽象類的子類中
// 實現這個抽象方法,不然會報錯
// 報錯,因為在子類中沒有實現eat抽象方法
class Person extends Animal{
    eat1(){
        console.log("吃米飯")
    }
}

// Dog類繼承Animal類後並且實現了抽象方法eat,所以不會報錯
class Dog extends Animal{
    eat(){
        console.log("吃骨頭")
    }
}
複製程式碼

五、介面

這裡的介面,主要是一種規範,規範某些類必須遵守規範,和抽象類有點類似,但是不侷限於類,還有屬性、函式等。

首先我們看看介面是如何規範物件的

// 假設我需要獲取使用者資訊
// 我們通過這樣的方式,規範必須傳name和age的值
function getUserInfo(user:{name:string,age:number}){
    console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
複製程式碼

這樣看,還是挺完美的,那麼問題就出現了,如果我另外還有一個方法,也是需要這個規範呢?

function getUserInfo(user:{name:string,age:number}){
    console.log(`${user.name} ${user.age}`)
}
function getInfo(user:{name:string,age:number}){
    console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
getInfo({name:"iamswr",age:28})
複製程式碼

可以看出,函式getUserInfogetInfo都遵循同一個規範,那麼我們有辦法對這個規範複用嗎?

// 首先把需要複用的規範,寫到介面中 關鍵字 interface
interface infoInterface{
    name:string,
    age:number
}
// 然後把這個介面,替換到我們需要複用的地方
function getUserInfo(user:infoInterface){
    console.log(`${user.name} ${user.age}`)
}
function getInfo(user:infoInterface){
    console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28})
getInfo({name:"iamswr",age:28})
複製程式碼

那麼有些引數可傳可不傳,該怎麼處理呢?

interface infoInterface{
    name:string,
    age:number,
    city?:string // 該引數為可選引數
}
function getUserInfo(user:infoInterface){
    console.log(`${user.name} ${user.age} ${user.city}`)
}
function getInfo(user:infoInterface){
    console.log(`${user.name} ${user.age}`)
}
getUserInfo({name:"邵威儒",age:28,city:"深圳"})
getInfo({name:"iamswr",age:28})
複製程式碼

介面是如何規範函式的

// 對一個函式的引數和返回值進行規範
interface mytotal {
  // 左側是函式的引數,右側是函式的返回型別
  (a:number,b:number) : number
}

let total:mytotal = function (a:number,b:number):number {
  return a + b
}

console.log(total(10,20))
複製程式碼

介面是如何規範陣列的

interface userInterface {
  // index為陣列的索引,型別是number
  // 右邊是陣列裡為字串的陣列成員
  [index: number]: string
}
let arr: userInterface = ['邵威儒', 'iamswr'];
console.log(arr);
複製程式碼

介面是如何規範類的

這個比較重要,因為寫react的時候會經常使用到類

// 首先實現一個介面
interface Animal{
    // 這個類必須有name
    name:string,
    // 這個類必須有eat方法
    // 規定eat方法的引數型別以及返回值型別
    eat(any:string):void
}
// 關鍵字 implements 實現
// 因為介面是抽象的,需要通過子類去實現它
class Person implements Animal{
    name:string
    constructor(name:string){
        this.name = name
    }
    eat(any:string):void{
        console.log(`吃${any}`)
    }
}
複製程式碼

那麼如果想遵循多個介面呢?

interface Animal{
    name:string,
    eat(any:string):void
}
// 新增一個介面
interface Animal2{
    sleep():void
}
// 可以在implements後面通過逗號新增,和java是一樣的
// 一個類只能繼承一個父類,但是卻能遵循多個介面
class Person implements Animal,Animal2{
    name:string
    constructor(name:string){
        this.name = name
    }
    eat(any:string):void{
        console.log(`吃${any}`)
    }
    sleep(){
        console.log('睡覺')
    }
}
複製程式碼

介面可以繼承介面

interface Animal{
    name:string,
    eat(any:string):void
}
// 像類一樣,通過extends繼承
interface Animal2 extends Animal{
    sleep():void
}
// 因為Animal2類繼承了Animal
// 所以這裡遵循Animal2就相當於把Animal也繼承了
class Person implements Animal2{
    name:string
    constructor(name:string){
        this.name = name
    }
    eat(any:string):void{
        console.log(`吃${any}`)
    }
    sleep(){
        console.log('睡覺')
    }
}
複製程式碼

六、泛型

泛型可以支援不特定的資料型別,什麼叫不特定呢?比如我們有一個方法,裡面接收引數,但是引數型別我們是不知道,但是這個型別在方法裡面很多地方會用到,引數和返回值要保持一致性

// 假設我們有一個需求,我們不知道函式接收什麼型別的引數,也不知道返回值的型別
// 而我們又需要傳進去的引數型別和返回值的型別保持一致,那麼我們就需要用到泛型

// <T>的意思是泛型,即generic type
// 可以看出value的型別也為T,返回值的型別也為T
function deal<T>(value:T):T{
    return value
}
// 下面的<string>、<number>實際上用的時候再傳給上面的<T>
console.log(deal<string>("邵威儒"))
console.log(deal<number>(28))
複製程式碼

實際上,泛型用得還是比較少,主要是看類的泛型是如何使用的

class MyMath<T>{
  // 定義一個私有屬性
  private arr:T[] = []
  // 規定傳參型別
  add(value:T){
    this.arr.push(value)
  }
  // 規定返回值的型別
  max():T{
    return Math.max.apply(null,this.arr)
  }
}

// 這裡規定了型別為number
// 相當於把T都替換成number
let mymath = new MyMath<number>()
mymath.add(1)
mymath.add(2)
mymath.add(3)
console.log(mymath.max())

// 假設我們傳個字串呢?
// 則會報錯:型別“"邵威儒"”的引數不能賦給型別“number”的引數。
mymath.add("邵威儒")
複製程式碼

那麼我們會思考,有了介面為什麼還需要抽象類?

介面裡面只能放定義,抽象類裡面可以放普通類、普通類的方法、定義抽象的東西。

比如說,我們父類有10個方法,其中9個是實現過的方法,有1個是抽象的方法,那麼子類繼承過來,只需要實現這一個抽象的方法就可以了,但是介面的話,則是全是抽象的,子類都要實現這些方法,簡而言之,介面裡面不可以放實現,而抽象類可以放實現。


六、用Typescript版React全家桶腳手架,讓你更清楚如何在專案中使用ts

這部分程式碼我傳到了github地址 github.com/iamswr/ts_r… ,大家可以結合來看

我們用ts來搭建一下ts版的react版全家桶腳手架,接下來這部分需要webpack和react的相關基礎,我儘量把註釋寫全,最好結合git程式碼一起看或者跟著敲一遍,這樣更好理解~

首先,我們生成一個目錄ts_react_demo,輸入npm init -y初始化專案

小邵教你玩轉Typescript、ts版React全家桶腳手架

然後在專案裡我們需要一個.gitignore來忽略指定目錄不傳到git上

小邵教你玩轉Typescript、ts版React全家桶腳手架

進入.gitignore輸入我們需要忽略的目錄,一般是node_modules

// .gitignore
node_modules
複製程式碼

接下來我們準備下載相應的依賴包,這裡需要了解一個概念,就是型別定義檔案

------------------------插入開始-------------------------

型別定義檔案

因為目前主流的第三方庫都是以javascript編寫的,如果用typescript開發,會導致在編譯的時候會出現很多找不到型別的提示,那麼如果讓這些庫也能在ts中使用呢?

我們在ios開發的時候,會遇到swift、co混合開發,為了解決兩種語法混合開發,是通過一個.h格式的橋接頭來把兩者聯絡起來,在js和ts,也存在這樣的概念。

型別定義檔案(*.d.ts)就是能夠讓編輯器或者外掛來檢測到第三方庫中js的靜態型別,這個檔案是以.d.ts結尾。

比如說react的型別定義檔案:github.com/DefinitelyT…

在typescript2.0中,是使用@type來進行型別定義,當我們使用@type進行型別定義,typescript會預設檢視./node_modules/@types資料夾,可以通過這樣來安裝這個庫的定義庫npm install @types/react --save

------------------------插入結束-------------------------

接下來,我們需要下載相關依賴包,涉及到以下幾個包

------------------------安裝依賴包開始-------------------------

這部分程式碼已傳到 github.com/iamswr/ts_r… 分支:webpack_done

react相關

- react // react的核心檔案
- @types/react // 宣告檔案
- react-dom // react dom的操作包
- @types/react-dom 
- react-router-dom // react路由包
- @types/react-router-dom
- react-redux
- @types/react-redux
- redux-thunk  // 中介軟體
- @types/redux-logger
- redux-logger // 中介軟體
- connected-react-router
複製程式碼

執行安裝依賴包npm i react react-dom @types/react @types/react-dom react-router-dom @types/react-router-dom react-redux @types/react-redux redux-thunk redux-logger @types/redux-logger connected-react-router -S

小邵教你玩轉Typescript、ts版React全家桶腳手架

webpack相關

- webpack // webpack的核心包
- webpack-cli // webapck的工具包
- webpack-dev-server // webpack的開發服務
- html-webpack-plugin // webpack的外掛,可以生成index.html檔案
複製程式碼

執行安裝依賴包npm i webpack webpack-cli webpack-dev-server html-webpack-plugin -D,這裡的-D相當於--save-dev的縮寫,下載開發環境的依賴包

小邵教你玩轉Typescript、ts版React全家桶腳手架

typescript相關

- typescript // ts的核心包
- ts-loader // 把ts編譯成指定語法比如es5 es6等的工具,有了它,基本不需要babel了,因為它會把我們的程式碼編譯成es5
- source-map-loader // 用於開發環境中除錯ts程式碼
複製程式碼

執行安裝依賴包npm i typescript ts-loader source-map-loader -D

小邵教你玩轉Typescript、ts版React全家桶腳手架

從上面可以看出,基本都是模組和宣告檔案都是一對對出現的,有一些不是一對對出現,就是因為都整合到一起去了。

宣告檔案可以在node_modules/@types/xx/xx中找到。

------------------------安裝依賴包結束-------------------------

---------------------Typescript config配置開始----------------------

首先我們要生成一個tsconfig.json來告訴ts-loader怎樣去編譯這個ts程式碼

tsc --init
複製程式碼

會在專案中生成了一個tsconfig.json檔案,接下來進入這個檔案,來修改相關配置

// tsconfig.json
{
  // 編譯選項
  "compilerOptions": {
    "target": "es5", // 編譯成es5語法
    "module": "commonjs", // 模組的型別
    "outDir": "./dist", // 編譯後的檔案目錄
    "sourceMap": true, // 生成sourceMap方便我們在開發過程中除錯
    "noImplicitAny": true, // 每個變數都要標明型別
    "jsx": "react", // jsx的版本,使用這個就不需要額外使用babel了,會編譯成React.createElement
  },
  // 為了加快整個編譯過程,我們指定相應的路徑
  "include": [
    "./src/**/*"
  ]
}
複製程式碼

---------------------Typescript config配置結束----------------------

---------------------webpack配置開始----------------------

./src/下建立一個index.html檔案,並且新增<div id='app'></div>標籤

// ./src/index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id='app'></div>
</body>
</html>
複製程式碼

./下建立一個webpack配置檔案webpack.config.js

// ./webpack.config.js
// 引入webpack
const webpack = require("webpack");
// 引入webpack外掛 生成index.html檔案
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path")

// 把模組匯出
module.exports = {
  // 以前是jsx,因為我們用typescript寫,所以這裡字尾是tsx
  entry:"./src/index.tsx",
  // 指定模式為開發模式
  mode:"development",
  // 輸出配置
  output:{
    // 輸出目錄為當前目錄下的dist目錄
    path:path.resolve(__dirname,'dist'),
    // 輸出檔名
    filename:"index.js"
  },
  // 為了方便除錯,還要配置一下除錯工具
  devtool:"source-map",
  // 解析路徑,查詢模組的時候使用
  resolve:{
    // 一般寫模組不會寫字尾,在這裡配置好相應的字尾,那麼當我們不寫字尾時,會按照這個字尾優先查詢
    extensions:[".ts",'.tsx','.js','.json']
  },
  // 解析處理模組的轉化
  module:{
    // 遵循的規則
    rules:[
      {
        // 如果這個模組是.ts或者.tsx,則會使用ts-loader把程式碼轉成es5
        test:/\.tsx?$/,
        loader:"ts-loader"
      },
      {
        // 使用sourcemap除錯
        // enforce:pre表示這個loader要在別的loader執行前執行
        enforce:"pre",
        test:/\.js$/,
        loader:"source-map-loader"
      }
    ]
  },
  // 外掛的配置
  plugins:[
    // 這個外掛是生成index.html
    new HtmlWebpackPlugin({
      // 以哪個檔案為模板,模板路徑
      template:"./src/index.html",
      // 編譯後的檔名
      filename:"index.html"
    }),
    new webpack.HotModuleReplacementPlugin()
  ],
  // 開發環境服務配置
  devServer:{
    // 啟動熱更新,當模組、元件有變化,不會重新整理整個頁面,而是區域性重新整理
    // 需要和外掛webpack.HotModuleReplacementPlugin配合使用
    hot:true, 
    // 靜態資源目錄
    contentBase:path.resolve(__dirname,'dist')
  }
}
複製程式碼

那麼我們怎麼執行這個webpack.config.js呢?這就需要我們在package.json配置一下指令碼

package.json裡的script,新增builddev的配置

{
  "name": "ts_react_demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack",
    "dev":"webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@types/react": "^16.7.13",
    "@types/react-dom": "^16.0.11",
    "@types/react-redux": "^6.0.10",
    "@types/react-router-dom": "^4.3.1",
    "connected-react-router": "^5.0.1",
    "react": "^16.6.3",
    "react-dom": "^16.6.3",
    "react-redux": "^6.0.0",
    "react-router-dom": "^4.3.1",
    "redux-logger": "^3.0.6",
    "redux-thunk": "^2.3.0"
  },
  "devDependencies": {
    "html-webpack-plugin": "^3.2.0",
    "source-map-loader": "^0.2.4",
    "ts-loader": "^5.3.1",
    "typescript": "^3.2.1",
    "webpack": "^4.27.1",
    "webpack-cli": "^3.1.2",
    "webpack-dev-server": "^3.1.10"
  }
}
複製程式碼

因為入口檔案是index.tsx,那麼我們在./src/下建立一個index.tsx,並且在裡面寫入一段程式碼,看看webpack是否能夠正常編譯

因為我們在webpack.config.jsentry設定的入口檔案是index.tsx,並且在module中的rules會識別到.tsx格式的檔案,然後執行相應的ts-loader

// ./src/index.tsx
console.log("hello swr")
複製程式碼

接下來我們npm run build一下,看看能不能正常編譯

npm run build
複製程式碼

小邵教你玩轉Typescript、ts版React全家桶腳手架

嗯,很好,編譯成功了,我們可以看看./dist/下生成了index.html index.js index.js.map三個檔案

那麼我們在開發過程中,不會每次都npm run build來看修改的結果,那麼我們平時開發過程中可以使用npm run dev

npm run dev
複製程式碼

小邵教你玩轉Typescript、ts版React全家桶腳手架

這樣就啟動成功了一個http://localhost:8080/的服務了。

那麼我們如何配置我們的開發伺服器呢?

接下來我們修改webpack.config.js的配置,新增一個devServer配置物件,程式碼更新在上面webpack.config.js中,配置開發環境的服務以及熱更新。

接下來我們看看熱更新是否配置正常,在./src/index.tsx中新增一個console.log('hello 邵威儒'),我們發現瀏覽器的控制檯會自動列印出這一個輸出,說明配置正常了。

為了更好查閱程式碼,到目前這一步的程式碼已傳到 github.com/iamswr/ts_r… 分支為"webpack_done""

---------------------webpack配置結束----------------------

---------------------計數器元件開始----------------------

這部分程式碼已傳到 github.com/iamswr/ts_r… 分支:CounterComponent_1

接下來我們開始寫react,我們按照官方文件( redux.js.org/ ),寫一個計數器來演示。

首先我們在./src/下建立一個資料夾components,然後在./src/components/下建立檔案Counter.tsx

// ./src/components/Counter.tsx
// import React from "react"; // 之前的寫法
// 在ts中引入的寫法
import * as React from "react";

export default class CounterComponent extends React.Component{
  // 狀態state
  state = {
    number:0
  }
  render(){
    return(
      <div>
        <p>{this.state.number}</p>
        <button onClick={()=>this.setState({number:this.state.number + 1})}>+</button>
      </div>
    )
  }
}
複製程式碼

我們發現,其實除了引入import * as React from "react"以外,其餘的和之前的寫法沒什麼不同。

接下來我們到./src/index.tsx中把這個元件導進來

// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import CounterComponent from "./components/Counter";
// 把我們的CounterComponent元件渲染到id為app的標籤內
ReactDom.render(<CounterComponent />,document.getElementById("app"))
複製程式碼

這樣我們就把這個元件引進來了,接下來我們看下是否能夠成功跑起來

小邵教你玩轉Typescript、ts版React全家桶腳手架

到目前為止,感覺用ts寫react還是跟以前差不多,沒什麼區別,要記住,ts最大的特點就是型別檢查,可以檢驗屬性的狀態型別。

假設我們需要在./src/index.tsx中給<CounterComponent />傳一個屬性name,而CounterComponent元件需要對這個傳入的name進行型別校驗,比如說只允許傳字串。

./src/index.tsx中修改一下

ReactDom.render(<CounterComponent name="邵威儒" />,document.getElementById("app"))
複製程式碼

然後需要在./src/components/Counter.tsx中寫一個介面來對這個name進行型別校驗

// ./src/components/Counter.tsx
// import React from "react"; // 之前的寫法
// 在ts中引入的寫法
import * as React from "react";
// 寫一個介面對name進行型別校驗
// 如果我們不寫校驗的話,在外部傳name進來會報錯的
interface IProps{
  name:string,
}
// 我們還可以用介面約束state的狀態
interface IState{
  number: number
}
// 把介面約束的規則寫在這裡
// 如果傳入的name不符合型別會報錯
// 如果state的number屬性不符合型別也會報錯
export default class CounterComponent extends React.Component<IProps, IState>{
  // 狀態state
  state = {
    number:0
  }
  render(){
    return(
      <div>
        <p>{this.state.number}</p>
        <p>{this.props}</p>
        <button onClick={()=>this.setState({number:this.state.number + 1})}>+</button>
      </div>
    )
  }
}
複製程式碼

接下來看看如何在redux中使用ts呢?

---------------------計數器元件結束----------------------

---------------------Redux開始----------------------

這部分程式碼已傳到 github.com/iamswr/ts_r…
分支:redux_thunk

上面state中的number就不放在元件裡了,我們放到redux中,接下來我們使用redux。

首先在./src/建立store目錄,然後在./src/store/建立一個檔案index.tsx

// .src/store/index.tsx
import { createStore } from "redux";
// 引入reducers
import reducers from "./reducers";
// 接著建立倉庫
let store = createStore(reducers);
// 匯出store倉庫
export default store;
複製程式碼

然後我們需要建立一個reducers,在./src/store/建立一個目錄reducers,該目錄下再建立一個檔案index.tsx

但是我們還需要對reducers中的函式引數進行型別校驗,而且這個型別校驗很多地方需要複用,那麼我們需要把這個型別校驗單獨抽離出一個檔案。

那麼我們需要在./src/下建立一個types目錄,該目錄下建立一個檔案index.tsx

// ./src/types/index.tsx
// 匯出一個介面
export interface Store{
  // 我們需要約束的屬性和型別
  number:number
}
複製程式碼

回到./src/store/reducers/index.tsx

// ./src/store/reducers/index.tsx
// 匯入型別校驗的介面
// 用來約束state的
import { Store } from "../../types/index"
// 我們需要給number賦予預設值
let initState:Store = { number:0 }
// 把介面寫在state:Store
export default function (state:Store=initState,action) {
  // 拿到老的狀態state和新的狀態action
  // action是一個動作行為,而這個動作行為,在計數器中是具備 加 或 減 兩個功能
}
複製程式碼

上面這段程式碼暫時先這樣,因為需要用到action,我們現在去配置一下action相關的,首先我們在./src/store下建立一個actions目錄,並且在該目錄下建立檔案counter.tsx

因為配置./src/store/actions/counter.tsx會用到動作型別,而這個動作型別是屬於常量,為了更加規範我們的程式碼,我們在./src/store/下建立一個action-types.tsx,裡面寫相應常量

// ./src/store/action-types.tsx
export const ADD = "ADD";
export const SUBTRACT = "SUBTRACT";
複製程式碼

回到./src/store/actions/counter.tsx

// ./src/store/actions/counter.tsx
import * as types from "../action-types";
export default {
  add(){
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.ADD}
  },
  subtract(){
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.SUBTRACT}
  }
}
複製程式碼

我們可以想一下,上面return { type:types.ADD }實際上是返回一個action物件,將來使用的時候,是會傳到./src/store/reducers/index.tsxaction中,那麼我們怎麼定義這個action的結構呢?

// ./src/store/actions/counter.tsx
import * as types from "../action-types";
// 定義兩個介面,分別約束add和subtract的type型別
export interface Add{
  type:typeof types.ADD
}
export interface Subtract{
  type:typeof types.SUBTRACT
}
// 再匯出一個type
// type是用來給型別起別名的
// 這個actions裡是一個物件,會有很多函式,每個函式都會返回一個action
// 而 ./store/reducers/index.tsx中的action會是下面某一個函式的返回值

export type Action = Add | Subtract

// 把上面定義好的介面作用於下面
// 約束返回值的型別
export default {
  add():Add{
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.ADD}
  },
  subtract():Subtract{
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.SUBTRACT}
  }
}
複製程式碼

接著我們回到./store/reducers/index.tsx

經過上面一系列的配置,我們可以給action使用相應的介面約束了並且根據不同的action動作行為來進行不同的處理

// ./store/reducers/index.tsx
// 匯入型別校驗的介面
// 用來約束state的
import { Store } from "../../types/index"
// 匯入約束action的介面
import { Action } from "../actions/counter"
// 引入action動作行為的常量
import * as types from "../action-types"
// 我們需要給number賦予預設值
let initState:Store = { number:0 }
// 把介面寫在state:Store
export default function (state:Store=initState,action:Action) {
  // 拿到老的狀態state和新的狀態action
  // action是一個動作行為,而這個動作行為,在計數器中是具備 加 或 減 兩個功能
  // 判斷action的行為型別
  switch (action.type) {
    case types.ADD:
        // 當action動作行為是ADD的時候,給number加1
        return { number:state.number + 1 }
      break;
    case types.SUBTRACT:
        // 當action動作行為是SUBTRACT的時候,給number減1
        return { number:state.number - 1 }
      break;
    default:
        // 當沒有匹配到則返回原本的state
        return state
      break;
  }
}
複製程式碼

接下來,我們怎麼樣把元件和倉庫建立起關係呢?

首先進入./src/index.tsx

// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
// 引入redux這個庫的Provider元件
import { Provider } from "react-redux";
// 引入倉庫
import store from './store'
import CounterComponent from "./components/Counter";
// 用Provider包裹CounterComponent元件
// 並且把store傳給Provider
// 這樣Provider可以向它的子元件提供store
ReactDom.render((
  <Provider store={store}>
    <CounterComponent />
  </Provider>
),document.getElementById("app"))
複製程式碼

我們到元件內部建立連線,./src/components/Counter.tsx

// ./src/components/Counter.tsx
import * as React from "react";
// 引入connect,讓元件和倉庫建立連線
import { connect } from "react-redux";
// 引入actions,用於傳給connect
import actions from "../store/actions/counter";
// 引入介面約束
import { Store } from "../types";
// 介面約束
interface IProps{
  number:number,
  // add是一個函式
  add:any,
  // subtract是一個函式
  subtract:any
}
class CounterComponent extends React.Component<IProps>{
  render(){
    // 利用解構賦值取出
    // 這裡比如和IProps保持一致,不對應則會報錯,因為介面約束了必須這樣
    let { number,add,subtract } = this.props
    return(
      <div>
        <p>{number}</p><br/>
        <button onClick={add}>+</button><br />
        <button onClick={subtract}>-</button>        
      </div>
    )
  }
}
// 這個connect需要執行兩次,第二次需要我們把這個元件CounterComponent傳進去
// connect第一次執行,需要兩個引數,

// 需要傳給connect的函式
let mapStateToProps = function (state:Store) {
  return state
}

export default connect(
  mapStateToProps,
  actions
)(CounterComponent);
複製程式碼

接下來我們看下是否配置成功

小邵教你玩轉Typescript、ts版React全家桶腳手架

成功了,可以通過加減按鈕對number進行控制

其實搞來搞去,跟原來的寫法差不多,主要就是ts會進行型別檢查。

如果對number進行非同步修改,該怎麼處理?這就需要我們用到redux-thunk

接著我們回到./src/store/index.tsx

// ./src/store/index.tsx
// 需要使用到thunk,所以引入中介軟體applyMiddleware
import { createStore, applyMiddleware } from "redux";
// 引入reducers
import reducers from "./reducers";
// 引入redux-thunk,處理非同步
// 現在主流處理非同步的是saga和thunk
import thunk from "redux-thunk";
// 引入日誌
import logger from "redux-logger";
// 接著建立倉庫和中介軟體
let store = createStore(reducers, applyMiddleware(thunk,logger));
// 匯出store倉庫
export default store;
複製程式碼

接著我們回來./src/store/actions,新增一個非同步的動作行為

// ./src/store/actions
import * as types from "../action-types";
// 定義兩個介面,分別約束add和subtract的type型別
export interface Add{
  type:typeof types.ADD
}
export interface Subtract{
  type:typeof types.SUBTRACT
}
// 再匯出一個type
// type是用來給型別起別名的
// 這個actions裡是一個物件,會有很多函式,每個函式都會返回一個action
// 而 ./store/reducers/index.tsx中的action會是下面某一個函式的返回值

export type Action = Add | Subtract

// 把上面定義好的介面作用於下面
// 約束返回值的型別
export default {
  add():Add{
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.ADD}
  },
  subtract():Subtract{
    // 需要返回一個action物件
    // type為動作的型別
    return { type: types.SUBTRACT}
  },
  // 一秒後才執行這個行為
  addAsync():any{
    return function (dispatch:any,getState:any) {
      setTimeout(function(){
        // 當1秒過後,會執行dispatch,派發出去,然後改變倉庫的狀態
        dispatch({type:types.ADD})
      }, 1000);
    }
  }
}
複製程式碼

./src/components/Counter.tsx元件內,使用這個非同步

// ./src/components/Counter.tsx
import * as React from "react";
// 引入connect,讓元件和倉庫建立連線
import { connect } from "react-redux";
// 引入actions,用於傳給connect
import actions from "../store/actions/counter";
// 引入介面約束
import { Store } from "../types";
// 介面約束
interface IProps{
  number:number,
  // add是一個函式
  add:any,
  // subtract是一個函式
  subtract:any,
  addAsync:any
}
class CounterComponent extends React.Component<IProps>{
  render(){
    // 利用解構賦值取出
    // 這裡比如和IProps保持一致,不對應則會報錯,因為介面約束了必須這樣
    let { number, add, subtract, addAsync } = this.props
    return(
      <div>
        <p>{number}</p><br/>
        <button onClick={add}>+</button><br/>
        <button onClick={subtract}>-</button><br/>
        <button onClick={addAsync}>非同步加1</button>
      </div>
    )
  }
}
// 這個connect需要執行兩次,第二次需要我們把這個元件CounterComponent傳進去
// connect第一次執行,需要兩個引數,

// 需要傳給connect的函式
let mapStateToProps = function (state:Store) {
  return state
}

export default connect(
  mapStateToProps,
  actions
)(CounterComponent);
複製程式碼

接下來到瀏覽器看看能否成功

小邵教你玩轉Typescript、ts版React全家桶腳手架

完美~ 能夠正常執行

---------------------Redux結束----------------------

---------------------合併reducers開始----------------------

這部分程式碼已傳到 github.com/iamswr/ts_r…
分支:reducers_combineReducers

假如我們的專案裡面,有兩個計數器,而且它倆是完全沒有關係的,狀態也是完全獨立的,這個時候就需要用到合併reducers了。

下面這些步驟,其實有時間的話可以自己實現一次,因為在實現的過程中,你會發現,因為有了ts的型別校驗,我們在修改的過程中,會給我們非常明確的報錯,而不像以前,寫一段,執行一下,再看看哪裡報錯,而ts是直接在編輯器中就提示報錯了,讓我們可以非常舒服地去根據報錯和提示,去相應的地方修改。

首先我們把涉及到計數器元件的程式碼拷貝兩份,因為改動太多了,可以在git上看,改動後的目錄如圖

小邵教你玩轉Typescript、ts版React全家桶腳手架

首先我們新增action的動作行為型別,在./src/store/action-types.tsx

export const ADD = "ADD";
export const SUBTRACT = "SUBTRACT";
// 新增作為Counter2.tsx中的actions動作行為型別
export const ADD2 = "ADD2";
export const SUBTRACT2 = "SUBTRACT2";
複製程式碼

然後修改介面檔案,./src/types/index.tsx

// ./src/types/index.tsx
// 把Counter/Counter2元件彙總到一起
export interface Store {
  counter: Counter,
  counter2: Counter2
}
// 分別對應Counter元件
export interface Counter {
  number: number
}
// 分別對應Counter2元件
export interface Counter2 {
  number: number
}
複製程式碼

然後把./src/store/actions/counter.tsx檔案拷貝在當前目錄並且修改名稱為counter2.tsx

// ./src/store/actions/counter2.tsx
import * as types from "../action-types";
export interface Add{
  type:typeof types.ADD2
}
export interface Subtract{
  type:typeof types.SUBTRACT2
}

export type Action = Add | Subtract

export default {
  add():Add{
    return { type: types.ADD2}
  },
  subtract():Subtract{
    return { type: types.SUBTRACT2}
  },
  addAsync():any{
    return function (dispatch:any,getState:any) {
      setTimeout(function(){
        dispatch({type:types.ADD2})
      }, 1000);
    }
  }
}
複製程式碼

然後把./src/store/reduces/index.tsx拷貝並且改名為counter.tsxcounter2.tsx

counter.tsx

import { Counter } from "../../types"
import { Action } from "../actions/counter"
import * as types from "../action-types"
let initState: Counter = { number:0 }
export default function (state: Counter=initState,action:Action) {
  switch (action.type) {
    case types.ADD:
        return { number:state.number + 1 }
      break;
    case types.SUBTRACT:
        return { number:state.number - 1 }
      break;
    default:
        return state
      break;
  }
}
複製程式碼

counter2.tsx

import { Counter2 } from "../../types"
import { Action } from "../actions/counter2"
import * as types from "../action-types"
let initState:Counter2 = { number:0 }
export default function (state:Counter2=initState,action:Action) {
  switch (action.type) {
    case types.ADD2:
        return { number:state.number + 1 }
      break;
    case types.SUBTRACT2:
        return { number:state.number - 1 }
      break;
    default:
        return state
      break;
  }
}
複製程式碼

index.tsc

我們多個reducer是通過combineReducers方法,進行合併的,因為我們一個專案當中肯定是存在非常多個reducer,所以統一在這裡處理。

// 引入合併方法
import { combineReducers } from "redux";
// 引入需要合併的reducer
import counter from "./counter";
// 引入需要合併的reducer
import counter2 from "./counter2";
// 合併
let reducers = combineReducers({
  counter,
  counter2,
});
export default reducers;
複製程式碼

最後修改元件,進入./src/components/,其中

// ./src/components/Counter.tsx
import * as React from "react";
import { connect } from "react-redux";
import actions from "../store/actions/counter";
import { Store, Counter } from "../types";
interface IProps{
  number:number,
  add:any,
  subtract:any,
  addAsync:any
}
class CounterComponent extends React.Component<IProps>{
  render(){
    let { number, add, subtract, addAsync } = this.props
    return(
      <div>
        <p>{number}</p><br/>
        <button onClick={add}>+</button><br/>
        <button onClick={subtract}>-</button><br/>
        <button onClick={addAsync}>非同步加1</button>
      </div>
    )
  }
}

let mapStateToProps = function (state: Store): Counter {
  return state.counter;
}
export default connect(
  mapStateToProps,
  actions
)(CounterComponent);
複製程式碼
// ./src/components/Counter2.tsx
import * as React from "react";
// 引入connect,讓元件和倉庫建立連線
import { connect } from "react-redux";
// 引入actions,用於傳給connect
import actions from "../store/actions/counter2";
// 引入介面約束
import { Store, Counter2 } from "../types";
// 介面約束
interface IProps{
  number:number,
  // add是一個函式
  add:any,
  // subtract是一個函式
  subtract:any,
  addAsync:any
}

class CounterComponent1 extends React.Component<IProps>{
  render(){
    // 利用解構賦值取出
    // 這裡比如和IProps保持一致,不對應則會報錯,因為介面約束了必須這樣
    let { number, add, subtract, addAsync } = this.props
    return(
      <div>
        <p>{number}</p><br/>
        <button onClick={add}>+</button><br/>
        <button onClick={subtract}>-</button><br/>
        <button onClick={addAsync}>非同步加1</button>
      </div>
    )
  }
}
// 這個connect需要執行兩次,第二次需要我們把這個元件CounterComponent傳進去
// connect第一次執行,需要兩個引數,

// 需要傳給connect的函式
let mapStateToProps = function (state: Store): Counter2 {
  return state.counter2;
}

export default connect(
  mapStateToProps,
  actions
)(CounterComponent1);
複製程式碼

到目前為止,我們完成了reducers的合併了,那麼我們看看效果如何,首先我們給./src/index.tsc新增Counter2元件,這樣的目的是與Counter元件完全獨立,互不影響,但是又能夠最終合併到readucers

// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import { Provider } from "react-redux";
import store from './store'
import CounterComponent from "./components/Counter";
import CounterComponent2 from "./components/Counter2";
ReactDom.render((
  <Provider store={store}>
    <CounterComponent />
    <br/>
    <CounterComponent2 />
  </Provider>
),document.getElementById("app"))
複製程式碼

然後到瀏覽器看看效果~

小邵教你玩轉Typescript、ts版React全家桶腳手架

完美,這樣我們就處理完reducers的合併了,在這個過程中,通過ts的型別檢測,我不再像以前那樣,寫一段程式碼,執行看看是否報錯,再定位錯誤,而是根據ts在編輯器的報錯資訊,直接定位,修改,把錯誤扼殺在搖籃。

---------------------合併reducers結束----------------------

---------------------路由開始----------------------

這部分程式碼已傳到 github.com/iamswr/ts_r…
分支:HashRouter

首先進入./src/index.tsx匯入我們的路由所需要的依賴包

// ./src/index.tsx
import * as React from "react";
import * as ReactDom from "react-dom";
import { Provider } from "react-redux";
import store from './store'
// 引入路由
// 路由的容器:HashRouter as Router
// 路由的規格:Route
// Link元件
import { HashRouter as Router,Route,Link } from "react-router-dom"
import CounterComponent from "./components/Counter";
import CounterComponent2 from "./components/Counter2";
import Counter from "./components/Counter";

function Home() {
  return <div>home</div>
}

ReactDom.render((
  <Provider store={store}>
    {/* 路由元件 */}
    <Router>
      {/*  放兩個路由規則需要在外層套個React.Fragment */}
      <React.Fragment>
        {/* 增加導航 */}
        <ul>
          <li><Link to="/">Home</Link></li>
          <li><Link to="/counter">Counter</Link></li>
          <li><Link to="/counter2">Counter2</Link></li>
        </ul>
        {/* 當路徑為 / 時是home元件 */}
        {/* 為了避免home元件一直渲染,我們可以新增屬性exact */}
        <Route exact path="/" component={Home}/>
        <Route path="/counter" component={CounterComponent}/>
        <Route path="/counter2" component={CounterComponent2} />
      </React.Fragment>
    </Router>
  </Provider>
),document.getElementById("app"))
複製程式碼

接下來看看路由是否配置成功

小邵教你玩轉Typescript、ts版React全家桶腳手架

完美,成功了,也可以看出Counter Counter2元件是互相獨立的。

但是我們發現了一個問題,http://localhost:8080/#/counter中有個#的符號,非常不美觀,那麼我們如何變成http://localhost:8080/counter這樣呢?

這部分程式碼已傳到 github.com/iamswr/ts_r…
分支:connected-react-router

我們還是進入./src/index.tsx

import { HashRouter as Router,Route,Link } from "react-router-dom"中的HashRouter更改為BrowserRouter

再從瀏覽器訪問http://localhost:8080/再跳轉到http://localhost:8080/counter發現還是很完美

小邵教你玩轉Typescript、ts版React全家桶腳手架

但是有個很大的問題,就是我們直接訪問http://localhost:8080/counter會找不到路由

小邵教你玩轉Typescript、ts版React全家桶腳手架

這是怎麼回事?因為我們的是單頁面應用,不管路由怎麼變更,實際上都是訪問index.html這個檔案,所以當我們訪問根路徑的時候,能夠正常訪問,因為index.html檔案就放在這個目錄下,但是當我們通過非根路徑的路由訪問,則出錯了,是因為我們在相應的路徑沒有這個檔案,所以出錯了。

從這一點也可以衍生出一個實戰經驗,我們平時專案部署上線的時候,會出現這個問題,一般我們都是用ngxin來把訪問的路徑都是指向index.html檔案,這樣就能夠正常訪問了。

那麼針對目前我們這個情況,我們可以通過修改webpack配置,讓路由不管怎麼訪問,都是指向我們制定的index.html檔案。

進入./webpack.config.js,在devServer的配置物件下新增一些配置

// ./webpack.config.js
...

  // 開發環境服務配置
  devServer:{
    // 啟動熱更新,當模組、元件有變化,不會重新整理整個頁面,而是區域性重新整理
    // 需要和外掛webpack.HotModuleReplacementPlugin配合使用
    hot:true, 
    // 靜態資源目錄
    contentBase:path.resolve(__dirname,'dist'),
    // 不管訪問什麼路徑,都重定向到index.html
    historyApiFallback:{
      index:"./index.html"
    }
  }

...
複製程式碼

修改webpack配置需要重啟服務,然後重啟服務,看看瀏覽器能否正常訪問http://localhost:8080/counter

小邵教你玩轉Typescript、ts版React全家桶腳手架

完美,不管訪問什麼路徑,都能夠正常重定向到index.html

接下來,完美這個路由的路徑,如何同步到倉庫當中呢?

以前是用一個叫react-router-redux的庫,把路由和redux結合到一起的,react-router-redux挺好用的,但是這個庫不再維護了,被廢棄了,所以現在推薦使用connected-react-router這個庫,可以把路由狀態對映到倉庫當中。

首先我們在./src下建立檔案history.tsx

// ./src/history.tsx
// 引入一個基於html5 api的history的createBrowserHistory
import { createBrowserHistory } from "history";
// 建立一個history
let history = createBrowserHistory();
// 匯出
export default history;
複製程式碼

假設我有一個需求,就是我不通過Link跳轉頁面,而是通過程式設計式導航,觸發一個動作,然後這個動作會派發出去,而且把路由資訊放到redux中,供我以後檢視。

我們進入./src/store/reducers/index.tsx

// ./src/store/reducers/index.tsx
import { combineReducers } from "redux";
import counter from "./counter";
import counter2 from "./counter2";
// 引入connectRouter
import { connectRouter } from "connected-react-router";
import history from "../../history";

let reducers = combineReducers({
  counter,
  counter2,
  // 把history傳到connectRouter函式中
  router: connectRouter(history)
});
export default reducers;
複製程式碼

我們進入./src/store/index.tsx來新增中介軟體

// ./src/store/index.tsx

複製程式碼

我們進入./src/store/actions/counter.tsx加個goto方法用來跳轉。

// ./src/store/actions/counter.tsx

複製程式碼

我們進入./src/components/Counter.tsx中加個按鈕,當我點選按鈕的時候,會向倉庫派發action,倉庫的action裡有中介軟體,會把我們這個請求攔截到,然後跳轉。

// ./src/components/Counter.tsx
import * as React from "react";
import { connect } from "react-redux";
import actions from "../store/actions/counter";
import { Store, Counter } from "../types";
interface IProps{
  number:number,
  add:any,
  subtract:any,
  addAsync:any,
  goto:any
}
class CounterComponent extends React.Component<IProps>{
  render(){
    let { number, add, subtract, addAsync,goto } = this.props
    return(
      <div>
        <p>{number}</p><br/>
        <button onClick={add}>+</button><br/>
        <button onClick={subtract}>-</button><br/>
        <button onClick={addAsync}>非同步加1</button>
        {/* 增加一個按鈕,並且點選的時候執行goto方法實現跳轉 */}
        <button onClick={()=>goto('/counter2')}>跳轉到/counter2</button>
      </div>
    )
  }
}

let mapStateToProps = function (state: Store): Counter {
  return state.counter;
}
export default connect(
  mapStateToProps,
  actions
)(CounterComponent);
複製程式碼

---------------------路由結束----------------------

到此為止,用typesript把react全家桶簡單過了一遍,之所以寫typesript版react全家桶,是為了讓大家知道這個typesript在實際專案中,是怎麼使用的,但是涉及到各個檔案跳來跳去,有時候很簡單的幾句話可以帶過,但是為了大家明白,寫得也囉裡囉嗦的,剛開始使用typesript,感覺效率也沒怎麼提高,但是在慢慢使用當中,會發現,確實很多錯誤,能夠提前幫我們發現,這對以後專案的維護、重構顯得非常重要,否則將來專案大了,哪裡出現錯誤了,估計也需要排查非常久的時間,typesript將來或許會成為趨勢,作為前端,總要不斷學習的嘛

相關文章