前端渣渣對使用react hooks進行重構的新認識

前言

這篇文章相對較長,是我重構的一次記錄。請耐住性子,慢慢看下去 ~ 爲了方便理解,函數/變量的取名有些 low,emmm,你們不要介意~react

前幾天,中途臨時接到一個需求,複雜程度雖然不高,但也不低,時間很趕(本來半個月,硬生生五天五夜肛完),基於這個需求,爲了遇上送測,代碼懟上去的,bug 相對較多,這不,送測階段,回過頭去看這模塊的代碼(本身都看不下去)...決定,利用這週末時間,用 react hooks 進行重構一波 ~redux

爲何要用 hooks 進行重構,這是由於基於業務邏輯,可能用 hooks 會更加方便且清晰,同時我的以前也只是看了看 hooks 的文檔,使用了一些簡單的 API,此次想借此機會,好好學一下 hooks ~promise

廢話很少說,直接看需求吧 ~async

需求

真實業務需求已被我和諧,首先,咱們有一個頁面,這個頁面是這樣的 ~ 應該都能理解這個組件是長什麼樣了吧?函數

給大家簡單畫一下,就是這個樣子 👇學習

這下子應該懂了吧 ~ 咱們繼續看一下需求是什麼 :fetch

  • 頭部 A 組件,有一個叫作 接收 的操做,接收完以後,刷新本身,同時須要 B 和 C 組件進行更新
  • 右側 C 組件,有一個叫作 更新刪除的操做,操做完以後,刷新本身,同時須要更新 A 已經 B

這麼一看,其實並不複雜啊,可是問題在於 :ui

  1. 全部的請求,都在各自的組件中進行,你不可能在A組件中,把B、C組件的請求邏輯copy一遍this

  2. 上邊只是寫的 A、B、C 組件,可是實際上,真實觸發此操做的是在它們的子組件進行url

  3. 我已經把這些請求、賦值等都作完了,這時候才知道須要更新,但我並不想去動原先的代碼

  4. 真實的業務場景更加複雜,好比,這個頁面父組件的顯示,還依賴於 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
                })
            }
        })
    }
}
複製代碼

這波操做,是真的騷啊,可是,你就會發現,真的太噁心了!!!

並且有時候,一個請求,會發送兩遍,你想一想,一個組件,在它的DidMountupdateMount週期,去發送請求,而後這個請求,根據 tabs 不一樣,請求的url不一樣,請求回來了,還要根據返回的數據,再一次請求詳情,而後再作一些其它 🐓 兒的操做。

關鍵是,這還不是一個組件,三個組件都這樣,說不定以後這塊複雜起來,更加難以維護!!!

每一個組件,都要引入connect、引入 bindActionCreators ,而後本身還要寫connect(mapStateToProps, mapDispatchToProps),這裏你能夠寫一個管理當前的connectReducer函數,在這個函數中處理connect,這裏我不過多介紹 ~ 我會在結尾的彩蛋中直接貼代碼 ~

重構

忍無可忍,因而去拿了一張 A4 紙,把現階段的一個流程圖及關係圖畫了出來,同時理清楚了每個思路,而後畫了一下重構以後的關係圖,而且諮詢了一下導師,終於,在今天,踏出了第一步。

這個圖不知道能不能說的清楚,大體就是這樣的 :

  • 封裝一個 hooks,用於獲取當前的 tabs,而後在頁面中,若是要用到就直接引入這個 hooks 便可

  • 封裝A 組件的請求,在外部的調用,無需在意 tabs 是什麼,總之,我引用這個 hooks,就只須要你發起 dispatch action 就行了。

  • 其它組件請求也是這樣,同時獲取數據,也寫一個 hooks,只須要返回我想要的結果,不須要我本身進行判斷 tabs

由於這個 tabs 是存在 redux 中的,咱們在每一個頁面都去寫 connect 吧,多累呀 ~

下邊咱們以 頭部-C 組件 來舉例,看看重構後的最終效果 ~

自定義 hooks

/** * @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 對應的文件,就很難受。有沒有好的辦法呢?

利用 hooks,拋棄 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,給大家看看怎麼寫的,代碼已被和諧。

1.封裝一個 Promise,用於請求

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
    });
  };
}
複製代碼

2. 處理 reducer,自定義 hooks

自定義兩個快速獲取 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'
);
複製代碼

拋棄 Action、Saga

還記得咱們以前寫的獲取 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數據
複製代碼

好了,今天就講到這,果真本身仍是太菜了,好好學習,奧裏給!!!

相關文章
相關標籤/搜索