本文描述了構建大型 React 應用程序的步驟。在使用 React 創建單頁應用程序時,代碼庫很容易變得雜亂無章。這使得應用程序很難調試,更難更新或擴展代碼庫。

React 生態系統中有很多很好的庫可以用來管理應用程序的某些方面,本文將深入介紹其中的一部分。除此之外,考慮到項目的可伸縮性,本文還列出了一些從項目開始就應該遵循的良好實踐。說到這里,我們開始第一步——如何提前計劃。

從畫板開始

大多數情況下,開發人員都會跳過這一步,因為它與實際代碼無關,但是不要低估它的重要性,稍后你將看到這一點。

為什么要做應用程序計劃

在開發軟件時,開發人員必須管理許多變化的部分。事情很容易出錯。有這么多的不確定性和障礙,每一件事你都不希望它超時。

這是計劃階段可以避免的。在這一階段,你要寫下應用程序的每一個細節。與在腦海中想象整個過程相比,提前預測構建這些單獨的小模塊所需的時間要容易得多。

如果你有多個開發人員在這個大型項目中工作(你會的),有這樣一個文檔將使彼此之間的溝通更加容易。事實上,這個文檔中的內容可以分配給開發人員,這將使每個人都更容易知道其他人在做什么。

最后,因為有了這個文檔,你將對自己在項目上的進展有一個非常好的了解。對于開發人員來說,從他們正在開發的應用程序的一個部分切換到另一個部分,然后再回到這個部分要比他們希望的延后許多,這非常常見。

步驟 1:視圖和組件

我們需要確定應用中每個視圖的外觀和功能。最好的方法是繪制應用程序的每個視圖,使用一個模型工具或在紙上,這樣你就可以很好地了解和計劃每個頁面上的信息和數據。

在上面的模型中,你可以很容易地看到應用程序的子容器和父容器。稍后,這些模型的父容器將是我們的應用程序的頁面,較小的項將放在應用程序的組件文件夾中。繪制好模型后,在其中每個模型中寫上頁面和組件的名稱。

步驟 2:APP 內部的 actions 和 events

在確定了組件之后,計劃將在每個組件中執行的操作。這些操作稍后將從這些組件發出。

考慮一個電子商務網站,它的主屏幕上有一個特色產品列表。列表中的每一項都是項目中的一個單獨組件。組件名稱為 ListItem。

Source

因此,在這個應用程序中,產品部分的組件執行的操作是 getItems。此頁面上的其他一些操作可能包括 getUserDetails、getSearchResults 等。

重點是觀察每個組件上的動作或用戶與應用程序數據的交互。在修改、讀取或刪除數據的地方,請注意每個頁面的操作。

步驟 3:數據和模型

應用程序的每個組件都有一些相關的數據。應用程序的多個組件都使用的相同的數據,將成為集中化狀態樹的一部分。該狀態樹將由redux 管理

該數據由多個組件使用,因此,當它在一個位置被更改時,其他組件也會反映出更改后的值。

在應用程序中列出這些數據,因為這些數據將構成應用程序的模型,你將根據這些值創建應用程序的 reducer。

復制代碼

products: {  productId: {productId, productName, category, image, price},  productId: {productId, productName, category, image, price},  productId: {productId, productName, category, image, price},}

考慮上面的電子商務商店的例子。“特色產品”部分和“新產品”部分所使用的數據類型是相同的,即 products。這將是這個電子商務應用的一個 reducer。

在記錄了你的操作計劃之后,接下來的部分將介紹設置應用程序的數據層的一些細節。

操作、數據源和 API

隨著應用程序的增長,redux store 經常會有冗余的方法和不合理的目錄結構,變得很難維護或更新。

讓我們看看如何做些調整,以確保 redux store 的代碼保持干凈。從一開始就使模塊更具可重用性,可以省去大量的麻煩,盡管這在一開始這可能看起來很麻煩。

API 設計和客戶端應用

在設置數據存儲時,從 API 接收數據的格式對 store 的布局有很大的影響。通常,在將數據提供給 reducer 之前,需要對數據進行格式化。

