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, 這樣做的原因如下:
- 各個頁面的邏輯相對獨立
- 各個頁面都可作爲項目入口
- 結合 RN 頁面生命週期進行數據處理 (避免數據初始化、緩存等一系列問題)
各個頁面中與外部相關的操作, 都在 Page 組件中定義
- 頁面跳轉邏輯
- 回退之後要處理的事件
- 需要操作哪些 storage 中的數據
- 需要請求哪些服務等等
Page 組件的主要作用
以其自身業務模塊爲基礎, 把可以抽象出來的外部依賴、外部交互都集中到此組件的代碼中.
方便開發人員在進行各個頁面間邏輯編寫、問題排查時, 可根據具體頁面 + 數據源, 準確定位到具體的代碼.
2. reducer
在以往的項目中, 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, filterList, 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);
讀取根據配置信息
// 在與外部對接時,經常會定義一些固定結構,可擴展性的數據列表
// 爲了適應此類契約,便於更好的閱讀與維護,總結出了以下函數
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