react與typescript搭配幹活就是不累(譯)

下半身要幸福發表於2019-02-18

原文地址

作者:Ross Bulat

注:本文並非直譯

一份用Typescript開發react和redux應用的指南

typescript在增強react應用的穩定性,可讀性以及易管理性方面一直都處在非常重要的位置上,typescript為React和其他javascript前端框架逐步引入了更多的支援;從3.0版本和3.1版本之後顯著增強了許多功能。在過去,typescript整合到react應用中是一件很頭疼的任務,但在這篇文章中,我門就會去探討一種更為直截了當的方式讓typescript和react更輕鬆的融合。

Create React App現在已經內建支援了typescript,從react-scripts2.1開始,通過新增一個typescript選項,就可以在一個新專案中開啟typescript,在探索如何將types和interface整合到React的props和state之前,我們將會使用Create React App去構造一個基於react和typescript應用

用Create React App建立typescript專案

Create React App官網有一個專門的頁面用來介紹專案建立過程以及給現有的專案新增typescript支援的遷移步驟,建立一個新的app並開啟typescript,先要執行以下命令:

  yarn create react-app app_name --typescript
  
  #or
  
  npx create-react-app app_name --typescript
複製程式碼

基於Create React App模板建立的專案,針對javascript有幾個需要注意的變化

  • 現在一份配置了typescript編譯器選項的 tsconfig.json 檔案被引入了進來
  • .js檔案現在要統一變為以 .tsx 為字尾的檔案,這樣typescript編譯器就能夠在編譯時識別出所有的.tsx檔案
  • package.json包含了針對各種@types包的依賴,包括對node,jest,react和react-dom的支援,並且是開箱即用的
  • 一份叫做 react-app-env.d.ts的檔案會去引用 react-scripts的型別,通過yarn start啟動本地開發服務,這個檔案會自動生成。

yarn start執行階段將會編譯和執行你的應用,並且會生成一個和原有js版本應用相同的副本

在我們繼續往下展開之前,最好先停止開發服務,重新考慮一些針對Typescript和React的檢查工具。

下載tslint-react

引入檢查工具對於開發typescript和react應用來說非常有幫助,你可以根據提示去得到一個確切的型別,尤其是事件(events)型別,檢查工具有極其嚴格的預設配置,所以我們在安裝過程中忽略一些規則。

注意:這些檢查工具通過NPM或者Yarn進行安裝

全域性安裝typescript,tslint,tslint-react

  yarn global add tslint typescript tslint-react
複製程式碼

現在在你的專案目錄下,初始化tslint

  tslint --init
複製程式碼

上述命令會生成一個擁有一些預設配置選項的tslint.json檔案,將檔案內容替換成如下內容

  {
    "defaultSeverity": "error",
    "extends": [
      "tslint-react"
    ],
    "jsRules": {
    },
    "rules": {
      "member-access": false,
      "ordered-imports": false,
      "quotemark": false,
      "no-console": false,
      "semicolon": false,
      "jsx-no-lambda": false
    },
    "rulesDirectory": [
    ],
    "linterOptions": {
      "exclude": [
        "config/**/*.js",
        "node_modules/**/*.ts"
      ]
    }
  }
複製程式碼

簡單說明一下檔案中的各個選項都是用來幹嘛的

  • defaultSeverity規定了錯誤處理的級別,error作為預設值將會在你的IDE裡出現紅色的錯誤提示,然而warning將會展現橘黃色的警告提示
  • "extends": ["tslint-react"]: 我們擴充套件的規則是基於已經刪除了的tslint-recommended庫,之所以沒有使用tslint-recommended庫,是因為該庫有些規則並沒有遵循React語法
  • "rules": {"rule-name": false, ...}: 我們可以在rules物件內忽略一些規則,比如,忽略member-access規則,以阻止tslint報出缺少函式訪問型別的提示,因為在react中成員訪問關鍵字(public,privaye)並不常用,另外一個例子,ordered-imports,這個規則會提示我們根據字母順序排列我們的import的語句,所有可用的規則可以點選這裡進行檢視這裡
  • "linterOptions": {"exclude": [...]}: 在這裡我們排除了所有在config目錄下的js字尾的檔案和在node_modules目錄下的ts檔案以避免tslint的檢查

我們可以在元件的props以及state上應用interface和type

定義interface

當我們傳遞props到元件中去的時候,如果想要使props應用interface,那就會強制要求我們傳遞的props必須遵循interface的結構,確保成員都有被宣告,同時也會阻止未期望的props被傳遞下去。

interface可以定義在元件的外部或是一個獨立檔案,可以像這樣定義一個interface

  interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
  }
複製程式碼

