Redux是一個很是流行的狀態管理解決方案,Redux應用執行過程當中的任何一個時刻,都是一個狀態的反映。能夠說,State 驅動了Redux邏輯的運轉。設計一個好的State並不是易事,本文先從設計State時最容易犯的兩個錯誤開始介紹,而後引出如何合理地設計State。前端
以API爲設計State的依據,每每是一個API對應一個子State,State的結構同API返回的數據結構保持一致(或接近一致)。例如,一個博客應用,/posts
接口返回博客列表,返回的數據結構以下:數據庫
[
{
"id": 1,
"title": "Blog Title",
"create_time": "2017-01-10T23:07:43.248Z",
"author": {
"id": 81,
"name": "Mr Shelby"
}
}
...
]
複製代碼
咱們還須要查看一篇博客的詳情,假設經過接口/posts/{id}
獲取博客詳情,經過接口/posts/{id}/comments
獲取博客的評論,返回的數據結構以下:數組
{
"id": 1,
"title": "Blog Title",
"create_time": "2017-01-10T23:07:43.248Z",
"author": {
"id": 81,
"name": "Mr Shelby"
},
"content": "Some really short blog content. "
}
複製代碼
[
{
"id": 41,
"author": "Jack",
"create_time": "2017-01-11T23:07:43.248Z",
"content": "Good article!"
}
...
]
複製代碼
上面三個接口的數據分別做爲3個子State,構成應用全局的State:bash
{
"posts": [
{
"id": 1,
"title": "Blog Title",
"create_time": "2017-01-10T23:07:43.248Z",
"author": {
"id": 81,
"name": "Mr Shelby"
}
},
...
],
"currentPost": {
"id": 1,
"title": "Blog Title",
"create_time": "2017-01-10T23:07:43.248Z",
"author": {
"id": 81,
"name": "Mr Shelby"
},
"content": "Some really short blog content. "
},
"currentComments": [
{
"id": 1,
"author": "Jack",
"create_time": "2017-01-11T23:07:43.248Z",
"content": "Good article!"
},
...
]
}
複製代碼
這個State中,posts和currentPost存在不少重複的信息,並且posts、currentComments是數組類型的結構,不便於查找,每次查找某條記錄時,都須要遍歷整個數組。這些問題本質上是由於API是基於服務端邏輯設計的,而不是基於應用的狀態設計的。好比,雖然獲取博客列表時,已經獲取了每篇博客的標題、做者等基本信息,但對於獲取博客詳情的API來講,根據API的設計原則,這個API依然應該包含博客的這些基本信息,而不能只是返回博客的內容。再好比,posts、currentComments之因此返回數組結構,是考慮到數據的順序、分頁等因素。服務器
既然不能依據API設計State,不少人又會走到另一個反面,基於頁面UI設計State。頁面UI須要什麼樣的數據和數據格式,State就設計成什麼樣。咱們以todo應用爲例,頁面會有三種狀態:顯示全部的事項,顯然全部的已辦事項和顯示全部的待辦事項。以頁面UI爲設計State的依據,那麼State將是這樣的:數據結構
{
"all": [
{
"id": 1,
"text": "todo 1",
"completed": false
},
{
"id": 2,
"text": "todo 2",
"completed": true
}
],
"uncompleted": [
{
"id": 1,
"text": "todo 1",
"completed": false
}
],
"completed": [
{
"id": 2,
"text": "todo 2",
"completed": false
}
]
}
複製代碼
這個State對於展現UI的組件來講,使用起來很是方便,當前應用處於哪一種狀態,就用對應狀態的數組類型的數據渲染UI,不用作任何的中間數據轉換。但這種State存在的問題也很容易被發現,一是這種State依然存在數據重複的問題;二是當新增或修改一條記錄時,須要修改不止一個地方。例如,當新增一條記錄時,all和uncompleted這兩個數組都要添加這條新增記錄。這種類型的State,既會形成存儲的浪費,又會存在數據不一致的風險。app
這兩種設計State的方式其實是兩種極端的設計方式,實際項目中,徹底按照這兩種方式設計State的開發者並很少,但絕大部分人都會受到這兩種設計方式的影響。請回憶一下,你是否有過把某個API返回的數據原封不動的做爲State的一部分?又是否有過,爲了組件渲染方便,專門爲某個組件的UI定義一個State?post
下面咱們來看一下應該如何合理地設計State。最重要最核心的原則是像設計數據庫同樣設計State。把State看作一個數據庫,State中的每一部分狀態看作數據庫中的一張表,狀態中的每個字段對應表的一個字段。設計一個數據庫,應該遵循如下三個原則:spa
這三個原則,能夠翻譯出設計State時的原則:翻譯
按照這三個原則,咱們從新設計博客應用的State。按領域劃分,State能夠拆分爲三個子State: posts、comments、authors,posts中的記錄以博客的id爲key值,包含title、create_time、author、comments,一樣的方式能夠設計出comments、authors的結構,最終State的結構以下:
{
"posts": {
"1": {
"id": 1,
"title": "Blog Title",
"content": "Some really short blog content.",
"created_at": "2016-01-11T23:07:43.248Z",
"author": 81,
"comments": [
352
]
},
...
},
"comments": {
"352": {
"id": 352,
"content": "Good article!",
"author": 41
},
...
},
"authors": {
"41": {
"id": 41,
"name": "Jack"
},
"81": {
"id": 81,
"name": "Mr Shelby"
},
...
}
}
複製代碼
如今這個State看起來是否是很像有三張表的數據庫呢?但這個State還有不知足應用需求的地方:鍵值對的存儲方式沒法保證博客列表數據的順序,但對於博客列表,有序性顯然是須要的。解決這個問題,咱們能夠經過定義另一個狀態postIds,以數組格式存儲博客的id:
{
"posts": {
"1": {
"id": 1,
"title": "Blog Title",
"content": "Some really short blog content.",
"created_at": "2016-01-11T23:07:43.248Z",
"author": 81,
"comments": [
352
]
},
...
},
"postIds": [1, ...],
"comments": {
"352": {
"id": 352,
"content": "Good article!",
"author": 41
},
...
},
"authors": {
"41": {
"id": 41,
"name": "Jack"
},
"81": {
"id": 81,
"name": "Mr Shelby"
},
...
}
}
複製代碼
這樣,當顯示博客列表時,根據postIds獲取列表順序,而後根據博客id從posts中獲取博客的信息。這個地方有些同窗可能有疑惑,認爲posts和postIds都保存了id數據,違反了不一樣State間不能有重複數據的原則。但其實這並非重複數據,postIds保存的數據是博客列表的順序,只不過「順序」這個數據是經過博客id來體現的。這和一張表的主鍵同時能夠用做另一張表的外鍵,是一樣的道理。一樣須要注意的是,當新增長一條博客時,posts和postId這兩個狀態都要進行修改。這看似變得麻煩,不如直接使用一個數組類型的狀態操做簡單,可是當須要修改某一篇博客的數據時,這種結構就有了明顯的優點,並且直接使用數組保存狀態,會存在對象嵌套層級過深的問題,想象下訪問評論的內容,須要經過相似posts[0].comments[0].content
三層結構才能獲取到,當業務越複雜,這個問題越突出。扁平化的State,才具備更好的靈活性和擴展性。
截至目前爲止,咱們的State都是根據後臺API返回的領域數據進行設計的,但實際上,應用的State,不只包含領域數據,還須要包含應用的UI邏輯數據,例如根據當前是否正在與服務器通訊,處理頁面的加載效果;當應用運行出錯時,須要顯示錯誤信息等。這時,State的結構以下:
{
"isFetching": false,
"error": "",
"posts": {
...
},
"postIds": [1, ...],
"comments": {
...
},
"authors": {
...
}
}
複製代碼
隨着應用業務邏輯的增長,State的第一層級的節點也會變得愈來愈多。這時候咱們每每會考慮合併關聯性較強的節點數據,而後經過拆分reducer的方式,讓每個子reducer處理一個節點的狀態邏輯。這個例子中,咱們能夠把posts、postIds進行合併,同時狀態名作了調整,把isFetching、error做爲全局的UI邏輯狀態合併:
{
"app":{
"isFetching": false,
"error": "",
},
"posts":{
"byId": {
"1": {
...
},
...
},
"allIds": [1, ...],
}
"comments": {
...
},
"authors": {
...
}
}
複製代碼
這樣,咱們就能夠定義appReducer、postsReducer、commentsReducer、authorsReducer四個reducer分別處理4個子狀態。至此,State的結構設計完成。
總結一下,設計Redux State的關鍵在於,像設計數據庫同樣設計State。把State看做應用在內存中的一個數據庫,action、reducer等看做操做這個數據庫的SQL語句。
歡迎關注個人公衆號:老幹部的大前端,領取21本大前端精選書籍!