一個(gè)簡(jiǎn)潔、強(qiáng)大、可擴(kuò)展的前端項(xiàng)目架構(gòu)是什么樣的?

大家好,我小盧,今天分享卡頌哥一篇文章。

React技術(shù)棧的一大優(yōu)勢(shì)在于 —— 社區(qū)繁榮,你業(yè)務(wù)中需要實(shí)現(xiàn)的功能基本都能找到對(duì)應(yīng)的開(kāi)源庫(kù)。

但繁榮也有不好的一面 —— 要實(shí)現(xiàn)同樣的功能,有太多選擇,到底選哪個(gè)?

本文要介紹一個(gè)12.7k的開(kāi)源項(xiàng)目 —— Bulletproof React[1]

這個(gè)項(xiàng)目為構(gòu)建「簡(jiǎn)潔、強(qiáng)大、可擴(kuò)展的前端項(xiàng)目架構(gòu)」的方方面面給出了建議。

Bulletproof React是什么
Bulletproof React與我們常見(jiàn)的腳手架(比如CRA)不同,后者的作用是「根據(jù)模版創(chuàng)建一個(gè)新項(xiàng)目」。

而前者包含一個(gè)完整的React全棧論壇項(xiàng)目:


用戶(hù)登錄頁(yè)面
作者通過(guò)這個(gè)項(xiàng)目舉例,展示了與「項(xiàng)目架構(gòu)」相關(guān)的13個(gè)方面的內(nèi)容,比如:

文件目錄該如何組織

工程化配置有什么推薦

寫(xiě)業(yè)務(wù)組件時(shí)該怎么規(guī)范

怎么做狀態(tài)管理

API層如何設(shè)計(jì)

等等......


限于篇幅有限,本文介紹其中部分觀點(diǎn)。

不知道這些觀點(diǎn)你是否認(rèn)同呢?

文件目錄如何組織
項(xiàng)目推薦如下目錄形式:

src
|
+-- assets            # 靜態(tài)資源
|
+-- components        # 公共組件
|
+-- config            # 全局配置
|
+-- features          # 特性
|
+-- hooks             # 公用hooks
|
+-- lib               # 二次導(dǎo)出的第三方庫(kù)
|
+-- providers         # 應(yīng)用中所有providers
|
+-- routes            # 路由配置
|
+-- stores            # 全局狀態(tài)stores
|
+-- test              # 測(cè)試工具、mock服務(wù)器
|
+-- types             # 全局類(lèi)型文件
|
+-- utils             # 通用工具函數(shù)
其中,features目錄與components目錄的區(qū)別在于:

components存放全局公用的組件,而features存放「業(yè)務(wù)相關(guān)特性」。

比如我要開(kāi)發(fā)「評(píng)論」模塊,「評(píng)論」作為一個(gè)特性,與他相關(guān)的所有內(nèi)容都存在于features/comments目錄下。

「評(píng)論」模塊中需要輸入框,輸入框這個(gè)通用組件來(lái)自于components目錄。

所有「特性相關(guān)」的內(nèi)容都會(huì)收斂到features目錄下,具體包括:

src/features/xxx-feature
|
+-- api         # 與特性相關(guān)的請(qǐng)求
|
+-- assets      # 與特性相關(guān)的靜態(tài)資源
|
+-- components  # 與特性相關(guān)的組件
|
+-- hooks       # 與特性相關(guān)的hooks
|
+-- routes      # 與特性相關(guān)的路由
|
+-- stores      # 與特性相關(guān)的狀態(tài)stores
|
+-- types       # 與特性相關(guān)的類(lèi)型申明
|
+-- utils       # 與特性相關(guān)的工具函數(shù)
|
+-- index.ts    # 入口
特性導(dǎo)出的所有內(nèi)容只能通過(guò)統(tǒng)一的入口調(diào)用,比如:






import { CommentBar } from "@/features/comments"
而不是:

import { CommentBar } from "@/features/comments/components/CommentBar
這可以通過(guò)配置ESLint實(shí)現(xiàn):

{
  rules: {
    'no-restricted-imports': [
      'error',
      {
        patterns: ['@/features/*/*'],
      },
    ],
    // ...其他配置
  }
}
相比于將「特性相關(guān)的內(nèi)容」都以「扁平的形式」存放在全局目錄下(比如將特性的hooks存放在全局hooks目錄),以features目錄作為「相關(guān)代碼的集合」能夠有效防止項(xiàng)目體積增大后代碼組織混亂的情況。

怎么做狀態(tài)管理
項(xiàng)目中并不是所有狀態(tài)都需要保存在「中心化的store」中,需要根據(jù)狀態(tài)類(lèi)型區(qū)別對(duì)待。

組件狀態(tài)
對(duì)于組件的局部狀態(tài),如果只有組件自身以及他的子孫組件需要這部分狀態(tài),那么可以用useState或useReducer保存他們。

應(yīng)用狀態(tài)
與應(yīng)用交互相關(guān)的狀態(tài),比如「打開(kāi)彈窗」、「通知」、「改變黑夜模式」等,應(yīng)該遵循「將狀態(tài)盡可能靠近使用他的組件」的原則,不要什么狀態(tài)都定義為「全局狀態(tài)」。

以Bulletproof React中的示例項(xiàng)目舉例,首先定義「通知相關(guān)的狀態(tài)」:

// 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),
    })),
}));
再在任何使用「通知相關(guān)的狀態(tài)」的地方引用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>
  );
};
這里使用的狀態(tài)管理工具是zustand,除此之外還有很多可選方案:

context + hooks
redux + redux toolkit
mobx
constate
jotai
recoil
xstate
這些方案各有特點(diǎn),但他們都是為了處理「應(yīng)用狀態(tài)」。

服務(wù)端緩存狀態(tài)
對(duì)于從服務(wù)端請(qǐng)求而來(lái),緩存在前端的數(shù)據(jù),雖然可以用上述處理「應(yīng)用狀態(tài)」的工具解決,但「服務(wù)端緩存狀態(tài)」相比于「應(yīng)用狀態(tài)」,還涉及到「緩存失效」、「序列化數(shù)據(jù)」等問(wèn)題。

所以最好用專(zhuān)門(mén)的工具處理,比如:

react-query - REST + GraphQL

swr - REST + GraphQL

apollo client - GraphQL

urql - GraphQl

表單狀態(tài)
表單數(shù)據(jù)需要區(qū)分「受控」與「非受控」,表單本身還有很多邏輯需要處理(比如「表單校驗(yàn)」),所以也推薦用專(zhuān)門(mén)的庫(kù)處理這部分狀態(tài),比如:

React Hook Form
Formik
React Final Form
URL狀態(tài)
URL狀態(tài)包括:

url params (/app/${dynamicParam})

query params (/app?dynamicParam=1)

這部分狀態(tài)通常是路由庫(kù)處理,比如react-router-dom。






作者:卡頌


歡迎關(guān)注微信公眾號(hào) :前端快快跑