這裡我們建立了一個FormProps介面,包含一些值,agreeToterms後面跟著?,代表該成員是可選的,非必傳,我們也可以給元件的state應用一個interface

  interface FormState {
      submitted?: boolean;
      full_name: string;
      age: number;
  }
複製程式碼

注意:tslint過去會提示我們給每一個interface名稱前面加上一個i,比如IFormProps和IFormState。然而預設是不強制加的

給元件應用interface

我們既可以給類元件也可以給無狀態元件應用interface。對於類元件,我們利用尖括號語法去分別應用我們的props和state的interface。

  export class MyForm extends React.Component<FormProps, FormState> {
    ...
  }
複製程式碼

注意:在只有state而沒有props的情況下,props的位置可以用{}或者object佔位,這兩個值都表示有效的空物件。

對於純函式元件,我們可以直接傳遞props interface

  function MyForm(props: FormProps) {
    ...
  }
複製程式碼

引入interface

按照約定,我們一般會建立一個 **src/types/**目錄來將你的所有interface分組:

  // src/types/index.tsx
  export interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
  }
複製程式碼

然後引入元件所需要的interface

  // src/components/MyForm.tsx
  import React from 'react';
  import { StoreState } from '../types/index';
  ...
複製程式碼

enums

列舉Enums是另外一個typescript有用的功能,假設我們想針對 MyForm元件來定一個列舉,然後對提交的表單值進行驗證

  // define enum
  enum HeardFrom {
      SEARCH_ENGINE = "Search Engine",
      FRIEND = "Friend",
      OTHER = "Other"
  }
  //construct heardFrom array
  let heardFrom = [HeardFrom.SEARCH_ENGINE, 
                  HeardFrom.FRIEND, 
                  HeardFrom.OTHER];

  //get submitted form value
  const submitted_heardFrom = form.values.heardFrom;

  //check if value is valid
  heardFrom.includes(submitted_heardFrom)
    ? valid = true
    : valid = false;
複製程式碼

iterables

在typescript中我們可以使用 for...offor...in方法來進行迴圈遍歷。這兩個方法有一個很重要的區別:

  • for...of方法會返回被迭代物件的鍵(key)的列表
  • for...in方法會返回被迭代物件的值(value)的列表
  for (let i in heardFrom) {
   console.log(i); // "0", "1", "2",
  }
  for (let i of heardFrom) {
    console.log(i); // "Search Engine", "Friend", "Other"
  }
複製程式碼

Typing Events

如果你希望比如onChange或者onClick事件利用語法工具可以獲取明確的你所需要的事件。 可以考慮下面這個例子,通過將游標懸浮在handleChange()方法上,我們就可以清晰的看到真實的事件型別React.ChangeEvent:

event type

然後在我們的handleChange函式定義中傳入e這個引數的時候會用到這個型別

我們也可以給e物件中的name和value指定型別,通過下面的語法:

  const {name, value}: {name: string; value: string;} = e.target;
複製程式碼

如果你不知道物件該指定什麼型別,你可以使用any型別,就像下面這樣

  const {name, value}: any = e.target;
複製程式碼

現在我們已經學會了一些基本的示例,接下來一起來看看typescript如何與redix搭配。探索typescript更多的功能

Redux with Typescript

Step1:給Store指定型別

首先,我們想要給我們的Redux store定義一個interface。定義合理的state結構將有利於你的團隊及更好的維護應用的狀態

這部分可以在我們先前討論過的 /src/types/index.tsx檔案中完成,下面是一個試圖解決位置與身份認證的示例:

  // src/types/index.tsx
  export interface MyStore {
    language: string;
    country: string;
    auth: {
        authenticated: boolean;
        username?: string;
    };
  }
複製程式碼
Step2:定義action的型別以及actions

所有的action型別可以用一種 const & type的模式來進行定義,我們首先在 src/constants/index.tsx檔案中定義action types:

  // src/constants/index.tsx
  export const SET_LANGUAGE = 'INCREMENT_ENTHUSIASM';
  export type SET_LANGUAGE = typeof SET_LANGUAGE;
  export const SET_COUNTRY = 'SET_COUNTRY';
  export type SET_COUNTRY = typeof SET_COUNTRY;
  export const AUTHENTICATE = 'AUTHENTICATE';
  export type AUTHENTICATE = typeof AUTHENTICATE;
複製程式碼

注意到如何讓我們剛剛定義的常量被用作interface型別還是字串字面量,我們會在後面進行使用講解

這些const & type所組成的物件現在可以在src/actions/index.tsx檔案中進行匯入了,這裡我們定義了action interface以及action自身,以及對它們都指定了型別

  // src/actions/index.tsx
  import * as constants from '../constants';

  //define action interfaces
  export interface SetLanguage {
      type: constants.SET_LANGUAGE;
      language: string;
  }
  export interface SetCountry {
      type: constants.SET_COUNTRY;
      country: string;
  }
  export interface Authenticate{
      type: constants.AUTHENTICATE;
      username: string;
      pw: string;
  }

  //define actions
  export function setLanguage(l: string): SetLanguage ({
      type: constants.SET_LANGUAGE,
      language: l
  })
  export function setCountry(c: string): SetCountry ({
      type: constants.SET_COUNTRY,
      country: c
  })
  export function authenticate(u: string, pw: string): Authenticate ({
      type: constants.SET_COUNTRY,
      username: u,
      pw: pw
  })
複製程式碼

在authenticate action中,我們傳入了username和password兩個引數,兩個引數都是string型別,返回值也指定了型別,在這個示例中是Authenticate

在Authenticate interface內部,我們也包括了有效的action所需要的username和pw的值

Step3:定義Reducers

為了簡化在reducer中指定一個action type的過程,我們可以利用聯合型別,這個特性是在typescript1.4版本之後引入進來的,聯合型別允許我們將兩種或兩種以上的型別合併為一個型別

回到我們的actions檔案,給我們表示位置的interface新增一個聯合型別

  // src/actions/index.tsx
  export type Locality = SetLanguage | SetCountry;
複製程式碼

現在我們就可以將Locality型別應用到我們reducer函式中的action

  // src/reducers/index.tsx
  import { Locality } from '../actions';
  import { StoreState } from '../types/index';
  import { SET_LANGUAGE, SET_COUNTRY, AUTHENTICATE} from '../constants/index';
  export function locality(state: StoreState, action: Locality):     StoreState {
    switch (action.type) {
      case SET_LANGUAGE:
        return return { ...state, language: action.language};
      case SET_COUNTRY:
        return { ...state, language: action.country};
      case AUTHENTICATE:
        return { 
          ...state, 
          auth: {
              username: action.username, 
              authenticated: true 
            }
        };
    }
    return state;
  }
複製程式碼

儘管已經全部指定了型別,這個reducer相對來說也是非常直觀

  • 這個命名為locality的reducer,將state指定為StoreState型別,以及將action指定為Locality型別
  • 這個reducer將會返回一個StoreState型別的物件,如果並沒有匹配到任何的action就將原state返回
  • 我們的 constant & type(常量和型別)對在這裡也被得到應用,作為action間切換的條件
Step4:建立初始化Store

利用尖括號傳入型別聯同createStore(),在index.ts檔案中我們可以初始化store了

  // src/index.tsx
  import { createStore } from 'redux';
  import { locality } from './reducers/index';
  import { StoreState } from './types/index';
  const store = createStore<StoreState>(locality, {
    language: 'British (English)',
    country: 'United Kingdom',
    auth: {
        authenticated: false
    }
  });
複製程式碼

已經快要完成了,現在已經覆蓋了整合typescript到redux中的大部分步驟了,再堅持一下,讓我們來看一下容器元件(container component)所需要的mapStateToPropsmapDispatchToProps

Mapping State and Dispatch

mapStateToProps內部,記得將state引數設定為StoreState型別,第二個引數ownProps也可以指定一個props的interface:

  //mapStateToProps example
  import { StoreState } from '../types/index';
  interface LocalityProps = {
      country: string;
      language: string;
  }
  export function mapStateToProps(state: StoreState, ownProps: LocalityProps) {
    return {
      language: state.language,
      country: state.country,
    }
  }
複製程式碼

mapDispatchToProps有些不同,我們利用尖括號想Dispatch方法中傳入一個interface,然後,在返回程式碼塊中dispatch我們Locality型別的action:

  //mapDispatchToProps example
  export function mapDispatchToProps(dispatch: Dispatch<actions.Locality>) {
      return {
          setLanguage: (l: string) => 
              dispatch(actions.setLanguage(l)),
          
          setCountry: (c: string) => 
              dispatch(actions.setCountry(c))
      }
  }
複製程式碼

最後,我們就可以和元件進行連線

  export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
  ...
複製程式碼

總結

這篇文章介紹了typescript和react如何聯合以及如何利用tslint-react進行更加平緩的開發。我們已經瞭解到如何在元件中讓props和state應用interface,同樣也瞭解到了在Typescript中如何處理事件。最終,我們瞭解了typescript如何整合到Redux中。

將typescript整合到react專案中,的確會增加一些額外的成本,但隨著應用範圍的擴大,支援typescript語言一定會增加專案的維護性和可管理性

使用typescript會促進模組化和程式碼分隔,記住,隨著專案的擴大。如果你發現了維護性方面的問題,typescript不僅可以提升程式碼的可讀性,同時也會降低錯誤發生的可能性。

相關文章