一個簡潔、強大、可擴展的前端項目架構是什麼樣的?
大家好,我卡頌。
React
技術棧的一大優勢在於 —— 社區繁榮,你業務中需要實現的功能基本都能找到對應的開源庫。
但繁榮也有不好的一面 —— 要實現同樣的功能,有太多選擇,到底選哪個?
本文要介紹一個 12.7k 的開源項目 —— Bulletproof React[1]
這個項目爲構建 「簡潔、強大、可擴展的前端項目架構」 的方方面面給出了建議。
Bulletproof React 是什麼
Bulletproof React
與我們常見的腳手架(比如CRA
)不同,後者的作用是 「根據模版創建一個新項目」。
而前者包含一個完整的React
全棧論壇項目:
作者通過這個項目舉例,展示了與 「項目架構」 相關的 13 個方面的內容,比如:
-
文件目錄該如何組織
-
工程化配置有什麼推薦
-
寫業務組件時該怎麼規範
-
怎麼做狀態管理
-
API
層如何設計 -
等等......
限於篇幅有限,本文介紹其中部分觀點。
不知道這些觀點你是否認同呢?
文件目錄如何組織
項目推薦如下目錄形式:
src
|
+-- assets # 靜態資源
|
+-- components # 公共組件
|
+-- config # 全局配置
|
+-- features # 特性
|
+-- hooks # 公用hooks
|
+-- lib # 二次導出的第三方庫
|
+-- providers # 應用中所有providers
|
+-- routes # 路由配置
|
+-- stores # 全局狀態stores
|
+-- test # 測試工具、mock服務器
|
+-- types # 全局類型文件
|
+-- utils # 通用工具函數
其中,features
目錄與components
目錄的區別在於:
components
存放全局公用的組件,而features
存放 「業務相關特性」。
比如我要開發 「評論」模塊,「評論」 作爲一個特性,與他相關的所有內容都存在於features/comments
目錄下。
「評論」 模塊中需要輸入框,輸入框這個通用組件來自於components
目錄。
所有 「特性相關」 的內容都會收斂到features
目錄下,具體包括:
src/features/xxx-feature
|
+-- api # 與特性相關的請求
|
+-- assets # 與特性相關的靜態資源
|
+-- components # 與特性相關的組件
|
+-- hooks # 與特性相關的hooks
|
+-- routes # 與特性相關的路由
|
+-- stores # 與特性相關的狀態stores
|
+-- types # 與特性相關的類型申明
|
+-- utils # 與特性相關的工具函數
|
+-- index.ts # 入口
特性導出的所有內容只能通過統一的入口調用,比如:
import { CommentBar } from "@/features/comments"
而不是:
import { CommentBar } from "@/features/comments/components/CommentBar
這可以通過配置ESLint
實現:
{
rules: {
'no-restricted-imports': [
'error',
{
patterns: ['@/features/*/*'],
},
],
// ...其他配置
}
}
相比於將 「特性相關的內容」 都以 「扁平的形式」 存放在全局目錄下(比如將特性的 hooks 存放在全局 hooks 目錄),以features
目錄作爲 「相關代碼的集合」 能夠有效防止項目體積增大後代碼組織混亂的情況。
怎麼做狀態管理
項目中並不是所有狀態都需要保存在 「中心化的 store」 中,需要根據狀態類型區別對待。
組件狀態
對於組件的局部狀態,如果只有組件自身以及他的子孫組件需要這部分狀態,那麼可以用useState
或useReducer
保存他們。
應用狀態
與應用交互相關的狀態,比如 「打開彈窗」、「通知」、「改變黑夜模式」 等,應該遵循 「將狀態儘可能靠近使用他的組件」 的原則,不要什麼狀態都定義爲 「全局狀態」。
以Bulletproof React
中的示例項目舉例,首先定義 「通知相關的狀態」:
// bulletproof-react/src/stores/notifications.ts
export const useNotificationStore = create<NotificationsStore>((set) => ({
notifications: [],
addNotification: (notification) =>
set((state) => ({
notifications: [...state.notifications, { id: nanoid(), ...notification }],
})),
dismissNotification: (id) =>
set((state) => ({
notifications: state.notifications.filter((notification) => notification.id !== id),
})),
}));
再在任何使用 「通知相關的狀態」 的地方引用useNotificationStore
,比如:
// bulletproof-react/src/components/Notifications/Notifications.tsx
import { useNotificationStore } from '@/stores/notifications';
import { Notification } from './Notification';
export const Notifications = () => {
const { notifications, dismissNotification } = useNotificationStore();
return (
<div
>
{notifications.map((notification) => (
<Notification
key={notification.id}
notification={notification}
onDismiss={dismissNotification}
/>
))}
</div>
);
};
這裏使用的狀態管理工具是zustand
,除此之外還有很多可選方案:
-
context
+hooks
-
redux
+redux toolkit
-
mobx
-
constate
-
jotai
-
recoil
-
xstate
這些方案各有特點,但他們都是爲了處理**「應用狀態」**。
服務端緩存狀態
對於從服務端請求而來,緩存在前端的數據,雖然可以用上述處理 「應用狀態」 的工具解決,但 「服務端緩存狀態」 相比於 「應用狀態」,還涉及到 「緩存失效」、「序列化數據」 等問題。
所以最好用專門的工具處理,比如:
-
react-query - REST
+GraphQL
-
swr - REST
+GraphQL
-
apollo client
-GraphQL
-
urql
-GraphQl
表單狀態
表單數據需要區分 「受控」 與 「非受控」,表單本身還有很多邏輯需要處理(比如 「表單校驗」 ),所以也推薦用專門的庫處理這部分狀態,比如:
-
React Hook Form
-
Formik
-
React Final Form
URL 狀態
URL
狀態包括:
-
url params
(/app/${dynamicParam}) -
query params
(/app?dynamicParam=1)
這部分狀態通常是路由庫處理,比如react-router-dom
。
總結
本文節選了部分Bulletproof React
中推薦的方案,有沒有讓你認可的觀點呢?
參考資料
[1] Bulletproof React: https://github.com/alan2207/bulletproof-react
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/vctWciAp1-AMtiMMUOz_xg