前端渣渣对使用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数据
复制代码

好了,今天就讲到这,果真本身仍是太菜了,好好学习,奥里给!!!

相关文章
相关标签/搜索