深入探討 React 前端應用架構:無需功能性設計


摘要

在當今快速發展的前端開發中,理解 React 應用架構的重要性不言而喻。本文將帶你深度解析如何運用 Class Component 和現代工具建構高效且易維護的應用程式。 歸納要點:

  • 超越 Functional Component 的思考,深入探討 Class Component 在複雜狀態管理、生命週期控制及第三方函式庫整合上的優勢。
  • 強調關注點分離的極致應用,結合設計模式提升程式碼的可重用性與可維護性,並使用 TypeScript 增強型別安全性。
  • 介紹如何有效管理大型 React 專案中的自定義元件庫,包括版本控制、相依性管理及建立完善文件和測試策略。
總體而言,這篇文章提供了多方面的方法來優化 React 應用程式設計,提高開發效率和程式碼質量。

大家好,我是 Pavel Rozhkov,Doubletapp 的首席前端開發者。我們公司專注於定製開發,在處理 React 專案時,我們非常依賴我們不斷完善的架構指導方針。這套約定定義了專案程式碼的組織方式。


提升開發效率與程式碼品質的團隊指導方針

我們的指導方針幫助我們:輕鬆地在專案之間調換團隊成員。新開發者可以迅速融入或增強團隊,而無需長時間的入職培訓。簡化開發時間。許多問題,例如「這應該放在哪裡?」或「應該如何結構化?」都能事先得到解決。維護舊有專案,因為它們遵循相同的原則。透過專注於重要任務來提升程式碼品質,而不是重新思考基本的組織問題。以清晰的文件協助新團隊成員上手。

在與社群的交流中,許多人將他們的指導方針描述為功能切片設計(Feature-Sliced Design, FSD)與根據自身需求進行自訂調整的混合體。這是完全合理的,因為架構是一種用來解決商業問題的工具,而這些問題各不相同。FSD 的學習曲線也很陡峭。一個挑戰在於其正規化轉變:FSD 主要圍繞實體(Entities)組織專案,這可能甚至不包括 UI 元素。我們建議採取更直接的方法——圍繞介面(Interfaces)建立應用程式,以直觀易懂的方式進行設計。

在實施這些指導方針時,團隊應定期進行回顧和反思,以確保所採用的架構和流程能夠持續滿足業務需求並適應技術變化。這樣不僅可以鞏固現有的開發標準,還能促進創新和改善,從而提升整體工作效率與團隊協作。

React 元件架構最佳實踐:公共程式庫與元件分類

我們準備了一個公共庫作為使用我們方法論建立應用程式的範本。這個範本包含了完全配置好的專案設定以及我們使用的最新技術堆疊。範本將持續保持更新,因此可以自由地用於您的專案。

• Api
• App
• Assets
• Components
◦ Pages
◦ Wrapper
◦ Layouts
◦ Widgets
◦ Dummies
◦ UI
• Constants
• Hooks
• Model
• Stores
• Styles
• Utils

接下來,我們將深入探討每一類別。在 React 架構中,一個關鍵問題是如何將介面劃分為元件。我們的方法如下:

我們使用六種型別的元件:
• Pages(頁面)
• Widgets(小部件)
• Dummies(佔位符)
• UI(使用者介面)
• Wrappers(高階元件,HOCs)
• Layouts(佈局)

在建立新頁面時,我們會識別出在應用程式中可以重複使用的元素,並將這些元素放置於對應的目錄中。而其他特定於某個元件的佈局或元素則直接儲存在該元件的資料夾或目錄內。這一原則適用於每種型別的元件。

React 元件設計:提升可重用性與維護性的最佳實踐

現在讓我們更仔細地看看元件的型別以及我們如何定義它們:

- 代表傳遞給路由器的全頁元件。必要時,它們可以包含商業邏輯。
- 定義用於排列介面元素的模板。一個常見的例子是頁面的佈局元件,這包括標頭、底部和一個子屬性,用於顯示特定於頁面的內容。佈局也可以透過插槽作為屬性接收元件,並根據需要進行排列。
- 用於擴充套件或修改功能的輔助元件。例如:一個包裝器,可以為其子項新增動畫效果。
- 自包含的元件,具備完整功能,包括商業邏輯。範例包括:標頭、登入表單、產品列表或橫幅。
- 複雜顯示元件,透過屬性接收所需資料。它們不包含商業邏輯,只具有顯示邏輯,例如切換區塊可見性。一個常見範例是產品卡片,我們傳遞產品詳情以及“加入購物車”按鈕的回撥,使該元件能夠在不同商業邏輯下重用。

