1. 程式人生 > 程式設計 >React Native專案框架搭建的一些心得體會

React Native專案框架搭建的一些心得體會

React Native 是Facebook於2015年4月開源的跨平臺移動應用開發框架,短短的一兩年的發展就已經有很多家公司支援並採用此框架來搭建公司的移動端的應用,
React Native使你能夠在javaschttp://www.cppcns.comript和React的基礎上獲得完全一致的開發體驗,構建世界一流的原生APP。

專案框架與專案結構

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,這樣做的原因如下:

  • 各個頁面的邏輯相對獨立
  • 各個頁面都可作為專案入口
  • 結合RN頁面生命週期進行資料處理(避免資料初始化、快取等一系列問題)

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

  • 頁面跳轉邏輯
  • 回退之後要處理的事件
  • 需要操作哪些storage中的資料
  • 需要請求哪些服務等等

Page元件的主要作用

以其自身業務模組為基礎,把可以抽象出來的外部依賴、外部互動都集中到此元件的程式碼中.

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

2. reduce程式設計客棧r

在以往的專案中,reducer中可能會涉及部分資料處理、使用者行為、日誌埋點、頁面跳轉等等程式碼邏輯.

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

隨著專案的維護,需求的迭代,reducer的體積不斷的增大.

因為缺乏條理,程式碼量又龐大,再想去對程式碼進行調整,只會困難重重.

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

為此,對reducer中的程式碼進行了一些減法:

  • reducer中只對state的資料進行修改
  • 使用immer的produce產生immutable data
  • 冗餘單獨欄位的修改,進行整合,枚舉出頁面行為對應的action

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中:

  • 涵蓋較多的業務邏輯
  • 只能是純函式
  • 必須進行UT覆蓋

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,false,EFlightSortType.PriceAsc)).toEqual(filterSortResult);
  });
});

4. Container

由上面的專案結構圖可以看出,每個Page都有base Container,作為資料處理的中心.

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

  • 生命週期處理(初始化時要進行的一些非同步操作)
  • 為渲染元件Components提供資料來源
  • 定義頁面中的行為函式

Container的主要作用

整個專案中,各種資料、UI、使用者行為的匯合點,要儘可能的把相關的模組抽離出來,避免造成程式碼量過大,難以維護的情況.

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

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

特別注意的是行為函式

  • 多個Container中公用的行為,可直接放入base Container中
  • 在上文架構圖中的action事例(setAction)為另外一種行為複用,根據具體的場景進行應用

利於程式碼閱讀,A模組的浮層展示邏輯,B模組使用時
模組產生的先後順序,先有A模組再有B模組需要使用A的方法

  • 定義資料埋點、使用者行為埋點
  • 頁面跳轉方法的呼叫(Page-->base 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
在整個專案中,定義了很多規範,是想在功能的實現之上,更利於專案人員的維護.

  • Page元件中包含頁面相關的外部依賴
  • reducer枚舉出所有對頁面資料操作的事件
  • handler中集合了業務邏輯的處理,以純函式的實現及UT的覆蓋,確保專案質量
  • Container中的行為函式,定義出所有與使用者操作相關的事件,並記錄埋點資料
  • Componet中避免出現業務邏輯的處理,只進行UI展示,減少UI自動化case,增加UT的case

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

分享幾個實用的函式

根據物件路徑取值

/**
 * 根據物件路徑取值
 * @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);

讀取根據配置資訊

// 在與外部對接時,經常會定義一些固定結構,可擴充套件性的資料列http://www.cppcns.com表
// 為了適應此類契約,便於更http://www.cppcns.com好的閱讀與維護,總結出了以下函式
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&kSsnclt;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));

以上就是React Native專案框架搭建的一些心得體會的詳細內容,更多關於React Native專案框架搭建的資料請關注我們其它相關文章!