React 是一個 JavaScript 庫,它是當今最流行和行業領先的前端開發庫。
JavaScript 是一種鬆散的型別化語言,因此,它捕獲了執行時。這樣做的結果就是 JavaScript 錯誤被捕獲得非常晚,這可能導致嚴重的 bug。
當然 React 作為一個 JavaScript 庫,也繼承了這個問題。
乾淨程式碼(Clean code)")是一種一致的程式設計風格,它使程式碼更容易編寫、讀取和維護。任何人都可以編寫計算機可以理解的程式碼,但是優秀的開發人員可以編寫人類可以理解的乾淨的程式碼。
乾淨的程式碼是一種以讀者為中心的開發風格,它提高了我們的軟體質量和可維護性。
編寫乾淨程式碼需要編寫具有清晰和簡單的設計模式的程式碼,這使得人們可以輕鬆地閱讀、測試和維護程式碼。因此,乾淨的程式碼可以降低軟體開發的成本。這是因為編寫乾淨的程式碼所涉及的原則,消除了技術債務。
在本文中,我們將介紹一些在使用 React 和 TypeScript 時使用的有用模式。
? 為了讓您的團隊更容易地保持程式碼健康並優先處理技術債務工作,請嘗試使用 Stepsize 的 VS Code 和 JetBrains 擴充套件。它們幫助工程師建立技術問題,將它們新增到迭代 中,並持續解決技術債務——而不離開編輯器。
現在讓我們來了解一下在使用 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
– 注意:不建議使用null
和undefined
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
型別,比如 DefaultProps
和 initialState
。
為了防止由於意外設定狀態而導致的開發錯誤: 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}
// ...
}
在上面的程式碼中,通過凍結 DefaultProps
和 initialState
,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 時,函式元件可以通過兩種方式編寫:
- 像一個正常函式一樣,如下面的程式碼:
type Props = { message: string };
const Greeting = ({ message }: Props) => <div>{message}</div>;
- 使用 React.FC 或者 React.FunctionComponent,像下面這樣:
import React, {FC} from "react";
type Props = { message: string };
const Greeting: FC<Props> = (props) => <div>{props}</div>;
使用 FC 提供了一些優勢,例如對諸如 displayName
、 propTypes
和 defaultProps
等靜態屬性進行型別檢查和自動完成。但是它有一個已知的問題,那就是破壞 defaultProps
和其他屬性: propTypes
,contextTypes
,displayName
。
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 過程。它將幫助您的工程團隊獲得技術債務的可見性,在程式碼庫問題上進行協作,並更好地規劃衝刺。