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

NO IMAGE

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

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

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

Author: 邵威儒
Email: [email protected]
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將來或許會成為趨勢,作為前端,總要不斷學習的嘛

相關文章

你可能遺漏的JS知識點(二)

徹底弄懂JS原型與繼承

不會發布node包?進來看看

webpack4.x最詳細入門講解