使用 React 和 TypeScript something 編寫乾淨程式碼的10個必知模式

leexiaoran 發表於 2022-03-09
React

React 是一個 JavaScript 庫,它是當今最流行和行業領先的前端開發庫。

JavaScript 是一種鬆散的型別化語言,因此,它捕獲了執行時。這樣做的結果就是 JavaScript 錯誤被捕獲得非常晚,這可能導致嚴重的 bug。

當然 React 作為一個 JavaScript 庫,也繼承了這個問題。

乾淨程式碼(Clean code)")是一種一致的程式設計風格,它使程式碼更容易編寫、讀取和維護。任何人都可以編寫計算機可以理解的程式碼,但是優秀的開發人員可以編寫人類可以理解的乾淨的程式碼。

乾淨的程式碼是一種以讀者為中心的開發風格,它提高了我們的軟體質量和可維護性。

編寫乾淨程式碼需要編寫具有清晰和簡單的設計模式的程式碼,這使得人們可以輕鬆地閱讀、測試和維護程式碼。因此,乾淨的程式碼可以降低軟體開發的成本。這是因為編寫乾淨的程式碼所涉及的原則,消除了技術債務。

在本文中,我們將介紹一些在使用 React 和 TypeScript 時使用的有用模式。

💡 為了讓您的團隊更容易地保持程式碼健康並優先處理技術債務工作,請嘗試使用 Stepsize 的 VS CodeJetBrains 擴充套件。它們幫助工程師建立技術問題,將它們新增到迭代 中,並持續解決技術債務——而不離開編輯器。

現在讓我們來了解一下在使用 React 和 Typescript 時應用的 10 個有用模式:

1. 使用預設匯入來匯入 React

考慮下面的程式碼:

import * as React from "react";

雖然上面的程式碼可以工作,但是如果我們不使用 React 的所有內容,那麼匯入它們是令人困惑的,也不是一個好的做法。一個更好的模式是使用如下所示的預設匯出:

import React, {useContext, useState} from "react";

使用這種方法,我們可以從 React 模組中解構我們需要的東西,而不是匯入所有的內容。

注意: 要使用這個選項,我們需要配置 tsconfig.json 檔案,如下所示:

{
  "compilerOptions": {
    "esModuleInterop": true"
  }
}

在上面的程式碼中,通過將 esModuleInterop 設定為 true,我們啟用了 allowSyntheticDefaultImports ,這對於 TypeScript 支援我們的語法非常重要。

2. 型別宣告要在執行時實現之前

考慮下面的程式碼:

import React, {Component} from "react";

const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }

type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps

class Counter extends Component {

   static defaultProps = defaultProps;
   state = initialState;

   // ...

}

如果我們將執行時宣告和編譯時宣告分開,並且編譯時宣告在執行時宣告之前,那麼上面的程式碼可以更清晰、更易讀。

考慮下面的程式碼:

import React, {Component} from "react";

type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps

const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }

class Counter extends Component {

   static defaultProps = defaultProps;
   state = initialState;

   // ...

}

現在,初看起來,開發人員知道元件 API 是什麼樣的,因為程式碼的第一行清楚地顯示了這一點。

此外,我們還將編譯時宣告與執行時宣告分開。

3. 給 children 提供明確的 props

Typescript 反映了 React 如何處理 children props,方法是在 react.d.ts 中為函式元件和類元件將其註釋為可選的。

因此,我們需要明確地為 children 提供一個 props 型別。但是,最好總是用型別明確地註釋children的 props。在我們希望使用 children 進行內容投影的情況下,這是非常有用的,如果我們的元件不使用它,我們可以簡單地使用 never 型別來註釋它。

考慮下面的程式碼:

import React, {Component} from "react";
// Card.tsx
type Props = {
    children: React.ReactNode
}

class Card extends Component<Props> {
    render() {
        const {children} = this.props;
        return <div>{children}</div>;
    }
}

下面是一些註釋 children 的 props 型別:

  • ReactNode | ReactChild | ReactElement
  • 對於原始型別可以使用: string | number | boolean
  • 物件和陣列也是有效的型別
  • never | null | undefined – 注意:不建議使用 nullundefined

4. 使用型別推斷來定義元件狀態或 DefaultProps

看下面的程式碼:

import React, {Component} from "react";

type State = { count: number };

type Props = {
    someProps: string & DefaultProps;
}

type DefaultProps = {
    name: string
}

class Counter extends Component<Props, State> {
    static defaultProps: DefaultProps = {name: "John Doe"}
    state = {count: 0}

    // ...
}

雖然上面的程式碼可以工作,但是我們可以對它進行以下改進: 啟用 TypeScript 的型別系統來正確推斷readonly 型別,比如 DefaultPropsinitialState

為了防止由於意外設定狀態而導致的開發錯誤: this.state = {}

考慮下面的程式碼:

import React, {Component} from "react";

const initialState = Object.freeze({ count: 0 })
const defaultProps = Object.freeze({name: "John Doe"})

type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;

class Counter extends Component<Props, State> {
    static readonly defaultProps = defaultProps;
    readonly state  = {count: 0}

    // ...
}

在上面的程式碼中,通過凍結 DefaultPropsinitialState,TypeScript 型別系統現在可以將它們推斷為readonly型別。

另外,通過在類中將靜態 defaultProps 和狀態標記為 readonly,我們消除了上面提到的設定狀態引起執行時錯誤的可能性。

5. 宣告 Props/State 時使用型別別名(type),而不是介面(interface)

