React Native 項目框架搭建的一些心得

React Native 項目框架搭建的一些心得

1. 項目中使用的技術棧

react native、react hook、typescript、immer、tslint、jest 等.
都是比較常見的, 就不多做介紹了

2. 數據處理使用的是 react hook 中的 useContext+useReducer

思想與 redux 是一致的, 用起來相對比較簡單, 適合不太複雜的業務場景.

const HomeContext = createContext<IContext>({
  state: defaultState,
  dispatch: () => {}
});
const ContextProvider = ({ urlQuery, pageCode }: IProps) => {
  const initState = getInitState(urlQuery, pageCode);
  const [state, dispatch]: [IHomeState, IDispatch] = useReducer(homeReducer, initState);

  return (
    <HomeContext.Provider value={{ state, dispatch }}>
      <HomeContainer />
    </HomeContext.Provider>
  );
};
const HomeContainer = () => {
const { dispatch, state } = useContext(HomeContext);
...

3. 項目的結構如下

|-page1
    |-handler   // 處理邏輯的純函數,需進行UT覆蓋
    |-container // 整合數據、行爲與組件
    |-component // 純UI組件,展示內容與用戶交互,不處理業務邏輯
    |-store     // 數據結構不能超過3層,可使用外部引用、冗餘字段的方式降低層級
    |-reducer   // 使用immer返回新的數據(immutable data)
    |-...
|-page2
|-...

項目中的規範

1. Page

整個項目做爲一個多頁應用, 最基本的拆分單元是 page.

每一個 page 有相應的 store, 並非整個項目使用一個 store, 這樣做的原因如下:

各個頁面中與外部相關的操作, 都在 Page 組件中定義

Page 組件的主要作用

以其自身業務模塊爲基礎, 把可以抽象出來的外部依賴、外部交互都集中到此組件的代碼中.

方便開發人員在進行各個頁面間邏輯編寫、問題排查時, 可根據具體頁面 + 數據源, 準確定位到具體的代碼.

2. reducer

在以往的項目中, reducer 中可能會涉及部分數據處理、用戶行爲、日誌埋點、頁面跳轉等等代碼邏輯.

因爲在開發人員寫代碼的過程中, 發現 reducer 作爲某個處理邏輯的終點 (更新了 state 之後, 此次事件即爲結束), 很適合去做這些事情.

隨着項目的維護, 需求的迭代, reducer 的體積不斷的增大.
因爲缺乏條理, 代碼量又龐大, 再想去對代碼進行調整, 只會困難重重.

讓你去維護這樣的一個項目, 可想而知, 將會是多麼的痛苦.

爲此, 對 reducer 中的代碼進行了一些減法:

reducer 的主要作用

以可枚舉的形式, 彙總出頁面中所有操作數據的場景.

在其本身適用於 react 框架的特性之外, 賦予一定的業務邏輯閱讀屬性, 在不依賴 UI 組件的情況下, 可大致閱讀出頁面中的所有數據處理邏輯.

// 避免dispatch時進行兩次,且定義過多單字段的更新case
// 整合此邏輯後,與頁面上的行爲相關聯,利於理解、閱讀
case EFHListAction.updateSpecifyQueryMessage:
    return produce(state, (draft: IFHListState) => {
        draft.specifyQueryMessage = payload as string;
        draft.showSpecifyQueryMessage = true;
    });    
case EFHListAction.updateShowSpecifyQueryMessage:
    return produce(state, (draft: IFHListState) => {
        draft.showSpecifyQueryMessage = payload as boolean;
    });

3. handler

這裏先引入一個純函數的概念:

一個函數的返回結果只依賴於它的參數,並且在執行過程裏面沒有副作用,我們就把這個函數叫做純函數.

把儘可能多的邏輯抽象爲純函數, 然後放入 handler 中:

handler 的主要作用

負責數據源到 store、container 到 component、dispatch 到 reducer 等等場景下的邏輯處理.

作爲各類場景下, 邏輯處理函數的存放地, 整個文件不涉及頁面流程上的關聯關係, 每個函數只要滿足其輸入與輸出的使用場景, 即可複用, 多用於 container 文件中.

export function getFilterAndSortResult(
  flightList: IFlightInfo[],
  filterList: IFilterItem[],
  filterShare: boolean,
  filterOnlyDirect: boolean,
  sortType: EFlightSortType
) {
  if (!isValidArray(flightList)) {
    return [];
  }

  const sortFn = getSortFn(sortType);
  const result = flightList.filter(v => doFilter(v, filterList, filterShare, 1, filterOnlyDirect)).sort(sortFn);

  return result;
}
describe(getFilterAndSortResult.name, () => {
  test('getFilterAndSortResult', () => {
    expect(getFilterAndSortResult(flightList, filterList, false, EFlightSortType.PriceAsc)).toEqual(filterSortResult);
  });
});

4. Container

由上面的項目結構圖可以看出, 每個 Page 都有 base Container, 作爲數據處理的中心.

在此 base Container 之下, 會根據不同模塊, 定義出各個子 Container:

Container 的主要作用

整個項目中, 各種數據、UI、用戶行爲的匯合點, 要儘可能的把相關的模塊抽離出來, 避免造成代碼量過大, 難以維護的情況.

Container 的定義應以頁面展示的模塊進行抽象. 如 Head Contianer、Content Container、Footer Container 等較爲常見的劃分方式.

一些頁面中相對獨立的模塊, 也應該產出其對應的 Container, 來內聚相關邏輯, 如贈送優惠券模塊、用戶反饋模塊等.

特別注意的是行爲函數

const OWFlightListContainer = () => {
    // 通過Context獲取數據
    const { state, dispatch } = useContext(OWFlightListContext);
    ...

    // 初次加載時進行超時的倒計時
    useOnce(overTimeCountDown);
    ...
    
    // 用戶點擊排序
    const onPressSort = (lastSortType: EFlightSortType, isTimeSort: boolean) => {
        // 引用了handler中的getNextSortType函數
        const sortType = getNextSortType(lastSortType, isTimeSort);
        dispatch({ type: EOWFlightListAction.updateSortType, payload: sortType });
        
        // 埋點操作
        logSort(state, sortType);
    };
    
    // 渲染展示組件
    return <.../>;
}

小結

由 easy to code 到 easy to read

在整個項目中, 定義了很多規範, 是想在功能的實現之上, 更利於項目人員的維護.

規範的定義是比較容易的, 想要維護好一個項目, 更多的是依靠團隊的成員, 在達成共識的前提下, 持之以恆的堅持了

分享幾個實用的函數

根據對象路徑取值

/**
 * 根據對象路徑取值
 * @param target {a: { b: { c: [1] } } }
 * @param path 'a.b.c.0'
 */
export function getVal(target: any, path: string, defaultValue: any = undefined) {
  let ret = target;
  let key: string | undefined = '';
  const pathList = path.split('.');

  do {
    key = pathList.shift();
    if (ret && key !== undefined && typeof ret === 'object' && key in ret) {
      ret = ret[key];
    } else {
      ret = undefined;
    }
  } while (pathList.length && ret !== undefined);

  return ret === undefined || ret === null ? defaultValue : ret;
}

// DEMO
const errorCode = getVal(result, 'rstlist.0.type', 0);

讀取根據配置信息

// 在與外部對接時,經常會定義一些固定結構,可擴展性的數據列表
// 爲了適應此類契約,便於更好的閱讀與維護,總結出了以下函數
export const GLOBAL_NOTE_CONFIG = {
  2: 'refund',
  3: 'sortType',
  4: 'featureSwitch'
};

/**
 * 根據配置,獲取attrList中的值,返回json對象類型的數據
 * @private
 * @memberof DetailService
 */
export function getNoteValue<T>(
  noteList: Array<T> | undefined | null,
  config: { [_: string]: string },
  keyName: string = 'type'
) {
  const ret: { [_: string]: T | Array<T> } = {};

  if (!isValidArray(noteList!)) {
    return ret;
  }

  //@ts-ignore
  noteList.forEach((note: any) => {
    const typeStr: string = (('' + note[keyName]) as unknown) as string;

    if (!(typeStr in config)) {
      return;
    }

    if (note === undefined || note === null) {
      return;
    }

    const key = config[typeStr];

    // 有多個值時,改爲數組類型
    if (ret[key] === undefined) {
      ret[key] = note;
    } else if (Array.isArray(ret[key])) {
      (ret[key] as T[]).push(note);
    } else {
      const first = ret[key];
      ret[key] = [first, note];
    }
  });

  return ret;
}

// DEMO
// 適用於外部定義的一些可擴展note節點列表的取值邏輯
const { sortType, featureSwitch } = getNoteValue(list, GLOBAL_NOTE_CONFIG, 'ntype');

多條件數組排序

/**
 * 獲取用於排序的sort函數
 * @param fn 同類型元素比較函數,true爲排序優先
 */
export function getSort<T>(fn: (a: T, b: T) => boolean): (a: T, b: T) => 1 | -1 | 0 {
  return (a: T, b: T): 1 | -1 | 0 => {
    let ret = 0;

    if (fn.call(null, a, b)) {
      ret = -1;
    } else if (fn.call(null, b, a)) {
      ret = 1;
    }

    return ret as 0;
  };
}

/**
 * 多重排序
 */
export function getMultipleSort<T>(arr: Array<(a: T, b: T) => 1 | -1 | 0>) {
  return (a: T, b: T) => {
    let tmp;
    let i = 0;

    do {
      tmp = arr[i++](a, b);
    } while (tmp === 0 && i < arr.length);

    return tmp;
  };
}

// DEMO
const ageSort = getSort(function(a, b) {
  return a.age < b.age;
});

const nameSort = getSort(function(a, b) {
  return a.name < b.name;
});

const sexSort = getSort(function(a, b) {
  return a.sex && !b.sex;
});

//判斷條件先後順序可調整
const arr = [nameSort, ageSort, sexSort];

const ret = data.sort(getMultipleSort(arr));
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://juejin.cn/post/6966080062168760327