
使用表格驅動方法和狀態模式管理前端狀態
這篇文章是通過 AI 翻譯生成,可能有不準確之處。
封面照片由 @shawnanggg 在 Unsplash 提供
本文的目的是分享一個在元件中管理狀態分支邏輯的模式。
在前端開發中,一個常見的情況是根據不同的狀態切換不同的 UI 或行為。例如:
- 活動頁面根據活動狀態、登入狀態等顯示不同的 UI
- 產品元件根據產品狀態、評論狀態、違規狀態等以不同的方式呈現
如果我們不小心處理,就會在程式碼中產生複雜的邏輯。這會使程式碼變得更難維護且容易出錯。
為了緩解這個問題,我們可以使用以下 2 種方法:
表格驅動方法
這是來自《Code Complete》一書的程式設計方法,用以更簡潔的方式管理 if else 分支邏輯(有時候)。
讓我們看一個例子:
// If Else
if (a === 'foo') {
if (b === 'abc') {
action1()
} else if (b === 'xyz') {
action2()
}
} else if (a === 'bar') {
action3()
}
// Table driven
const action = {
foo: {
abc: action1,
xyz: action2,
},
bar: {
abc: action3,
xyz: action3,
},
}[a][b]
action()
雖然第二種方法有重複的 action3
宣告,但在這種情況下仍然更直接。但請記住,如果你只有少數幾個動作但有很多狀態,表格驅動方法可能會相當冗餘和冗長。我們需要找到何時使用哪種方法的平衡點。
這裡有一個創建這種表格驅動 JS 程式碼的簡單工具,你可能想看看。歡迎對這個工具提供任何意見 :)
https://playground.kenneth.pro/easy-state
狀態模式
據我理解,GOF 狀態模式的主要思想是:
首先根據給定狀態選擇一組預定義的方法和變數,並提供相同的介面,使其依賴者可以使用該介面而不需要知道細節。
如果我們使用狀態模式,實作會是:
- 一開始就預定義所有需要切換的變數和方法,並根據給定狀態選擇集合。
讓我們看一個 Typescript 的實際例子,結合我們剛才談到的表格驅動方法。
在這個例子中,CampaignComponent
根據不同的 LoginStatus
、CampaignStatus
、UserCampaignStatus
顯示不同的 UI:
// Define Status Type
enum LoginStatus {
NOT_LOGIN,
LOGIN,
}
enum CampaignStatus {
NOT_STARTED,
STARTED,
ENDED,
}
enum UserCampaignStatus {
NOT_ATTENDED,
ATTENDED,
}
// Define Component Props Type
interface Props {
banner: string
submit: () => void
// ...
}
// Define Concrete Props
const NotStarted: Props = { banner: 'Campaign Not stated' /*...*/ }
const NotLogin: Props = { banner: 'Please login' /*...*/ }
const Eligible: Props = { banner: 'Welcome!!' /*...*/ }
const Attended: Props = { banner: 'Attended before' /*...*/ }
const Ended: Props = { banner: 'Campaign is ended' /*...*/ }
// Define States Mapping
const States = {
[LoginStatus.NOT_LOGIN]: {
[CampaignStatus.NOT_STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: NotStarted,
[UserCampaignStatus.ATTENDED]: NotStarted,
},
[CampaignStatus.STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: NotLogin,
[UserCampaignStatus.ATTENDED]: NotLogin,
},
[CampaignStatus.ENDED]: {
[UserCampaignStatus.NOT_ATTENDED]: CampaignEnded,
[UserCampaignStatus.ATTENDED]: CampaignEnded,
},
},
[LoginStatus.LOGIN]: {
[CampaignStatus.NOT_STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: NotStarted,
[UserCampaignStatus.ATTENDED]: NotStarted,
},
[CampaignStatus.STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: Eligible,
[UserCampaignStatus.ATTENDED]: Attended,
},
[CampaignStatus.ENDED]: {
[UserCampaignStatus.NOT_ATTENDED]: CampaignEnded,
[UserCampaignStatus.ATTENDED]: CampaignEnded,
},
},
}
在這個例子中,雖然一些狀態分支是冗餘和冗長的,但它將業務邏輯與你的 UI 元件分離。這在某種程度上更可測試和可維護。
以 React 為例,我們可以在我們的自定義 hooks 或 redux containers 中準備我們的 props。
此外,你可以將 States
物件包裝在一個函數中,以使其對於給定的業務需求更直接或更簡潔,像這樣:
const getState = (
loginStatus: LoginStatus,
campaignStatus: CampaignStatus,
userCampaignStatus: UserCampaignStatus
) => {
if (campaignStatus === CampaignStatus.NOT_STARTED) return NotStarted
if (campaignStatus === CampaignStatus.ENDED) return CampaignEnded
const States = {
[LoginStatus.NOT_LOGIN]: {
[CampaignStatus.STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: NotLogin,
[UserCampaignStatus.ATTENDED]: NotLogin,
},
},
[LoginStatus.LOGIN]: {
[CampaignStatus.STARTED]: {
[UserCampaignStatus.NOT_ATTENDED]: Eligible,
[UserCampaignStatus.ATTENDED]: Attended,
},
},
}
return States[loginStatus][campaignStatus][userCampaignStatus]
}
只要程式碼是可理解的、可測試的、一致的並且能正確運作,我們使用哪種範式並不重要。這就是每個人都能使用的好程式碼。
希望這篇文章或多或少對你有幫助,開發愉快!