React-Redux基礎(三):Redux 在項目中的實際寫法,組合狀態對象的設計和疑問,Provider 和 Router 的衝突原因和解決方法

在上篇博客裏面,我們有說過,之前對 redux 的用法有幾個問題,這裏我們一起來看看:

首先第一個問題,就是 index.js。

我們現在是把 reducer 和 store 全都扔在 index.js 裏面了,這個不太好。

因爲 index.js 一般來說比較專用一些,它就是一個啓動的入口。

我們如果把太多業務代碼直接扔到 index.js 裏面是不太好的。

尤其是像 store,將來會膨脹成一個很大的東西,因爲我們的數據可能會很多,這個是第一個問題,所以我們需要把它拿走。

 

然後第二個問題就是 action。

我們現在寫的都是一堆的字符串,這個太麻煩了。

如果因爲某種原因不得不改名字,那你所有用到的這個名字的地方,全得改。可能有幾百上千個組件,這沒人能記得住,所以這是第二個問題。

 

那麼第三個問題:就是我們現在是單一數據對象,換句話來說,就是隻有一個 reducer。

其實真正在應用當中,我們應該是一個模塊化的 reducer。比如說有用戶數據,購物車數據,等等一大堆東西。

 

當然,還有第四個問題,就是我們直接把 App 放在這裏不太好,是會有問題的,在用 Router 的時候就表現出來了。

 

那麼我們一個個來。

首先,我們先把 store 給拿出去:

第一步,新建 store.js。

當然現在創建一個 store.js 只是簡單起見,做過項目應該知道,以後多了其實應該是個目錄纔對。

然後,createStore 不需要放在 index.js 裏面了,因爲我們已經不是在 index.js 裏面去創建了。

當然 Provider 還放在 index.js 這,因爲我們需要 Provider 幫我們連接上去。

接下來,reducer 和 createStore 也就都可以直接拿走了:

然後我們就可以把這個創建好的 store 對象給它 export 出去,那麼在 index.js 裏面就直接引用了:

這個時候 index.js 就變得更加的專用,簡單了。

因爲大家維護程序的時候,都是從 index.js 這個入口文件開始看起,如果太亂了,太雜了,就不方便。

 

接下來我們來看第二個問題:

就是 action 的名字都是以一個字符串的形式存在,這是一個非常巨大的風險。

所以我們可以把 actions 都單獨的獨立出來,創建一個 actions.js 文件:

那麼怎麼獨立呢?很簡單,我們只需要能夠把字符串輸出出來就行:

並且千萬別忘了,我們用到 setname 的組件裏面也需要更改:

所以,以後如果某天我們需要改這個東西,直接在 actions 裏面更改就行了。

比如我將 setname 改成了 set_name:

可以看到,是沒有問題的。因爲我這一改,所有的地方全都改了,這樣我們就會方便很多。

 

一般我們如果光是去封裝 action,其實還是不夠的,倒不是說完全不行,只不過還是有點麻煩。

因爲有可能在很多組件裏面,都會要用到 setname 這個方法。比如說,我在頭像裏面可以修改名字,個人資料也可以修改名字,羣聊裏面還是可以修改名字。所以一個操作其實它是有可能在很多個組件裏面需要跨着來用的。

所以其實我們還可以更近一步,把這個東西封裝的更深一點。

我們直接對外輸出一個 function,名字叫啥無所謂,都是可以改的:

這個 setname 函數,其實就是我們在 Cmp2 組件裏面用的 setname:

換句話說,我不再把它放到組件裏面來定義了,而是直接提取出來,大家公用:

所以,用到它的地方,連 SET_NAME 都不用引了,我們直接就引入 setname,然後放進去就行了:

這樣的話,所有人用到 setname 的時候,都是同一個函數,這樣就更統一,更好了。

 

那麼現在我們就解決了前兩個問題:

1,我們不可能把整個 store 都放到 index.js 裏面。

因爲這個 store 未來會越膨脹越大,現在只是一個簡單的小 demo 就已經有20多行了,將來真要是一個大程序的話,幾千行都不夠,所以我們會選擇把它拿出來。

2,我們把 action 裏面的字符串,甚至還有操作本身都給拿出來。這樣就會特別的方便。

 

那麼第三個問題:

只有一個 reducer 的時候,我們叫做單一狀態對象。但是我們是不可能只有一個 reducer 的,全在這一個裏面,你的 reducer 函數得寫多大啊。

然後接下來,我們就看看怎麼樣去用所謂的組合狀態對象