關于在設計 API 時應該做什么和不應該做什么,有很多爭論。后端框架、應用程序大小等因素會進一步影響 API 的設計。

就像在后端應用程序中一樣,將格式化和映射等實用程序函數保存在單獨的文件夾中。確保這些函數沒有副作用——參見JavaScript Pure Functions

復制代碼

export function formatTweet (tweet, author, authedUser, parentTweet) {  const { id, likes, replies, text, timestamp } = tweet  const { name, avatarURL } = author  return {    name,    id,    timestamp,    text,    avatar: avatarURL,    likes: likes.length,    replies: replies.length,    hasLiked: likes.includes(authedUser),    parent: !parentTweet ? null : {      author: parentTweet.author,      id: parentTweet.id,    }  }}

在上面的代碼片段中,formatTweet 函數向前端應用程序的 tweet 對象插入一個新鍵 parent,并根據參數返回數據,而不會影響到外部數據。

你可以更進一步,將數據映射到預定義的對象,而該對象的結構是特定于前端應用程序的,并且對某些鍵進行了驗證。讓我們討論一下負責進行API 調用的部分。

數據源設計模式

我在本節中描述的這部分內容將被 redux action 直接用于修改狀態。根據應用的大小(以及你有多少時間),你可以通過以下兩種方式中的其中一種設置數據存儲:

  • 不使用 Courier

  • 使用 Courier

不使用 Courier

以這種方式設置數據存儲需要你為每個模型分別定義 GET、POST 和 PUT 請求。

在上圖中,每個組件分派調用不同數據存儲方法的 action。這就是 BlogApi 文件中的 updateBlog 方法。

復制代碼

function updateBlog(blog){   let blog_object = new BlogModel(blog)    axios.put('/blog', { ...blog_object })  .then(function (response) {    console.log(response);  })  .catch(function (error) {    console.log(error);  });}

這種方法節省時間。首先,它還允許你進行修改,而不必過多擔心副作用。但是會有很多冗余代碼,執行批量更新非常耗時。

使用 Courier

從長遠來看,這種方法使維護或更新變得更容易。代碼庫可以很干凈,這樣就省去了通過 axios 進行重復調用的麻煩。

然而,這種方法需要時間來進行初始設置,缺乏靈活性。這是一把雙刃劍,因為它阻止你做一些不尋常的事情。

復制代碼

export default function courier(query, payload) {   let path = `${SITE_URL}`;   path += `/${query.model}`;   if (query.id) path += `/${query.id}`;   if (query.url) path += `/${query.url}`;   if (query.var) path += `?${QueryString.stringify(query.var)}`;       return axios({ url: path, ...payload })     .then(response => response)     .catch(error => ({ error }));}

下面是一個基本的 courier 方法的樣子,所有的 API 處理程序都可以簡單地調用它,通過傳遞以下變量:

  • 一個查詢對象,其中包含 URL 相關的具體信息,如模型名稱、查詢字符串等;

  • Payload,其中包含請求頭和請求體。

API 調用和 App 內部 Action

在使用 redux 時,一個突出的問題是預定義 action 的使用。它使得整個應用程序中的數據變化更加可預測。

盡管在一個大型應用程序中定義一堆常量看起來要做很多工作,但是計劃階段的步驟 2 使它變得更加容易。

復制代碼

export const BOOK_ACTIONS = {   GET:'GET_BOOK',   LIST:'GET_BOOKS',   POST:'POST_BOOK',   UPDATE:'UPDATE_BOOK',   DELETE:'DELETE_BOOK',} export function createBook(book) {   return {      type: BOOK_ACTIONS.POST,    	book   }} export function handleCreateBook (book) {   return (dispatch) => {      return createBookAPI(book)         .then(() => {            dispatch(createBook(book))         })         .catch((e) => {            console.warn('error in creating book', e);            alert('Error Creating book')         })   }} export default {   handleCreateBook,}

