[譯] 構建大型 React 應用程序的最佳實踐

原文連接,點這裏javascript

Posted by Aman Khalid on May 30, 2019前端

以爲不錯的話,點個贊 👍java

  1. Start on the board [從草圖開始]
  2. Actions,數據源和 API
  3. Redux 集成
  4. Dynamic UI at scale

本文將介紹構建大型 React 應用程序的步驟。在使用 React 建立單個頁面應用程序時,代碼庫很容易變得雜亂無章。這致使了很難對應用程序進行調試,甚至使更新或擴展代碼庫變得更加困難。react

在 React 生態系統中有不少不錯的庫能夠用來管理應用程序的某些方面,本文將深刻介紹其中的一些方面。除此以外,若是您考慮到可擴展性,它還列出了從項目開始就要遵循的一些良好實踐。說到這一點,讓咱們邁出第一步 - 如何提早規劃。ios

Start on the board [從草圖開始]

大多數狀況下,開發者們都會習慣性跳過這一環節,由於它與實際編碼無關,但它的重要性不容小覷,稍後您將明白爲何。git

應用規劃階段 - 爲何這樣作?

在開發軟件時,開發者們必須面臨許多存在變數的部分,很容易出現問題。既然有這麼多的不肯定性和障礙,因此不但願在這件事上面花費過多時間。這個問題在規劃階段能夠避免出現,在這一階段,你要寫下應用程序的每個細節。與在腦海中腦補整個過程相比,預測在你面前構建這些單獨的小模塊所需的時間,相比要容易得多。程序員

若是您有多個開發人員在這個大型項目中工做(您將會這樣作),擁有這個文檔將使彼此之間的溝通更加容易。實際上,能夠將此文檔中的內容分配給開發人員,這將使每一個人更容易知道其餘人在作什麼。github

最後,因爲有了這份文檔,您會很是瞭解項目的進展。對於開發人員來講,從他們正在開發的應用程序某個功能(A)切換到另外一個功能(B),而且要從新回到當前功能(A)開發,這其中須要的時間是比他們預期要晚不少的,這是狀況很是常見。web

Step 1: 視圖和組件

咱們須要肯定應用內每一個視圖的外觀和功能。最好的方法是使用模型工具或在紙面上繪製應用程序的每一個視圖,這將使您很好地瞭解您肯定在每一個頁面上擁有哪些信息和數據。編程

資源

在上面的模型中,您能夠很容易地看到應用程序的父子容器。稍後,這些模型的父容器將是咱們應用程序的頁面,較小的部件將放在 component 文件夾中。繪製好模型後,在其中每一個模型中寫上頁面和組件的名稱。

Step 2: 應用程序內的動做和事件

在決定組件以後,規定好在每一個組件中執行的操做。這些操做稍後將從這些組件中發出

在一個電子商務網站,它的主屏幕上有一個特點產品列表,列表中的每一項都是項目中的一個單獨組件,這些單獨的組件被命名爲 ListItem

資源

所以,在這個應用程序中,Product 部分組件執行的操做是 getItems。此頁面上的其餘一些操做可能包括 getUserDetailsgetSearchResults 等。

重點是觀察每一個組件的操做或用戶與應用程序數據的交互。不管在何處修改,讀取或刪除數據,請注意每一個頁面的操做。

Step 3: 數據和模型

應用程序的每一個組件都有一些與之關聯的數據。若是應用程序的多個組件使用相同的數據,它將成爲集中狀態樹的一部分。狀態樹將由 redux 管理。

該數據被多個組件使用,所以當在某個組件對該數據進行更改時,其餘組件也會進行數據更新。

在應用程序中列出這些數據,由於這些數據將構成應用程序的模型,並根據這些值建立應用程序的 reducers。

products: {
  productId: {productId, productName, category, image, price},
  productId: {productId, productName, category, image, price},
  productId: {productId, productName, category, image, price},
}
複製代碼