雖然可以使用interface,但為了一致性和清晰性起見,最好使用 type,因為有些情況下interface不能工作。例如,在前面的示例中,我們重構了程式碼,以使 TypeScript 的型別系統能夠通過從實現中定義狀態型別來正確推斷 readonly型別。我們不能像下面的程式碼那樣使用這個模式的interface:

// works
type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;

// throws error
interface State = typeof initialState;
interface Props = { someProps: string } & typeof defaultProps;

此外,我們不能用聯合和交集建立的型別擴充套件interface,因此在這些情況下,我們必須使用 type

6. 不要再 interface/type 中使用方法宣告

這可以確保我們的程式碼中的模式一致性,因為 type/interface 推斷的所有成員都是以相同的方式宣告的。另外,--strictFunctionTypes 僅在比較函式時工作,而不適用於方法。你可以從這個 TS 問題中得到進一步的解釋。

// Don't do
interface Counter {
  start(count:number) : string
  reset(): void
}

// Do
interface Counter {
  start: (count:number) => string
  reset: () => string
}

7. 不要使用 FunctionComponent

或者簡稱為 FC 來定義一個函式元件。

當使用 Typescript 和 React 時,函式元件可以通過兩種方式編寫:

  1. 像一個正常函式一樣,如下面的程式碼:
type Props = { message: string };

const Greeting = ({ message }: Props) => <div>{message}</div>;
  1. 使用 React.FC 或者 React.FunctionComponent,像下面這樣:
import React, {FC} from "react";

type Props = { message: string };

const Greeting: FC<Props> = (props) => <div>{props}</div>;

使用 FC 提供了一些優勢,例如對諸如 displayNamepropTypesdefaultProps 等靜態屬性進行型別檢查和自動完成。但是它有一個已知的問題,那就是破壞 defaultProps 和其他屬性: propTypescontextTypesdisplayName

FC 還提供了一個隱式型別的 children 屬性,也有已知的問題。此外,正如前面討論的,元件 API 應該是顯式的,所以一個隱式型別的 children 屬性不是最好的。

8. 不要對類元件使用建構函式

有了新的 類屬性 提議,就不再需要在 JavaScript 類中使用建構函式了。使用建構函式涉及呼叫 super ()和傳遞 props,這就引入了不必要的樣板和複雜性。

我們可以編寫更簡潔、更易於維護的 React class 元件,使用類欄位,如下所示:

// Don't do
type State = {count: number}
type Props = {}

class Counter extends Component<Props, State> {
  constructor(props:Props){
      super(props);
      this.state = {count: 0}
  }
}

// Do
type State = {count: number}
type Props = {}

class Counter extends Component<Props, State> {
  state = {count: 0}
}

在上面的程式碼中,我們看到使用類屬性涉及的樣板檔案較少,因此我們不必處理 this 變數。

9. 不要在類中使用 public 關鍵字

考慮下面的程式碼:

import { Component } from "react"

class Friends extends Component {
  public fetchFriends () {}
  public render () {
    return // jsx blob
  }
}

由於類中的所有成員在預設情況下和執行時都是 public 的,因此不需要通過顯式使用 public 關鍵字來新增額外的樣板檔案。相反,使用下面的模式:

import { Component } from "react"

class Friends extends Component {
  fetchFriends () {}
  render () {
    return // jsx blob
  }
}

10. 不要在元件類中使用 private

考慮下面的程式碼:

import {Component} from "react"

class Friends extends Component {
  private fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

在上面的程式碼中,private 只在編譯時將 fetchProfileByID 方法私有化,因為它只是一個 Typescript 模擬。但是,在執行時,fetchProfileByID 方法仍然是公共的。

有不同的方法使 JavaScript 類的屬性/方法私有化,使用下劃線(\_)變數命名原則如下:

import {Component} from "react"

class Friends extends Component {
  _fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

雖然這並沒有真正使 fetchProfileByID 方法成為私有方法,但它很好地向其他開發人員傳達了我們的意圖,即指定的方法應該被視為私有方法。其他技術包括使用 WeakMap、Symbol 和限定作用域的變數。

但是有了新的 ECMAScript 類欄位的提議,我們可以通過使用私有欄位輕鬆優雅地實現這一點,如下所示:

import {Component} from "react"

class Friends extends Component {
  #fetchProfileByID () {}

  render () {
    return // jsx blob
  }
}

而且 TypeScript 支援 3.8 及以上版本私有欄位的新 JavaScript 語法。

附加:不要使用 enum

儘管 enum 在 JavaScript 中是一個保留字,但是使用 enum 並不是一個標準的慣用 JavaScript 模式。

但是如果你使用的是 c # 或者 JAVA 這樣的語言,那麼使用 enum 可能是非常誘人的。但是,還有更好的模式,比如使用編譯型別文字,如下所示:

// Don't do this
enum Response {
  Successful,
  Failed,
  Pending
}

function fetchData (status: Response): void => {
    // some code.
}

// Do this
type Response = Sucessful | Failed | Pending

function fetchData (status: Response): void => {
    // some code.
}

總結

毫無疑問,使用 Typescript 會給你的程式碼增加很多額外的樣板檔案,但是這樣做的好處是非常值得的。

為了使您的程式碼更乾淨、更好,不要忘記實現一個健壯的 TODO/issue 過程。它將幫助您的工程團隊獲得技術債務的可見性,在程式碼庫問題上進行協作,並更好地規劃衝刺。

本文譯自:https://dev.to/alexomeyer/10-...