核心要點
構建有狀態的現代應用程序是一項複雜的任務。隨著狀態的改變,應用程序變得不可預測且難以維護。這就是 Redux 的用武之地。 Redux 是一個輕量級的庫,用於處理狀態。可以把它想像成一個狀態機。
在本文中,我將通過構建一個工資處理引擎來深入探討 Redux 的狀態容器。該應用程序將存儲工資單以及所有額外內容,例如獎金和股票期權。我將使用純 JavaScript 和 TypeScript 進行類型檢查來保持解決方案的簡潔性。由於 Redux 非常易於測試,我還將使用 Jest 來驗證應用程序。
在本教程中,我假設您對 JavaScript、Node 和 npm 有一定的了解。
首先,您可以使用 npm 初始化此應用程序:
npm init
當詢問測試命令時,請繼續使用 jest。這意味著 npm t 將啟動 Jest 並運行所有單元測試。主文件將是 index.js,以保持其簡潔性。您可以隨意回答 npm init 的其餘問題。
我將使用 TypeScript 進行類型檢查並確定數據模型。這有助於概念化我們正在嘗試構建的內容。
要開始使用 TypeScript:
npm i typescript --save-dev
我將把開發工作流程中的一部分依賴項放在 devDependencies 中。這清楚地表明哪些依賴項是為開發人員準備的,哪些依賴項將用於生產環境。準備好 TypeScript 後,在 package.json 中添加一個啟動腳本:
"start": "tsc && node .bin/index.js"
在 src 文件夾下創建一個 index.ts 文件。這將源文件與項目的其餘部分分開。如果您執行 npm start,則解決方案將無法執行。這是因為您需要配置 TypeScript。
創建一個包含以下配置的 tsconfig.json 文件:
{ "compilerOptions": { "strict": true, "lib": ["esnext", "dom"], "outDir": ".bin", "sourceMap": true }, "files": [ "src/index" ] }
我本可以將此配置放在 tsc 命令行參數中。例如,tsc src/index.ts --strict .... 但是將所有這些放在單獨的文件中要清晰得多。請注意,package.json 中的啟動腳本只需要一個 tsc 命令。
以下是一些合理的編譯器選項,它們將為我們提供一個良好的起點,以及每個選項的含義:
因為我將使用 Jest 進行單元測試,所以我將繼續添加它:
npm init
ts-jest 依賴項為測試框架添加了類型檢查。一個需要注意的地方是在 package.json 中添加一個 jest 配置:
npm i typescript --save-dev
這使得測試框架能夠拾取 TypeScript 文件並知道如何對其進行轉換。一個不錯的功能是,您在運行單元測試時可以進行類型檢查。為了確保此項目已準備好,請創建一個 __tests__ 文件夾,其中包含一個 index.test.ts 文件。然後,進行健全性檢查。例如:
"start": "tsc && node .bin/index.js"
現在執行 npm start 和 npm t 將不會出現任何錯誤。這告訴我們我們現在可以開始構建解決方案了。但在我們這樣做之前,讓我們將 Redux 添加到項目中:
{ "compilerOptions": { "strict": true, "lib": ["esnext", "dom"], "outDir": ".bin", "sourceMap": true }, "files": [ "src/index" ] }
此依賴項將用於生產環境。因此,無需使用 --save-dev 包含它。如果您檢查您的 package.json,它將位於 dependencies 中。
工資單引擎將包含以下內容:工資、報銷、獎金和股票期權。在 Redux 中,您不能直接更新狀態。相反,會調度操作來通知存儲任何新的更改。
因此,這留下了以下操作類型:
npm i jest ts-jest @types/jest @types/node --save-dev
PAY_DAY 操作類型可用於在發薪日發放支票並跟踪工資歷史記錄。這些操作類型在我們完善工資單引擎時指導其餘的設計。它們捕獲狀態生命週期中的事件,例如設置基本工資金額。這些操作事件可以附加到任何內容,無論是點擊事件還是數據更新。 Redux 操作類型對於調度來自何處是抽象的。狀態容器可以在客戶端和/或服務器上運行。
使用類型理論,我將根據狀態數據確定數據模型。對於每個工資單操作,例如操作類型和可選金額。金額是可選的,因為 PAY_DAY 不需要資金來處理工資單。我的意思是,它可以向客戶收費,但現在先忽略它(也許在第二版中引入)。
例如,將其放在 src/index.ts 中:
"jest": { "preset": "ts-jest" }
對於工資單狀態,我們需要一個用於基本工資、獎金等的屬性。我們還將使用此狀態來維護工資歷史記錄。
此 TypeScript 接口應該可以做到:
npm init
對於每個屬性,請注意 TypeScript 使用冒號指定類型。例如,: number。這確定了類型契約,並為類型檢查器增加了可預測性。使用具有顯式類型聲明的類型系統可以增強 Redux。這是因為 Redux 狀態容器是為可預測的行為而構建的。
這個想法並不瘋狂或激進。 《學習 Redux》第 1 章(僅限 SitePoint Premium 會員)對此進行了很好的解釋。
隨著應用程序的改變,類型檢查增加了額外的可預測性。隨著應用程序的擴展,類型理論也有助於簡化大型代碼段的重構。
現在使用類型概念化引擎有助於創建以下操作函數:
npm i typescript --save-dev
好的一點是,如果您嘗試執行 processBasePay('abc'),類型檢查器會向您發出警告。破壞類型契約會降低狀態容器的可預測性。我使用像 PayrollAction 這樣的單個操作契約來使工資處理器更可預測。請注意,金額通過 ES6 屬性簡寫在操作對像中設置。更傳統的方法是 amount: amount,這比較冗長。箭頭函數,例如 () => ({}),是編寫返回對象文字的函數的一種簡潔方法。
reducer 函數需要一個狀態和一個操作參數。狀態應該具有具有默認值的初始狀態。那麼,你能想像我們的初始狀態可能是什麼樣子嗎?我認為它需要從零開始,並帶有一個空的工資歷史記錄列表。
例如:
"start": "tsc && node .bin/index.js"
類型檢查器確保這些是屬於此對象的正確值。有了初始狀態,就開始創建 reducer 函數:
{ "compilerOptions": { "strict": true, "lib": ["esnext", "dom"], "outDir": ".bin", "sourceMap": true }, "files": [ "src/index" ] }
Redux reducer 具有一個模式,其中所有操作類型都由 switch 語句處理。但在遍歷所有 switch case 之前,我將創建一個可重用的局部變量:
npm i jest ts-jest @types/jest @types/node --save-dev
請注意,如果您不改變全局狀態,則可以改變局部變量。我使用 let 運算符來傳達此變量將來會發生變化。改變全局狀態(例如狀態或操作參數)會導致 reducer 不純。這種函數式範式至關重要,因為 reducer 函數必須保持純淨。 《JavaScript 從新手到忍者》第 11 章(僅限 SitePoint Premium 會員)對此進行了解釋。
開始 reducer 的 switch 語句以處理第一個用例:
"jest": { "preset": "ts-jest" }
我使用 ES6 rest 運算符來保持狀態屬性不變。例如,...state。您可以在新對像中的 rest 運算符之後覆蓋任何屬性。 basePay 來自解構,這很像其他語言中的模式匹配。 computeTotalPay 函數設置如下:
it('is true', () => { expect(true).toBe(true); });
請注意,您會扣除 stockOptions,因為這筆錢將用於購買公司股票。假設您想處理報銷:
npm init
由於金額是可選的,請確保它具有默認值以減少故障。這就是 TypeScript 的優勢所在,因為類型檢查器會發現此陷阱並向您發出警告。類型系統知道某些事實,因此它可以做出合理的假設。假設您想處理獎金:
npm i typescript --save-dev
此模式使 reducer 可讀,因為它只維護狀態。您獲取操作的金額,計算總工資,並創建一個新的對象文字。處理股票期權沒有什麼不同:
"start": "tsc && node .bin/index.js"
對於在發薪日處理工資單,它需要抹去獎金和報銷。這兩個屬性不會在每個工資單中保留在狀態中。並且,向工資歷史記錄中添加一個條目。基本工資和股票期權可以保留在狀態中,因為它們不會經常更改。考慮到這一點,這就是 PAY_DAY 的處理方式:
{ "compilerOptions": { "strict": true, "lib": ["esnext", "dom"], "outDir": ".bin", "sourceMap": true }, "files": [ "src/index" ] }
在一個像 newPayHistory 這樣的數組中,使用擴展運算符,它是 rest 的反義詞。與收集對像中屬性的 rest 不同,它會將項目展開。例如,[...payHistory]。儘管這兩個運算符看起來很相似,但它們並不相同。仔細觀察,因為這可能會出現在面試問題中。
對 payHistory 使用 pop() 不會改變狀態。為什麼?因為 slice() 返回一個全新的數組。 JavaScript 中的數組是通過引用複制的。將數組分配給新變量不會更改底層對象。因此,在處理這些類型的對象時必須小心。
因為 lastPayHistory 有可能未定義,所以我使用窮人的空值合併來將其初始化為零。請注意 (o && o.property) || 0 模式用於合併。 JavaScript 或甚至 TypeScript 的未來版本可能會有一種更優雅的方法來做到這一點。
每個 Redux reducer 都必須定義一個默認分支。為了確保狀態不會變得未定義:
npm i jest ts-jest @types/jest @types/node --save-dev
編寫純函數的眾多好處之一是它們易於測試。單元測試是指您必須期望可預測的行為的測試,您可以將所有測試作為構建的一部分自動化。在 __tests__/index.test.ts 中,取消虛擬測試並導入所有感興趣的函數:
"jest": { "preset": "ts-jest" }
請注意,所有函數都設置為導出,因此您可以導入它們。對於基本工資,啟動工資單引擎 reducer 並對其進行測試:
it('is true', () => { expect(true).toBe(true); });
Redux 將初始狀態設置為未定義。因此,在 reducer 函數中提供默認值始終是一個好主意。處理報銷怎麼樣?
npm i redux --save
處理獎金的模式與此相同:
const BASE_PAY = 'BASE_PAY'; const REIMBURSEMENT = 'REIMBURSEMENT'; const BONUS = 'BONUS'; const STOCK_OPTIONS = 'STOCK_OPTIONS'; const PAY_DAY = 'PAY_DAY';
對於股票期權:
interface PayrollAction { type: string; amount?: number; }
請注意,當 stockOptions 大於 totalPay 時,totalPay 必須保持不變。由於這家假設的公司是合乎道德的,它不想從員工那裡拿錢。如果您運行此測試,請注意 totalPay 設置為 -10,因為 stockOptions 會被扣除。這就是我們測試代碼的原因!讓我們修復計算總工資的地方:
npm init
如果員工賺的錢不夠買公司股票,請繼續跳過扣除。另外,確保它將 stockOptions 重置為零:
npm i typescript --save-dev
該修復程序確定了 newStockOptions 中他們是否有足夠的錢。有了這個,單元測試通過,代碼健全且有意義。我們可以測試有足夠的錢進行扣除的積極用例:
"start": "tsc && node .bin/index.js"
對於發薪日,請使用多個狀態進行測試,並確保一次性交易不會持續存在:
{ "compilerOptions": { "strict": true, "lib": ["esnext", "dom"], "outDir": ".bin", "sourceMap": true }, "files": [ "src/index" ] }
請注意,我如何調整 oldState 以驗證獎金並將報銷重置為零。
reducer 中的默認分支怎麼樣?
npm i jest ts-jest @types/jest @types/node --save-dev
Redux 在開始時設置了一個像 INIT_ACTION 這樣的操作類型。我們只關心我們的 reducer 是否設置了一些初始狀態。
此時,您可能會開始懷疑 Redux 是否更像是一種設計模式。如果您回答它既是模式又是輕量級庫,那麼您是對的。在 index.ts 中,導入 Redux:
"jest": { "preset": "ts-jest" }
下一個代碼示例可以圍繞此 if 語句包裝。這是一個權宜之計,因此單元測試不會洩漏到集成測試中:
it('is true', () => { expect(true).toBe(true); });
我不建議在實際項目中這樣做。模塊可以放在單獨的文件中以隔離組件。這使其更易於閱讀,並且不會洩漏問題。單元測試也受益於模塊獨立運行的事實。
使用 payrollEngineReducer 啟動 Redux 存儲:
npm i redux --save
每個 store.subscribe() 都返回一個後續的 unsubscribe() 函數,該函數可用於清理。它會在通過存儲調度操作時取消訂閱回調。在這裡,我使用 store.getState() 將當前狀態輸出到控制台。
假設這位員工賺了 300,有 50 的報銷,100 的獎金,以及 15 用於公司股票:
const BASE_PAY = 'BASE_PAY'; const REIMBURSEMENT = 'REIMBURSEMENT'; const BONUS = 'BONUS'; const STOCK_OPTIONS = 'STOCK_OPTIONS'; const PAY_DAY = 'PAY_DAY';
為了使其更有趣,再進行 50 的報銷並處理另一張工資單:
interface PayrollAction { type: string; amount?: number; }
最後,運行另一張工資單並取消訂閱 Redux 存儲:
interface PayStubState { basePay: number; reimbursement: number; bonus: number; stockOptions: number; totalPay: number; payHistory: Array<PayHistoryState>; } interface PayHistoryState { totalPay: number; totalCompensation: number; }
最終結果如下所示:
export const processBasePay = (amount: number): PayrollAction => ({type: BASE_PAY, amount}); export const processReimbursement = (amount: number): PayrollAction => ({type: REIMBURSEMENT, amount}); export const processBonus = (amount: number): PayrollAction => ({type: BONUS, amount}); export const processStockOptions = (amount: number): PayrollAction => ({type: STOCK_OPTIONS, amount}); export const processPayDay = (): PayrollAction => ({type: PAY_DAY});
如所示,Redux 維護狀態、改變狀態並在一個簡潔的小包中通知訂閱者。可以將 Redux 想像成一個狀態機,它是狀態數據的真實來源。所有這些都採用了編碼的最佳實踐,例如健全的函數式範式。
Redux 為複雜的狀態管理問題提供了一個簡單的解決方案。它依賴於函數式範式來減少不可預測性。因為 reducer 是純函數,所以單元測試非常容易。我決定使用 Jest,但是任何支持基本斷言的測試框架都可以工作。
TypeScript 使用類型理論增加了額外的保護層。將類型檢查與函數式編程結合起來,您將獲得幾乎不會中斷的健全代碼。最重要的是,TypeScript 在增加價值的同時不會妨礙工作。如果您注意到,一旦類型契約到位,幾乎沒有額外的編碼。類型檢查器會完成其餘的工作。像任何好工具一樣,TypeScript 在保持不可見的同時自動化編碼紀律。 TypeScript 吠叫聲很大,但咬起來很輕。
如果您想試用此項目(我希望您這樣做),您可以在 GitHub 上找到本文的源代碼。
以上是深入研究Redux的詳細內容。更多資訊請關注PHP中文網其他相關文章!