在設計這些元件時,考慮可重用性和維護性至關重要。透過將共用邏輯抽象為高階元件或自定義鉤子,可以減少重複程式碼,提高開發效率,同時使未來功能擴充套件更加靈活。

React 元件架構:高效能開發的 PascalCase 命名與層級式設計

基本介面元素如按鈕、輸入框、載入器和提示工具。可能包括本地狀態或顯示邏輯。元件分為中立型或層級型:

中立型元件:
• 佈局
• 包裝器

對匯入沒有任何限制。

層級型元件(從上到下):
• 頁面
• 小部件
• 假元件
• 使用者介面

每個元件只能被匯入到更高層級的元件中。例如,使用者介面不能包含小部件,但小部件可以包括較低層級的元件,或者僅擁有自己的佈局和邏輯。同一層級內的匯入也是被允許的。

每個元件都存放在以 PascalCase 命名的專用資料夾中(例如 MyComponent)。資料夾結構包括以下內容:

MyComponent.tsx。
這是主要的元件檔案,命名方式與其所在資料夾相同。如果該元件接受 props,那麼 Props 型別會在此檔案中定義,位於元件宣告之上。如有需要,也可在此處加入額外的型別定義。

提升 React 元件開發效率:最佳實務與檔案結構

index.ts:一個用於簡化元件匯入路徑的檔案。它也可以作為該目錄中所有導出材料的單一入口點。Styles.module.scss:包含特定於該元件的樣式。types.ts(可選):存放該元件的型別定義。例如,當元件與後端模型互動時,如果前端表示和後端要求不匹配(例如,表單結構與後端需求不同),這些型別就會儲存在此處。constants.ts(可選):任何與該元件相關的靜態資料。useMyComponent.ts(適用於具有業務邏輯的元件):一個處理該元件業務邏輯的 Hook。在這種方式下分離職責使得程式碼更具可維護性和重用性。如果需要將業務邏輯在不同介面中重用,可以輕鬆實現。

高效程式碼組織:元件、模型與資源管理

元件(可選)。如果部分元件的程式碼可以提取到僅由父元件使用的子元件中,則將其儲存於此。這些元件的結構遵循與其父元件相同的規則。巢狀層級可以是任何深度。您可以根據需要擴充套件此列表,增加該部分應用程式所需的其他資源,只要它們是專屬於該部分即可。此目錄包含跨應用程式共享的型別定義,包括伺服器請求/響應和未繫結於特定介面的客戶端模型。應用程式的不同部分通常需要來自各種類別的模型。在一個地方儲存模型簡化了重用,減少了重複,並提供了一個清晰的“地圖”,顯示所有可用型別。我們為每個模型類別建立一個資料夾:• 通用 • 認證 • 使用者 • 等等。

每個資料夾包含:• api.ts • client.ts這兩類別的模型可以在我們的應用程式中隨處使用。描述了伺服器請求和回應的結構。與伺服器相關的模型名稱字尾為 Request 和 Response。例如:

interface User {   id: string;   name: string;   bio: string;   avatar: ImageDTO; }   export interface GetUsersRequest {   limit: number;   offset: number; }   export interface GetUsersResponse {   count: number;   items: User[]; }

有時我們需要為後端實體建立一個模型,以描述來自不同類別的端點。例如,一個可能出現在各種端點回應中的檔案。我們將此模型放置在 Common 資料夾中,並新增 DTO 字尾,以避免與客戶端模型潛在的名稱衝突,同時表明這是一種型別於伺服器端的資料傳輸物件。

export interface FileDTO {   id: string   fileUrl?: string   fileName?: string   fileSize?: number }

包含不依賴於後端結構的客戶端模型,但可在應用程式中廣泛使用。示例:

export interface SelectOption {   value: string   label: string }

注意:如果後端模型的樣式與前端樣式不同(例如,後端模型使用 snake_case,而前端則使用 camelCase),我們會使用 Axios 的攔截器來標準化格式為 camelCase。另一方面,也可以為每個請求編寫序列化函式以轉換資料格式,但根據我們的經驗,這種方法被證明不太實用。