考慮上面的電子商務商店的例子。feature sectionnew arrival section 所使用的數據類型是相同的,即 product,這將是這個電子商務應用的一個 reducers 之一。

在記錄了您的操做計劃以後,接下來的部分將介紹設置應用程序的數據層所需的一些細節。

Actions, Datasource and API

隨着應用程序的迭代開發,redux store 常常會有冗餘的方法和不正確的目錄結構,而且難以維護或更新。

讓咱們看看如何重整一些東西以確保 redux store 的代碼保持整潔。讓模塊從一開始就更具有可重用性,能夠節省大量的麻煩,雖然剛開始作起來比較棘手。

API 設計和客戶端應用程序

在設置數據存儲時,從 API 接收數據的格式對 store 的佈局有很大的影響。一般,在將數據提供給 reducers 以前,須要對數據進行格式化。

關於在設計 API 時應該作什麼和不該該作什麼,有不少爭論。後端框架、應用程序大小等因素會進一步影響 API 的設計。

就像在後端應用程序中同樣,將格式化程序和映射程序等實用程序函數保存在單獨的文件夾中,確保這些函數沒有反作用 —— 參見 Javascript 純函數

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 對象插入一個新的 key parent,並根據參數返回數據,而不影響外部數據。

您能夠將數據映射到一個預約義的對象,該對象的結構特定於您的前端應用程序,而且對某些鍵進行驗證,從而進一步實現這一點。咱們來談談負責進行 API 調用的部分。

Datasource design patterns

我在本節中描述的部分將經過 redux actions 去修改狀態。根據應用程序的大小(以及您擁有的時間),您能夠經過如下兩種方式之一去存儲數據。

  • Without Courier
  • With Courier

Without Courier

以這種方式存儲數據須要爲每一個模型分別定義 GET、POST 和 PUT 請求。

在上圖中,每一個組件分派調用不一樣數據存儲方法的操做,這就是 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);
  });
}
複製代碼

這種方法能夠節省時間......首先,它還容許您進行修改,而沒必要過多擔憂反作用,但會有不少冗餘代碼,執行批量更新很是耗時。

With 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 調用和應用程序內操做

在使用 redux 時,有一點很突出,那就是使用預約義的操做,它使得整個應用程序中的數據變化更加可預測。

儘管在一個大型應用程序中定義一堆常量看起來要作不少工做,可是規劃階段的 Step 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 actions 混合在一塊兒。 handleCreateBook 方法能夠安全地傳遞給 redux 的 dispatch 方法。

另外,請注意上面的代碼位於項目的 actions 目錄中,咱們一樣能夠爲應用程序的其餘各類模型建立包含操做名稱和處理程序的 javascript 文件。

Redux 集成

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

Javascript generator 函數可以解決與異步編程相關的許多問題,由於它們能夠隨意啓動和中止。Redux Sagas 中間件使用這個概念來管理應用程序的非純方面。

管理應用程序的不純方面

考慮這樣一個場景。你被要求開發一個 real-estate discovery 應用程序。客戶想要遷移到一個新的更好的網站。REST API 已經就緒,您已經得到了 Zapier 上每一個頁面的設計,而且已經起草了一個計劃,但災難仍然存在。

CMS 客戶端已經在他們的公司使用了很長時間,他們很是熟悉它,所以不但願僅僅爲了寫博客而更換一個新的客戶端。此外,複製全部舊博客將是一件麻煩事。

幸運的是,CMS 有一個可讀的 API ,能夠提供博客內容。不幸的是,若是您編寫了一個 courier,那麼 CMS API 位於另外一個具備不一樣語法的服務器上。

這是應用程序的一個不純方面,由於您正在使用一個用於簡單獲取博客的新 API,這能夠經過使用React Sagas來處理。

考慮下圖。咱們使用 Sagas 從後臺獲取博客。這就是整個交互的邏輯。