redux 裏面除了 createStore,它還有一個方法:combineReducers。

combine 翻譯過來就是組合的意思。看它名字就知道,它能夠把 reducers 給組合到一起。那這個東西怎麼用呢?

首先,我們先準備 2 個 reducer,叫啥名都行。

之前的 reducer 我們改名爲 user,然後在隨便加個 news。

當然,這兩個 reducer 本身都是一樣的結構。這裏需要的注意的一點就是,沒人規定過 state 必須是 json,其實什麼都可以,比如我這裏就定義爲一個數組:

爲了後面方便演示,我們在 news 裏面先隨便加一個 addnews:

那麼現在,我們就已經有了 2 個 reducer,接下來就是時候把他們都合併起來了。

我們需要 2 個步驟:

首先,我們在真正去創建 store 之前,需要做一件事:就是直接用 combineReducers 把它們給合起來。

combineReducers 它裏面用的是一個 json,因爲每個 reducer 都是有名字的,也就是我們所說的命名空間。

所以就是 user 對應 user,news 對應 news。那這裏我們就可以不用寫這麼麻煩,可以直接簡寫:

那麼組合起來之後,我們就隨便起個名字,叫做 reducers 吧,然後把它扔到 createStore 裏面就可以了

然後,如果你嫌麻煩,也可以直接這麼寫:

這樣就不用寫很多名字,github 上面很多項目也都是這種寫法,比較方便。

那麼這個時候,我們就已經對外輸出了一個組合後的 store。

當然,其他位置的代碼是不需要改的,這個組合後的 store,照樣可以扔到它裏面去,是沒有問題的。

然後我們添加一個組件 News.js 來試試,做個實驗:

這裏需要注意的:因爲在 store.js 裏面有了名字,所以用的時候就需要加上 news 前綴。這個 news 指代的就是 state 裏面的那個空數組,所以我們纔可以直接用 map 方法。

包括我們以前在 App 組件裏面用的 this.props.name,現在得改成 this.props.user.name。

因爲它現在有一個命名空間的概念在裏面。

如果我們不加 user 前綴的話,它就找不到數據,這樣是會出事的:

然後我們引入 News 組件,點擊添加按鈕,頁面上就會多出個 aaa,bbb。

並且添加年齡,修改姓名的功能也沒有受到任何的影響。

所以現在,我們就學會了一個新東西,就是如何讓單一的狀態對象,變成一個組合的狀態對象。

這個是非常有用的,因爲在稍微有點規模的項目當中,你不可能只有一個數據,那樣的話是非常麻煩的。

 

然後這裏面還有一個小事,我們需要知道一下,那就是:

如果你的組件那邊發出任何一個 action,那麼實際上來說, user 和 news 這兩個 reducer 會共同執行。

什麼意思呢?一起來看下就知道了。

我們在 user 和 news 裏面都打印一下 console.log:

然後我們現在想做一個年齡 +3 的操作:

你可以看到,其實這兩個 reducer 都有反應,並且它們得到的是一個相同的 action。

那麼這時候,我們就需要知道 2 件事:

1,爲什麼?人家這麼設計是有什麼原因的嗎?

2,這樣對我寫程序有沒有什麼影響?

 

首先第一個,爲什麼這個東西不能有一個命名空間一樣的感覺?爲什麼非得要給它都觸發了?

首先,redux 裏面是故意這麼設計的。那爲什麼要故意來設計這個事呢?

原因很簡單,因爲它考慮到你有可能會通過一個 action,需要讓所有的這些 reducer 都發生一定的變化。

比如我舉個最簡單的例子,假設這個網站有一個服務:

如果你是我們的 VIP,我們的商品就給你打 8 折。這個需求很正常吧?

那麼這時候,假設用戶突然衝了一個 VIP,那這時候你商品裏面的價格要不要有變化?肯定要有變化的,因爲都要乘以 0.8,打8折。

然後你購物車裏面的那些數據要不要有變化?也要。

甚至於包括你的用戶信息,頭像那裏是不是也要有變化?比如多個 VIP 標誌之類的。

所以你這一個操作,有可能引發好多個狀態都去變化。

那麼在這種情況下,我們確實存在着一個 action,然後要觸發多個地方,這種情況。

所以 redux 是故意這麼設計的,它設計成了一個 action 可以同時觸發所有的 reducer。

 

但這時候,大家更多的疑問是:會不會對我們的程序造成影響?

答案是不太會。因爲我們正常的時候,是這麼寫的:

在 user 裏面,我們是有個 case setname 和 case addage。

但是在 news 裏面,我們並沒有去處理 setname 和 addage。

那麼我們上面那個例子的 action,其實它找的是 user 裏面的 addage。 

那麼在 news 裏面,它走的就是 default。也就是說,state 是怎麼樣,就還是怎麼樣,原模原樣的返回出來,並且它不是一個新對象,而是一個老對象。

那麼現在就帶來了 2 個好處:

1,是不是就沒有創建對象的開銷和成本了。

2,就是經過 redux 的檢查,redux 認爲你這個數據對象沒變,所以這時候它也不會通知用到這個數據的組件更新,也就可以防止過多的開銷。

那麼,唯一的一個程序開銷就是 switch,但是它運行的速度是非常快的,你不用擔心。一般我們在工作中,差不多10-20 個就已經頂天了。就算你有幾百上千個,一個 switch 造成的開銷也非常的小,所以完全不用擔心。

所以:

第一,當你提出任何一個 action 的時候,所有的 reducer 函數都會被執行。

第二,對我們的性能沒有影響,你不用擔心。因爲跟你沒關係的東西,它會直接走 default 略過去。

除非我們自己犯了錯誤,把字符串寫成了一樣:

那這個時候,它的確一個 action 會觸發很多個東西,但這個就是我們自己寫代碼的錯誤了。

我們本來就不應該讓它們相同,這樣就不會出事了。

所以我們現在就又理解了一個東西,就是關於組合狀態對象的問題。

 

那麼接下來,我們對 redux 就算是有了一些比較深的認識。但是我們還有另一個事情要說,就是 redux 和 router:

 

redux 是用來共享數據的,同一套數據大家用,每個組件還有一些自己的狀態,但是有一些公共的狀態,這是 redux。

而 router 是根據路徑不同,讓不同的組件蹦出來。

redux 跟 router 這兩個東西,它們本身是不相關的,但是如果我們把它們放在一塊用,就容易出事。

我們來看看是怎麼回事,以及如何解決避免。

 

爲了方便更好的演示,我們重開一個全新的項目:

創建完新項目之後,我們先安裝:npm install redux react-redux react-router-dom -D

首先,在我們的 index.js 裏面,我們不可能讓 App 空着,我們肯定至少要放 2 個東西:

一個是 Router,我們要用 Router 來包住這個 App。同時,我們還要讓 Provider 來包住這個 App。

那我怎麼包?是先放 Router 還是 Provider?

其實無所謂,你先放哪個都可以。

我們直接來試試,首先,先創建 store.js 文件。

這裏就不用 combineReducers 了,因爲有沒有 combineReducers 都不影響它跟 Router 配合的一個問題。

然後在 index.js 裏面引入 store.js,並加上 Provider 和 Router:

然後我們在創建 2 個組件 Cmp1 和 Cmp2。

在 Cmp1 中顯示姓名,這個 name 就是從 store 來的。

並且我們還有個按鈕,我們讓它可以設置年齡 +3。

在 Cmp2 裏面,我們顯示的是年齡,然後反過來改 Cmp1 的姓名。

這裏我們是故意的讓 Cmp1 去改 Cmp2 的東西,Cmp2 裏面在反過來改 Cmp1 的東西。

然後在 App.js 裏面引入,並寫入路由跳轉,因爲我們需要在 App.js 裏面有一個路由關係來方便演示:

然後我們看看能不能正常顯示:

可以看到,Cmp1 和 Cmp2 正常顯示是沒有問題的,那麼我們來稍微的試一下:

好像是挺正常的,那你前面不是說有問題嗎?

別忘了,我們這個例子其實是 redux 和路由綜合來應用的。

所以這時候,我還希望來測驗一下路由的跳轉,那麼這會就出事了:

首先我們在 Cpm2 裏面加個功能,可以跳到 Cpm1中:

然後我們從 Cmp2 跳轉到 Cmp1:

可以看到,還是沒問題。但是注意,你現在內部的 Cpm1,Cmp2 它們都沒問題,但是出問題的是 App。

比如說,我們在 App 裏面不用 Link 來跳轉了,我們用 JS 來做跳轉:

表面上看起來好像一切都沒問題,而且我們剛纔在 Cmp2 裏面已經試過了,是可以跳轉的。

但就是在這個 App 上,它就不能跳轉。

當我們點擊按鈕跳轉的時候:

這時候就跳轉不過去,出事了。

這個就是我們所說的:在組件裏面,想要跳轉沒問題,但是在 App 上是用不了的。

 