上面的代碼片段展示了一種簡單的方法,可以將數據源的 createBookAPI 方法與 redux action 混合在一起。handleCreateBook 方法可以安全地傳遞給 redux 的 dispatch 方法。

另外請注意,上面的代碼位于項目的 actions 目錄中,我們同樣可以為應用程序的其他各種模型創建包含 action 名稱和處理程序的 JavaScript 文件。

Redux 集成

在本節中,我將系統地討論如何擴展 redux 的功能來處理更復雜的應用程序操作。如果實現得不好,這些東西可能會破壞 store 的模式。

JavaScript 生成器函數能夠解決與異步編程相關的許多問題,因為它們可以隨意啟動和停止。Redux Sagas 中間件使用這個概念來管理 app 中不純凈的地方。

管理 App 中不純凈的地方

考慮這樣一個場景。你被要求開發一個房產發現應用程序。客戶想要遷移到一個新的更好的網站。REST API 已經就緒,你已經獲得了 Zapier 上每個頁面的設計,并且已經起草了一個計劃,可是問題來了。

他們公司使用 CMS 客戶端已經很長時間了,他們非常熟悉它,因此不希望僅僅為了寫博客而更換一個新的客戶端。此外,復制所有的舊博客將是一個麻煩。

幸運的是,CMS 有一個可讀的 API,可以提供博客內容。不幸的是,假若你已經編寫了一個 courier 方法,而 CMS API 位于另一個具有不同語法的服務器上。

這是應用中一個不純凈的地方,因為你正在適應一個新的 API,用于簡單地獲取博客。這可以通過使用 React Sagas 來處理。

考慮下面這幅圖。我們使用 Sagas 在后臺獲取博客。這就是整個交互的過程。

這里,組件執行 Dispatch action,即 GET.BLOGS,在應用中,使用 redux 中間件攔截請求,在后臺,生成器函數將從數據存儲中獲取數據并更新 redux。

下面是一個示例,展示了博客 sagas 的生成器函數是什么樣子。你還可以使用 sagas 存儲用戶數據(例如身份驗證令牌),因為這是另一個不純凈的 action。

復制代碼