這裏,組件執行分派操做 GET。博客和使用 redux 中間件的應用程序中的請求將被攔截,在後臺,您的生成器函數將從數據存儲中獲取數據並更新 redux。

下面是一個示例,展現了博客 sagas 的生成器函數是什麼樣子的。您還可使用 sagas 存儲用戶數據(例如auth令牌),由於這是另外一個不純操做。

...
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獲取數據。它分派另外一個動做 WP_POSTS.LIST.SUCCESS,而後更新博客 reducer。

Reducer Injections

對於一個大型的應用程序,預先規劃每個模型是不可能的,並且隨着應用程序的迭代開發,這種技術節省了大量的工時,而且容許開發人員添加新的 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 庫中的另外一個函數,它所作的是,它容許您傳遞多個函數,這些函數能夠從左到右讀取,也能夠從下到上讀取。

All compose does is let you write deeply nested function transformations without the rightward drift of the code. Don't give it too much credit!
(From Redux Documentation)

最左邊的函數能夠接收多個參數,可是在那以後只有一個參數傳遞給函數。最終,將使用最右邊函數的簽名。這就是咱們將 withConnect 做爲最後一個參數傳遞的緣由,這樣組合就能夠像 connect 同樣使用。

路由和 Redux

開發者們喜歡在應用程序中使用一系列工具來處理路由,但在本節中,我將堅持使用 react router dom 並擴展它的功能,以使用redux。

使用 react router 最多見的方法是用 BrowserRouter 標籤包裝根組件,用 withRouter() 方法包裝子容器並導出它們 [示例]。

經過這種方式,子組件接收一個 history prop,該屬性具備特定於用戶會話的某些屬性以及一些可用於控制導航的方法。

因爲沒有 history 對象的中心視圖,以這種方式實現可能會在大型應用程序中引發問題。此外,未經過 route 組件呈現的組件沒法訪問它:

<Route path="/" exact component={HomePage} />
複製代碼

爲了克服這個問題,咱們將使用 connected react router 庫,它容許您經過 dispatch 方法輕鬆地使用路由。集成這一點須要作一些修改,即建立一個專門針對路由的新 reducer(明顯)並添加一個新的中間件。

初始設置後,能夠經過 redux 使用它,應用內導航能夠簡單地經過 dispatching 操做來完成。

要在組件中使用 connected react router,咱們能夠根據您的路由需求簡單地將 dispatch 方法映射到 store。下面是一個片斷,展現了 connected react router 的用法(須要確保初始設置已經完成)。

import { push } from 'connected-react-router'
...
const mapDispatchToProps = dispatch => ({
  goTo: payload => {
    dispatch(push(payload.path));
  },
});

class DemoComponent extends React.Component {
  render() {
    return (
      <Child onClick={ () => { this.props.goTo({ path: `/gallery/`}); } } /> ) } } ... 複製代碼

在上面的代碼示例中,goTo 方法中的 dispatches 操做在瀏覽器的歷史堆棧中推送您想要的 URL。因爲 goTo 方法已被映射到 store,因此它被傳遞給 DemoComponent 一個屬性。

Dynamic UI at scale

有時,儘管有足夠的後端和核心 SPA 邏輯,但因爲組件的實現過於粗糙,用戶界面的某些元素最終會損害整個用戶體驗,這些組件在表面上看起來很是基礎。在本節中,我將討論實現某些小部件的最佳實踐,這些小部件會隨着應用程序的擴展而變得棘手。

Soft Loading and Suspense

關於 javascript 的異步特性,最好的一點是您能夠充分利用瀏覽器的潛力。沒必要等待進程完成後再排隊等待新進程,這確實是一件好事。然而,做爲開發人員,咱們沒法控制網絡和在網絡上加載的資源。

通常來講,網絡層被認爲是不可靠和容易出錯的,不管您的單頁應用程序經過多少次質量檢查,都有一些東西是咱們沒法控制的,好比鏈接性、響應時間等。

