這篇文章相對較長,是我重構的一次記錄。請耐住性子,慢慢看下去 ~ 爲了方便理解,函數/變量的取名有些 low,emmm,你們不要介意~react
前幾天,中途臨時接到一個需求,複雜程度雖然不高,但也不低,時間很趕(本來半個月,硬生生五天五夜肛完),基於這個需求,爲了遇上送測,代碼懟上去的,bug 相對較多,這不,送測階段,回過頭去看這模塊的代碼(本身都看不下去)...決定,利用這週末時間,用 react hooks 進行重構一波 ~redux
爲何要用 hooks 進行重構,這是由於基於業務邏輯,可能用 hooks 會更加方便且清晰,同時我的以前也只是看了看 hooks 的文檔,使用了一些簡單的 API,此次想借此機會,好好學一下 hooks ~promise
廢話很少說,直接看需求吧 ~async
真實業務需求已被我和諧,首先,咱們有一個頁面,這個頁面是這樣的 ~ 應該都能理解這個組件是長什麼樣了吧?函數
給大家簡單畫一下,就是這個樣子 👇學習
這下子應該懂了吧 ~ 咱們繼續看一下需求是什麼 :fetch
這麼一看,其實並不複雜啊,可是問題在於 :ui
全部的請求,都在各自的組件中進行,你不可能在A組件中,把B、C組件的請求邏輯copy一遍this
上邊只是寫的 A、B、C 組件,可是實際上,真實觸發此操做的是在它們的子組件進行url
我已經把這些請求、賦值等都作完了,這時候才知道須要更新,但我並不想去動原先的代碼
真實的業務場景更加複雜,好比,這個頁面父組件的顯示,還依賴於 tabs 的值(舉例,tabs = 場景1
,pageContainerData = 場景1
, tabs = 場景2
,pageContainerData = 場景2
)
也就是這個頁面父組件,它顯示的數據,會根據 Tabs 的不一樣,顯示不一樣
以右側-C 組件爲例子,它是一個列表,存在着更新
、刪除
操做,那麼它的代碼就是這樣的
// 組件C
componentDidMount() {
this.fetchList();
}
fetchList = () => {
if (tabs === '場景1') {
fetchList1()
} else if (tabs === '場景2') {
fetchList2()
}
}
handleUpdate = () => {
// 刷新邏輯
// ...
this.fetchList()
}
handleDelete = () => {
// 刪除邏輯
// ...
this.fetchList()
}
render() {
const data = tabs === '場景1' ? list1 : list2;
return (
<List data={data} deleteCallback={this.handleDelete} updateCallback={this.handleUpdate} /> ) } 複製代碼
看出問題了嗎,A、B、C 組件,每次在請求、渲染以前,都要判斷當前 reducer 中 tabs 的值。真實業務複雜程度相對較高,舉個例子,組件 B 的邏輯多是這樣的 👇
// 組件B
componentDidMount() {
if (tabs === '場景1') {
// 獲取列表
promisify(getList1)(params, res => {
if (res.code === 0) {
storeToRedux('場景1-列表', res.data);
// 根據列表第一條數據id,獲取詳情
getDetail(res.data[0].id)
}
})
} else if (tabs === '場景2') {
// 獲取列表
promisify(getList2)(params, res => {
if (res.code === 0) {
storeToRedux('場景2-列表', res.data);
// 根據列表第一條數據id,獲取詳情
getDetail(res.data[0].id)
}
})
}
}
render() {
const data = tabs === '場景1' ? list1 : list2;
const detail = tabs === '場景1' ? detail1 : detail12;
return (
// ...
)
}
複製代碼
咱們來想一想,A、B、C 組件,都須要這麼寫,累不累,麻不麻煩?咱們再來看另外一個問題
上邊也給出圖片了,有數據的時候,顯示內容頁(B、C 組件),無數據的時候,須要顯示缺省組件,那麼代碼可能就是這樣的
// 爲了更加容易理解,取名就比較直觀,don't care ~
render() {
const bList = tabs === '場景1' ? reduxBList1 : reduxBList2;
const cList = tabs === '場景2' ? reduxCList1 : reduxCList2;
// ... 若是更多,那就會寫的更多
return (
<div>
{bList.length === 0 && cList.length === 0 && (
<Empty />
) : (
<div>
<B-Component />
<C-Component />
</div>
)}
</div>
)
}
複製代碼
可能會以爲,不這麼寫,還能怎麼寫??咱們繼續往下看
這個是最難受的一個問題,由於真的時間緊,我不想改動原先的代碼,因此我用了一個很蠢的辦法,就是在 redux 中定義變量,用於通知更新。因此代碼就是這樣的,我以 B 組件爲例子
// redux
let initRedux = Immutable({
noticeUpdateToA: false, // 通知A組件進行更新
noticeUpdateToB: false, // 通知B組件進行更新
noticeUpdateToC: false // 通知C組件進行更新
});
複製代碼
而後無論如何,在執行完操做以後,都會修改 redux 中的這些值,同時在組件的 componentWillReceiveProps
中監聽
// 組件B
componentDidMount() {
this.fetchList();
}
componentWillReceiveProps(nextProps) {
if (nextProps && nextProps.noticeUpdateToB) {
this.fetchList();
}
}
fetchList = () => {
if (tabs === '場景1') {
// 獲取列表
promisify(getList1)(params, res => {
if (res.code === 0) {
storeToRedux('場景1-列表', res.data);
// 根據列表第一條數據id,獲取詳情
getDetail(res.data[0].id)
// ❗❗❗ 須要改成false
storeToRedux({
noticeUpdateToB: false
})
}
})
} else if (tabs === '場景2') {
// 獲取列表
promisify(getList2)(params, res => {
if (res.code === 0) {
storeToRedux('場景2-列表', res.data);
// 根據列表第一條數據id,獲取詳情
getDetail(res.data[0].id)
// ❗❗❗ 須要改成false
storeToRedux({
noticeUpdateToB: false
})
}
})
}
}
複製代碼
這波操做,是真的騷啊,可是,你就會發現,真的太噁心了!!!
並且有時候,一個請求,會發送兩遍,你想一想,一個組件,在它的DidMount
和updateMount
週期,去發送請求,而後這個請求,根據 tabs 不一樣,請求的url不一樣,請求回來了,還要根據返回的數據,再一次請求詳情,而後再作一些其它 🐓 兒的操做。
關鍵是,這還不是一個組件,三個組件都這樣,說不定以後這塊複雜起來,更加難以維護!!!
每一個組件,都要引入connect、引入 bindActionCreators ,而後本身還要寫connect(mapStateToProps, mapDispatchToProps),這裏你能夠寫一個管理當前的connectReducer函數,在這個函數中處理connect,這裏我不過多介紹 ~ 我會在結尾的彩蛋中直接貼代碼 ~
忍無可忍,因而去拿了一張 A4 紙,把現階段的一個流程圖及關係圖畫了出來,同時理清楚了每個思路,而後畫了一下重構以後的關係圖,而且諮詢了一下導師,終於,在今天,踏出了第一步。
這個圖不知道能不能說的清楚,大體就是這樣的 :
封裝一個 hooks,用於獲取當前的 tabs,而後在頁面中,若是要用到就直接引入這個 hooks 便可
封裝A 組件的請求,在外部的調用,無需在意 tabs 是什麼,總之,我引用這個 hooks,就只須要你發起 dispatch action 就行了。
其它組件請求也是這樣,同時獲取數據,也寫一個 hooks,只須要返回我想要的結果,不須要我本身進行判斷 tabs
由於這個 tabs 是存在 redux 中的,咱們在每一個頁面都去寫 connect 吧,多累呀 ~
下邊咱們以 頭部-C 組件 來舉例,看看重構後的最終效果 ~
/** * @Desc 自定義hooks * @Author pengdaokuan */
import { useAsyncFn } from 'react-use';
import { useDispatch, useSelector } from 'react-redux';
/** * @desc 當前 tabs = 場景1 */
export function useTabsType() {
const tabsType = useSelector(state => state.global.tabs);
const isTabsScense1 = () => {
return tabsType === '場景1';
};
return isTabsScense1;
}
/** * @desc 跳轉到詳情頁面 * @param {String} uid - 詳情信息的uid */
export function useHandleDetails() {
const handleToDetails = uid => {
const url = `/juejin/author/pengdaokuan/${uid}`;
window.open(window.location.origin + url, '_blank');
};
return handleToDetails;
}
/** * @desc 獲取組件C的列表數據 */
export function useFetchC_List() {
const isTabs1 = useTabsType();
const tabs1ActionName = 'FETCH_TABS_1_SHOP_LIST';
const tabs2ActionName = 'FETCH_TABS_2_SHOP_LIST';
const resuktActionName = isTabs1() ? tabs1ActionName : tabs2ActionName;
const dispatch = useDispatch();
const result = useAsyncFn(async () => {
const useAction = await dispatch(resuktActionName);
return useAction;
});
return result;
}
/** * @desc 獲取當前tabs對應的數據 */
export function useCurrentTabsData() {
const isTabs1 = useTabsType();
// 場景1
const redux1_listA_data = useSelector(state => state.redux1.listA_data);
const redux1_listB_data = useSelector(state => state.redux1.listB_data);
const redux1_listC_data = useSelector(state => state.redux1.listB_data);
// 場景2
const redux2_listA_data = useSelector(state => state.redux2.listA_data);
const redux2_listB_data = useSelector(state => state.redux2.listB_data);
const redux2_listC_data = useSelector(state => state.redux2.listB_data);
let tabsData = {};
if (isTabs1()) {
tabsData = {
listA_data: redux1_listA_data,
listB_data: redux1_listB_data,
listC_data: redux1_listC_data
};
} else {
tabsData = {
listA_data: redux2_listA_data,
listB_data: redux2_listB_data,
listC_data: redux2_listC_data
};
}
return [tabsData];
}
複製代碼
上邊是部分的 hooks,咱們來看看 右側-C 組件 的相關代碼
// 組件C
import { useCurrentTabsData, useHandleDetails, useFetchC_List } from './useInitHooks';
function C_Layout() {
const [tabsData] = useCurrentTabsData();
const [fetchResult, fetchAction] = useFetchC_List();
const handleToDetails = useHandleDetails();
useEffect(() => {
fetchAction();
}, []);
return (
<div> {tabsData.listC.map(item => { return <Item handleToDetails={handleToDetails(item.uid)} />; })} </div> ); } export default C_Layout; 複製代碼
上邊就是對 右側-C 組件 重構後的代碼,其實真實業務,可能不止這麼點代碼,包括 useTabsType
這個 hooks,確定不止就一種 tabs,這裏我只是提供了我本身的思路 ~
這樣一來,組件 A 和 組件 B 也能夠這麼操做了~
可是當我把 A、B、C 都這麼寫了以後,發現,我還要寫 const、action、saga 對應的文件,就很難受。有沒有好的辦法呢?
emmmm,本想這個重寫寫篇文章的,可是想了想,都是對 hooks 的使用,仍是寫在這裏吧 ~
👍 這波騷操做,是我導師迪哥寫的,我以爲這波操做挺有意思~ 爲我迪哥打 call!!!
咱們知道,在 react 中,咱們想要發請求獲取數據,存入 redux,通常是這樣的 :
頁面發起 Dispatch -> Action -> Saga -> Reducer
舉個例子,咱們通常都是這樣寫一個請求的 👇
// 頁面組件-發起Dispatch
useEffect(() => {
dispatch(props.fetchList);
});
// const.js
export const FETCH_LIST = 'FETCH_LIST';
export const FETCH_LIST_SUCCESS = 'FETCH_LIST_SUCCESS';
// action.js
export function fetchList(params, callback) {
return {
type: FETCH_LIST,
params,
callback
};
}
// saga.js
function* fetchList({ params, callback }) {
const res = yield call(); // 發起請求
if (res.code === 0) {
yield put({
type: FETCH_LIST_SUCCESS,
data: res.data
});
}
if (isFunction(callback)) callback(null, res);
}
// reducer.js
function reduxReducer(state = initReducer, action) {
switch (action.type) {
case FETCH_LIST_SUCCESS:
return Immutable.set(state, 'list', action.data);
default:
return state;
}
}
複製代碼
這你們應該都看得懂吧,試想,咱們每次寫個東西,都要在 const 裏邊定義,再到 action、saga 文件去寫對應的邏輯,有沒有什麼更好的騷氣操做呢?
有,我迪哥就是這麼寫的,直接不要 action、saga,給大家看看怎麼寫的,代碼已被和諧。
function promiseDispatch(dispatch) {
const Promise = require('bluebird');
return params => {
return Promise.promisify(callback => {
dispatch({
...params,
callback
});
})();
};
}
/** * @description: 構造一個可發送請求方法 */
export function useSendAsync() {
const sendAsync = promiseDispatch(useDispatch());
return (action, params) => {
return sendAsync({
...params,
action
});
};
}
複製代碼
自定義兩個快速獲取 reducer 中值的 hooks 和導出一個提供修改 reducer 的 hooks
export function createReduxFunction(name, storeType, initType) {
// 獲取redux方法
const getFunction = function(...keys) {
// 具體如何獲取,根據業務自行處理~
};
// 設置redux方法
const setFunction = function(key) {
// 具體如何設置,看業務自行處理
// 這裏主要就是對reducer中的key,進行賦值
};
// reduxState
const reduxFunction = function(key) {
// 具體看業務自行處理
};
const funcArray = [reduxFunction, getFunction, setFunction];
return funcArray;
}
複製代碼
就很牛逼,而後在 reducer 文件中,引入便可
export const [usePDKReducerRedux, usePDKReducerSelector, usePDKReducerFunction] = createReduxFunction(
'PDKReducer',
'STORE_LIB_PROPS'
);
複製代碼
還記得咱們以前寫的獲取 C 列表的 hooks 嗎?
// 修改前
export function useFetchC_List() {
const isTabs1 = useTabsType();
const tabs1ActionName = 'FETCH_TABS_1_SHOP_LIST';
const tabs2ActionName = 'FETCH_TABS_2_SHOP_LIST';
const resuktActionName = isTabs1() ? tabs1ActionName : tabs2ActionName;
const dispatch = useDispatch();
const result = useAsyncFn(async () => {
const useAction = await dispatch(resuktActionName);
// 在 saga 進行 yield put 操做賦值 redux
return useAction;
});
return result;
}
// 修改後
export function useFetchC_List() {
const isTabs1 = useTabsType();
const tabs1ActionName = 'FETCH_TABS_1_SHOP_LIST';
const tabs2ActionName = 'FETCH_TABS_2_SHOP_LIST';
const resuktActionName = isTabs1() ? tabs1ActionName : tabs2ActionName;
const sendAsync = useSendAsync();
const setTabs1_CList = usePDKReducerFunction('listC_1');
const setTabs2_CList = usePDKReducerFunction('listC_2');
return () =>
sendAsync(resuktActionName).then(res => {
if (res.code === 0) {
// 直接set data to redux
if (isTabs1()) {
setTabs1_CList(res.data)
} else {
setTabs2_CList(res.data)
}
}
});
}
複製代碼
就很簡單,直接一個請求,這裏的 sendAsync('FETCH_LIST')
對應原先 saga 裏的FETCH_LIST
,而後獲取數據後,一個 hooks 取得修改 reducer 的方法,把請求數據寫入 reducer。
頁面調用也更加方便了,直接一個 hooks,而後請求,請求完調用另外一個 hooks 把數據寫入 reducer,獲取 reducer 數據以前的複雜邏輯,也用一個 hook 進行處理。
const fetchList = useFetchC_List();
useEffect(() => {
fetchList();
});
複製代碼
我以爲很 ok ~ 再次給迪哥打卡 !!!!
不知道這篇文章,你們有沒有看明白,其實說白了,就是本身寫的代碼太 low 了,而後重構,重構過程的一些思考和對 hooks 的使用,以前有看過一些 hooks 的教材,大部分都是對 useState、useEffect、useRef 這些經常使用的 API 進行介紹,可是對 hooks 在項目中的一些深刻使用,相對較少,此次,也是借鑑了一下導師迪哥的騷操做,對 hooks 的使用,似乎是打開了一片新天地,並且,不是我說,我以爲用 hooks 重構完以後,我這模塊代碼,邏輯清晰了不少,代碼好看了不少,感受寫的真好,啊哈哈哈哈,王婆賣瓜,自賣自詡。
平常工做,雖然也有進步,可是更多的仍是主動性,爲何要重構,其實以當前的代碼,也不是不能跑,可是代碼寫的真的是太醜了(五天五夜趕出來的代碼,哪想那麼多),並且他人來接手這模塊,看的也是頭暈,加上重構採起本身以前接觸較少的hooks,還能借此學習一波hooks,看一波前輩寫的代碼,何樂而不爲呢?
若是你注意看的話,我上邊有說會在彩蛋中,貼出一個處理connectReducer的代碼,emmmm,這也是我重構的時候,借鑑迪哥寫的,而後本身簡單封裝了一下,主要是由於,重構這個模塊,這個模塊的代碼,好比叫作商城模塊,那麼這個商城模塊都只插 shopReducer,寫一個只處理商城模塊的reducer,而後再寫一個處理這個reducer的函數。全部組件只須要引入這個函數,就能夠連上 shopReducer 了。
/** * @desc 商城模塊redux * @author pengdaokuan */
import React from 'react';
import { isArray, isString } from 'lodash'
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from './action'; // 連入當前商城模塊的action
/** * @param {React.Component} SourceComponent 須要鏈接Redux的組件 * @param {String/Array} keys 能夠是string,也能夠是array */
const ShopConnect = (SourceComponent, keys) => {
class ShopConnect extends React.Component {
render() {
return <SourceComponent {...this.props} />; } } const mapStateToProps = state => { if (keys) { if (isString(keys)) { return { [keys]: state.shopReducer[keys] }; } else if (isArray(keys)) { const redux = {}; keys.forEach(key => { redux[key] = state.shopReducer[key]; }); return redux; } } return state.shopReducer; } const mapDispatchToProps = (dispatch, ownProps) => { return { ...bindActionCreators(actions, dispatch) }; }; return connect(mapStateToProps, mapDispatchToProps)(ShopConnect); }; export default ShopConnect; 複製代碼
使用起來就特別方便了,只須要在組件中,引入便可,咱們就不用在組件裏,寫 connect、action,mapStateToProps, mapDispatchToProps 寫這些玩意,並且若是多個組件,都直連redux的時候,直接調用,多麼舒服。你說是吧,節省了我每次開發一個小組件,用到 redux 的時候,都要去 copy 一下,多麻煩~
import React from 'react';
import ShopConnect from './shopConnect';
class Demo extends React.Component {}
export default ShopConnect(Demo, 'goodlist'); // 獲取shopReducer中的goodlist數據
複製代碼
好了,今天就講到這,果真本身仍是太菜了,好好學習,奧裏給!!!