... function* fetchPosts(action) { if (action.type === WP_POSTS.LIST.REQUESTED) {   try {     const response = yield call(wpGet, {       model: WP_POSTS.MODEL,       contentType: APPLICATION_JSON,       query: action.payload.query,     });     if (response.error) {       yield put({         type: WP_POSTS.LIST.FAILED,         payload: response.error.response.data.msg,       });       return;     }     yield put({       type: WP_POSTS.LIST.SUCCESS,       payload: {         posts: response.data,         total: response.headers['x-wp-total'],         query: action.payload.query,       },       view: action.view,     });   } catch (e) {     yield put({ type: WP_POSTS.LIST.FAILED, payload: e.message });   } }...

它監聽類型為 WP_POSTS.LIST 的操作,然后從 API 獲取數據。它分派另一個 action WP_POSTS.LIST.SUCCESS,然后更新博客 reducer。

Reducer 注入

對于大型應用程序而言,預先規劃每一個模型是不可能的,而且,隨著應用程序的增長,這種技術節省了大量的工時,它還允許開發人員添加新的 reducer,而無需重新布局整個 store。

有一些可以讓你立即完成這項工作,但是我更喜歡這種方法,因為你可以靈活地將它與舊代碼集成在一起,而不需要太多的重新布局。

這是一種代碼分割的形式,正在被社區積極采用。我將使用這個代碼片段作為一個例子來展示 reducer 注入器的樣子及其工作原理。讓我們先看看它是如何與 redux 集成的。

復制代碼

... const withConnect = connect( mapStateToProps, mapDispatchToProps,); const withReducer = injectReducer({ key: BLOG_VIEW, reducer: blogReducer,}); class BlogPage extends React.Component {  ...} export default compose( withReducer, withConnect,)(BlogPage);

上面的代碼是 BlogPage.js 的一部分,它是我們應用程序的組件。

這里我們導出的不是 connect 而是 compose,這是 redux 庫中的另一個函數,它所做的是,允許你傳遞多個函數,這些函數可以從左到右讀取,也可以從下到上讀取。

Compose 所做的就是讓你編寫深度嵌套的函數轉換,而不需要右移代碼。不要太相信它!—— 來自Redux 文檔

最左邊的函數可以接收多個參數,但之后只有一個參數傳遞給該函數。最終,將使用最右邊函數的簽名。這就是我們將 withConnect 作為最后一個參數傳遞的原因,這樣 compose 就可以像 connect 一樣使用了。

路由和 Redux

人們喜歡在他們的應用程序中使用一系列工具來處理路由,但在本節中,為了使用 redux,我將堅持使用react router dom并擴展它的功能。

使用 react router 最常見的方法是用 BrowserRouter 標記封裝根組件,用 withRouter() 方法封裝子容器并輸出它們示例

通過這種方式,子組件接收到一個 history prop,其中包括一些特定于用戶會話的屬性和一些可用于控制導航的方法。

在大型應用程序中,以這種方式實現可能會引起問題,因為沒有 history 對象的中心視圖。此外,沒有像這樣通過 route 組件渲染的組件不能訪問它:

復制代碼

[code language="plain"]
[/code]

封裝在 React.Suspense 中的組件會在加載主要內容時加載后備 prop 中指定的組件。務必確保后備 prop 中的組件是輕量級的。

使用 Suspense

自適應組件

在一個大型前端應用程序中,重復的模式開始出現,即使它們起初可能不那么明顯。你不禁覺得,自己以前竟然干過這種事。

例如,在你正在構建的應用程序中有兩種模型:賽道和汽車。汽車列表頁面有正方形的平鋪塊,每個平鋪塊上都有一幅圖像和一些描述。

而賽道列表頁面有一幅圖像和一些描述,以及一個小框,表明賽道是否提供食物。

上面的兩個組件在樣式(背景顏色)上有一點不同,而賽道平鋪塊上有額外的信息。這個例子中只有兩個模型。大型應用程序中會有很多模型,為每個模型創建單獨的組件是有悖常理的。

你可以通過創建可以感知其加載上下文的自適應組件來避免重寫類似的代碼。考慮下應用搜索欄。

它將在應用程序的多個頁面上使用,功能和外觀略有不同。例如,它在主頁上會稍大一些。要處理這個問題,你可以創建一個單獨的組件,它將根據傳遞給它的 prop 進行渲染。

復制代碼

static propTypes = {  open: PropTypes.bool.isRequired,  setOpen: PropTypes.func.isRequired,  goTo: PropTypes.func.isRequired,};

使用此方法,還可以在這些組件中切換 HTML 類,以控制它們的外觀。

另外一個可以使用自適應組件的例子是分頁助手。應用程序的幾乎每個頁面都有它,它們或多或少是相同的。

如果你的 API 遵循不變的設計模式,那么你唯一需要傳遞給自適應分頁組件的 prop 就是 URL 和每個頁面上要顯示的項。

結論

多年來,React 生態系統已經成熟,以至于幾乎沒有必要在開發的任何階段重新造輪子。雖然這非常有用,但也導致你在選擇適合項目的組件時更加復雜。

每個項目在規模和功能方面都是不同的。沒有一種方法或泛化每次都有效,因此,在實際編碼開始之前有一個計劃是必要的。

在這樣做的時候,很容易就能識別出哪些工具適合你,哪些工具是多余的。一個只有 2-3 個頁面和最少 API 調用的應用不需要像上面討論的那樣復雜的數據存儲。我想說的是,小項目不需要 REDUX

當我們提前計劃并繪制出應用中將要出現的組件時,我們可以看到頁面之間有很多重復。只需重用代碼或編寫智能組件就可以節省大量的工作。

最后,我想說的是,數據是每個軟件項目的支柱,對于 React 應用程序也是如此。隨著應用的增長,數據量和與之相關的操作很容易讓程序員應接不暇。事實證明,預先確定關注點(如數據存儲、reducer action、sagas 等)可以帶來巨大的優勢,并使得編寫它們變得更加有趣。

Comments are closed.