但軟件開發人員拋開「那不是我該作的」這一想法,開發了優雅的解決方案來處理這類問題。

前端應用的某些部分,你會想要顯示一些 fallback 內容(一些比你試圖加載的內容更輕量的組件),這樣用戶就不會看到加載後的延時抖動,或者更糟,這個標誌:

裂圖

React suspense 讓你作到這一點,能夠在加載內容時顯示某種類型的加載效果。雖然這能夠經過將 isLoaded 屬性更改成 true 來實現效果,可是使用 suspense 更加簡潔。

這裏瞭解更多關於如何使用它的信息,在這段視頻中,Jared Palmer 介紹了實際應用中的 React suspense 及其一些功能。

沒有使用 Suspense 的效果展現

在組件中添加 suspense 要比在全局狀態中管理 isLoaded 對象容易得多。咱們首先用 React.StrictMode 包裝父容器,確保應用程序中使用的任何 React 模塊都不被棄用。

<React.Suspense fallback={<Spinner size="large" />}>
  <ArtistDetails id={this.props.id}/>
  <ArtistTopTracks />
  <ArtistAlbums id={this.props.id}/>
</React.Suspense>
複製代碼

在加載主要內容時,包裝在 React.Suspense 標記中的組件將加載其 fallback 屬性中指定的組件,確保 fallback 屬性中的組件是輕量的。

使用 Suspense 的效果展現

自適應組件

在一個大型前端應用程序中,重複的模式開始出現,即便它們可能不像開始時那麼明顯。你不由以爲本身之前幹過這種事。

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

然而,賽道列表頁面有一個圖像和描述,以及一個小框,代表賽道是否提供服務。

上面的兩個組件在樣式(背景顏色)上有一點不一樣,而賽道有額外的信息。在這個例子中只有兩個模型,而在大型應用程序中,會有不少組件,爲每一個組件建立單獨的組件是違反直覺的。

經過建立了解它們加載的上下文的自適應組件,您能夠避免重寫相似的代碼。考慮應用程序的搜索欄。

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

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

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

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

若是您的 API 遵循恆定的設計模式,那麼您須要傳遞給自適應分頁組件的惟一屬性是 URL 和每一個頁面的項。

結論

多年來,React 生態系統已經成熟,以致於幾乎沒有必要在開發的任何階段從新造輪子。雖然很是有用,但致使了更多的複雜性爲您的項目在選擇什麼是正確的。

每一個項目在規模和功能方面都是不一樣的。沒有單一的方法或歸納能夠每次都有效,所以,在實際編碼開始以前制定一個規劃是必要的。

在這樣作的時候,很容易就能識別出哪些工具適合您,哪些工具是多餘的。一個只有 2-3 個頁面和極少 API 調用的應用程序不須要像上面討論的那樣複雜的數據存儲。我想說的是,小型項目不須要使用 redux。

當咱們提早規劃分析並繪製出應用程序中將要出現的組件時,咱們能夠看到頁面之間有不少重複部分,只需重用代碼或編寫靈活的組件就能夠節省大量開發成本。

最後,我想說的是,數據是每一個軟件項目的支柱,這對於 React 應用程序也是如此。隨着應用程序的迭代開發,數據量和與之相關的操做很容易讓程序員目不暇接。預先肯定關注點如數據存儲、reducers actions、sagas等,能夠證實是一個巨大的優點,並使編寫它們變得更加有趣。

若是您認爲在建立大型 React 應用程序時,還有其餘庫或方法能夠證實是有用的,請在評論中告訴咱們。但願你喜歡這篇文章,謝謝你的閱讀。

文章中部分標題沒有進行翻譯,本人暫時還想不到如何翻譯比較好,若是有不錯的建議,歡迎評論區告知我,本文有其餘翻譯不合理的地方,也能夠在評論區告知,感激。

相關文章
相關標籤/搜索