export const http = axios.create({   baseURL: BASE_URL,   headers: {     'X-API-KEY': BASE_API_KEY,     'Content-Type': 'application/json'   } })   const responseInterceptors = {   onSuccess: (response: AxiosResponse) => {     if (response.data && response.headers['content-type'] === 'application/json') {       response.data = camelizeKeys(response.data)     }       return response.data ? response.data : response   },   onError: (error: Error) => Promise.reject(error) }   const requestInterceptors = {   onSuccess: (config: InternalAxiosRequestConfig) => {     config.params = decamelizeKeys(config.params)     if (config.data && config.headers['Content-Type'] === 'application/json') {       config.data = decamelizeKeys(config.data)     }       return config   },   onError: (error: Error) => Promise.reject(error) }   http.interceptors.request.use(requestInterceptors.onSuccess, requestInterceptors.onError) http.interceptors.response.use(responseInterceptors.onSuccess, responseInterceptors.onError)

此目錄包含在整個應用程式中與伺服器互動的函式。類似於模型目錄,檔案根據其管理的實體進行組織:• auth.ts • users.ts • products.ts • 等等。// auth.ts

import { http } from 'config/axios/http' import { privateHttp } from 'config/axios/privateHttp' import {   VerifyContactRequest,   VerifyContactResponse,   VerifyCodeRequest,   VerifyCodeResponse,   UpdateTokensResponse,   UpdateTokensRequest } from 'models/auth/api' import { SuccessResponse } from 'models/common/api'   export const updateTokens = (data: UpdateTokensRequest) =>   http.post('/auth/update-tokens', data)   export const verifyContact = (data: VerifyContactRequest) =>   http.post('/auth/verify/contact', data)   export const verifyCode = (data: VerifyCodeRequest) =>   http.post('/auth/verify/contact/code', data)   export const logout = () => privateHttp.post('/auth/logout')

這是應用程式的初始化層,包含啟動所需的一切。• 提供者 • 樣式 • 型別 • 鉤子 • ... • App.ts 初始化過程發生在 App.tsx 元件中(/app/App.tsx)。在這裡,匯入了全域性樣式、應用字型、提供者、路由器、型別宣告(d.ts)、鉤子以及其他必要資源。例子:

import { DEFAULT_ARIA_LOCALE } from 'constants/variables' import { RouterProvider } from '@tanstack/react-router' import { I18nProvider } from 'react-aria' import { router } from './constants/router' import { withAppProviders } from './providers/appProvider' import './styles/global.scss'  function App() {   return (                    ) }  export default withAppProviders(App)

此目錄包含該專案的所有媒體檔案(圖片、圖示、音訊、影片)。它按類別組織成子目錄:
• 圖示
• 圖片
• 音訊
• 影片

在每個類別中,檔案也可以進一步組織。例如:/icons/arrows/regular-arrow.svg

包含應用程式所需的所有常量。範例:// permissions.ts

export const rules: Rules = {   [UserRole.VISITOR]: {     [Permissions.READ_PRIVATE_PAGES]: false   },   [UserRole.TUTOR]: {     [Permissions.READ_PRIVATE_PAGES]: true,     [Permissions.EDIT_CHATROOM]: (adminId, user) => adminId === user?.uuid,     [Permissions.READ_SETTINGS_DOCUMENTS]: true,     [Permissions.READ_SETTINGS_MEMBERSHIP]: false,     [Permissions.CREATE_KIDS_ONLY_CHATROOM]: false,   } }

包含應用程式範圍的鉤子。範例:// useWindowSize.ts

import { useLayoutEffect, useState } from 'react'   const useWindowSize = () => {   const [windowSize, setWindowSize] = useState({ width: 0, height: 0 })   const handleSize = () => {     setWindowSize({       width: window.innerWidth,       height: window.innerHeight     })   }     useLayoutEffect(() => {     handleSize()     window.addEventListener('resize', handleSize)       return () => window.removeEventListener('resize', handleSize)   }, [])     return windowSize }   export default useWindowSize

前端專案架構:高效狀態管理、樣式與實用函式庫

包含狀態管理器的 store。團隊使用 zustand 進行狀態管理。
• alertStore.ts
• userStore.ts
• uiStore.ts
• ...

持有應用程式的全域性樣式。
• layout
• mixin
• variables
• ...

包含各種實用功能,例如防抖、組合、本地儲存管理等。實用檔案根據其功能類別命名,示例:
• common.ts
• storageManager.ts
• validators.ts
• ...

這些規則對於幫助團隊快速開發專案並保持無問題的維護至關重要。這種方法對於小型應用程式(約100–1000小時的前端開發)和長期專案(超過1000小時的前端開發)均有效。相同原則可以根據特定需求進行調整或擴充套件,關鍵在於保持一致的整體方法。

試試這個結構來進行你的專案,或許對你也會很有幫助。記住,設計應用程式是一門藝術!😊


J.D.

專家

相關討論

❖ 相關專欄