React Native项目框架搭建的一些心得体会

React Native 是Facebook于2015年4月开源的跨平台移动应用开发框架, 短短的一两年的发展就已经有很多家公司支持并采用此框架来搭建公司的移动端的应用,
React Native使你能够在Javascript和React的基础上获得完全一致的开发体验,构建世界一流的原生APP。

项目框架与项目结构

1. 项目中使用的技术栈

react native、react hook、typescript、immer、tslint、jest等.

都是比较常见的,就不多做介绍了

2. 数据处理使用的是react hook中的useContext+useReducer

思想与redux是一致的,用起来相对比较简单,适合不太复杂的业务场景.

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

  return (
    
      
    
  );
};
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(
  noteList: Array | undefined | null,
  config: { [_: string]: string },
  keyName: string = 'type'
) {
  const ret: { [_: string]: T | Array } = {};

  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(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(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项目框架搭建的资料请关注脚本之家其它相关文章!

你可能感兴趣的:(React Native项目框架搭建的一些心得体会)