那麼是怎麼回事呢?

首先,如果想解決問題的話,千萬別撲騰撲騰一頓改,先分析一下原因在哪,爲什麼會有問題。

我們可以先把 this.props 打印出來看看是什麼:

可以看到,這時候的 this.props 是個空對象。

那麼 this.props.history 自然就是一個 undefined 了,我們在一個 undefined 身上去調用 push,出不來纔是正常的。

 

奇怪了,我們原來沒用 redux 的時候,是沒這個事的,用了 redux 纔有這個問題,爲什麼呢?其實問題就出在爭寵上面。

簡單來說, Provider 和 Router 這兩個東西都在爭 props。

本身我們在用 Router 的時候,它會給 props 上面加一堆東西,比如 history,match 等等,而 Provider 它裏面也要裝一堆東西。

我們在上面已經看到了,App 它身上的 props 已經沒了,是個空對象了。

因爲我們現在需要給 App 去加上 Provider 的各種各樣的數據,所以就導致 Router 給它加的東西沒了。

 

那麼這時候怎麼辦呢?我們有一個小小的辦法來解決。

我們可以先刪掉 App,然後在引入一個 Route:

那麼我們就不用直接寫的方式了,而是用一個 Route,並且這個 Route 它對應的組件是 App:

其實這個時候,應該跟我原來直接寫個App,效果差不多才對。

然後我們跳轉到 Cmp2:

可以看到,跳轉是沒問題的,並且數據也是好的。

那麼我們點下修改姓名,在跳回 Cmp1:

可以看到,不管我們怎麼操作,現在都沒問題了。

說明這種情況下,我們既能夠把 Provider 相關的功能發揮出來,同時又能把 Router 相關的功能發揮出來,就避免了這個問題。

那麼是怎麼回事呢?

很簡單,因爲直接放到 Provider 裏面的那個組件,就是我們剛纔的 App,它身上帶的那個 props 就會被影響到。

而我們給它加了一層 Route,其實是把它保護起來了。

相當於 Provider 有輻射,我們給 App 套層殼,真正承受這個東西的是 Route,我們的 App 並沒有直接暴漏在外,所以 App 還是經由我們的 Route,經過它的封裝,給它調取出來的,它並不是直接放在那的。

所以這種時候,我們的 App 身上跟 Route 相關的那些東西消失,這個問題就可以解決了。

 

這個問題如果往深的說,其實還是因爲 Provider 和 Router 它們兩個人都在搶這個 props 造成的。

當然這也沒辦法,大家都覺得 props 好。

因爲第一,如果 props 它裏面發生變化,這個組件會自動重新渲染。第二,這個 props 還是受保護的,它是隻讀的,無法被修改。總之怎麼看都覺得 props 好,所以大家就都在找這個 props。

而 react-redux 和 react-router-dom 這兩個組件不是一個作者寫的,所以在這個過程當中它們就產生了衝突。

如果我們看過它們的源碼就知道:

直接被 Provider 包住的所有的這些組件,其實都會受到它的影響,它上面一部分的 props 會消失,因爲它會先把它給重新創建一遍,然後在加上自己的 props,這是它內部的原理。

所以在這時候,App 它身上所帶的跟路由相關的屬性就消失了,所以這時候我們要做的是讓 Provider 直接去影響 Route,它沒事,它本身就是路由的東西,所以它不需要 history,match 之類的東西。

所以我們相當於是讓 Route 來承受 Provider 的影響,讓 App 躲在裏面。因爲現在這個 App 並非是直接渲染出來的,而是被 Route 創建出來的,所以 Route就能夠把它那些相應的屬性給加上去。

 

那麼你肯定會有個疑問,爲什麼 App 身上 Provider 相關的東西怎麼沒消失呢?

這是因爲 Router 作者在寫這個代碼的時候,它內部是把它原本的 props 剝離下來,然後不做任何改變,直接又放上去了,所以它不會破壞東西。

 

所以到這爲止,我們就瞭解瞭如何讓 redux 和 router 放在一起來用,就一個:

那就是讓 Route 包裹住 App 就行了。

直接寫 App 的話,App 它裏面的組件是沒事,但如果說我這個 App 裏面就是有路由相關功能的情況下,那這時候你就必須得用 Route 包裹住 App 這種方法了。

這樣做的目的是爲了讓 App 不被直接渲染出來,而是通過 Route 給創建出來,那這時候 Route 就會把路由相關的一些屬性都放到 App 身上去,就沒事了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章