react-native项目总结

Posted by Eric on November 9, 2017

因为在闲暇之余折腾过react-native(以下简称RN),所以在转前端之初就参与到公司两个RN的项目中。其中一个已上线,是个线上项目,另一个纯粹练手(这种项目请再给我来一打😄),有了这两个项目经验已完全不是新司机了。

先来讲讲这个已上线的App

业务十分简单,几个表单而已,也是我第一个RN项目。项目由一个老司机搭建,用到技术栈有:

"id-validator": "^1.3.0",
"lodash": "^4.17.4",
"md5": "^2.2.1",
"react": "16.0.0-alpha.6",
"react-native": "0.44.0",
"react-native-camera": "git+https://github.com/lwansbrough/react-native-camera.git",
"react-native-config": "^0.4.2",
"react-native-contacts": "^0.8.2",
"react-native-contacts-wrapper": "^0.2.3",
"react-native-device-info": "^0.10.2",
"react-native-fetch-blob": "^0.10.8",
"react-native-fit-image": "^1.5.3",
"react-native-image-picker": "^0.26.3",
"react-native-linear-gradient": "^2.0.0",
"react-native-navigation": "^1.1.250",
"react-native-scrollable-tab-view": "^0.6.6",
"react-native-splash-screen": "^2.1.0",
"react-native-storage": "^0.2.2",
"react-native-swiper": "^1.5.13",
"react-native-toast": "^1.0.1",
"react-native-vector-icons": "^4.1.1",
"react-native-version-number": "^0.1.3"

状态同步

对,没有错,没有用到Redux相关的技术栈,原因是这位老司机当时对Redux并不熟🤣。当然了没用Redux也能开发React和RN,状态同步问题解决起来就没这么顺手了,总有点恶心~比如登陆用户信息,目前的解决方案是定义在一个全局变量(global.G_User)里。在某些场景下使用了RN自带的DeviceEventEmitter,没有Redux,它也算是个不错的解决方案。

路由

项目中用到的路由组件是react-native-navigation(https://wix.github.io/react-native-navigation/#/) ,此库的一大亮点就是可定义4个全新的生命周期,willAppear、didAppear、willDisappear、didDisappear,分别是即将进入路由,已经进入路由,即将释放,已经释放。在项目中,每个路由组件都会继承封装好的基类Page,此外我们还将react-native-navigation中的顶层API,如路由跳转、返回、开关模态窗口、开关灯罩等一并封装在基类Page中。

小坑

react-native-navigation无法操作路由栈,只有reset能重置路由栈。例如页面A->页面B->页面C,而页面C无法直接回到页面A,目前还无法解决,如有解决方案可评论告知,3q。

闪屏页

闪屏页使用react-native-splash-screen库,这个库在android上兼容不友好,在许多android模拟器和真机上都出现闪退现象,原因是闪屏页使用图片会使内存溢出。解决方案是android使用布局方式显示闪屏页或者android放弃react-native-splash-screen库,原生写。ios暂时没有发现问题。

UI

在UI上没有使用如ant等库,使用的组件拼拼凑凑,当然老司机从别的项目中引入了一套已封装好的组件,棒棒的!包括listview、swiper、cursor等

调用原生组件

在本项目中有调用原生相机用于扫描二维码和调用通讯录来选取联系人。ios的权限提示比较友好,在Xcode中就可以配置,具体在Info.plist中。android采用原生方式来编写提示框,并跳转到系统设置页面。

其他

lint

虽然在项目初期就已经配置eslint,但在项目后期才正式启用,导致后期修eslint问题时漏洞百出。将来在使用eslint时可配合git hook、husky或pre-commit库来进行list工作流。

CI

项目中有让运维配了一套Jenkins的环境,包括android和ios,并且都能打包成功。在配完环境后第二天这位运维哥们就离职了😳,重要的是docker环境都在他本机,so,不说了都是泪。将来有机会的还是要玩一玩Jenkins的,毕竟随便一个人点一个按钮就能生成一个app的感觉真的太好了。

本地存储

项目中使用react-native-storage来实现本地存储,这个库号称可以兼容web和RN,其中RN版是对官方AsyncStorage进行封装。操作缓存比较方便,也能设置过期时间。将来可以尝试使用数据库,如sqlite、Realm等。

发布

android多渠道打包使用 https://github.com/mcxiaoke/packer-ng-plugin 简单好用,可以使用java和python两种方式多渠道打包。

另一个app是练手项目😁

本项目所用的技术栈由我来选型。具体如下,

"antd-mobile": "^1.4.2",
"dva": "^1.3.0-beta.4",
"ejs": "^2.5.6",
"id-validator": "^1.3.0",
"lodash": "^4.17.4",
"md5": "^2.2.1",
"moment": "^2.18.1",
"react": "16.0.0-alpha.12",
"react-native": "^0.45.1",
"react-native-config": "^0.5.0",
"react-native-device-info": "^0.10.2",
"react-native-image-picker": "^0.26.3",
"react-native-keyboard-aware-scroll-view": "^0.2.9",
"react-native-linear-gradient": "^2.1.0",
"react-native-scrollable-tab-view": "^0.6.7",
"react-native-snap-carousel": "^2.4.0",
"react-native-splash-screen": "^2.1.0",
"react-native-storage": "^0.2.2",
"react-native-vector-icons": "^4.2.0",
"react-navigation": "^1.0.0-beta.11"

相比于前一个项目的青涩,在做这个项目的时候就熟门熟路很多~

技术亮点

项目中使用antd-mobile,dva两个阿里系的开源框架。前一个是UI框架,使得整体UI风格保持一致。dva是一个将redux、react-router、redux-saga等进行一层轻量封装的框架。对!我们用上redux了╮( ̄▽ ̄)╭

ant

ant不用过多解释,是蚂蚁金服旗下的开源框架,熟悉react的同学应该都多多少少使用过,是个十分强大的UI库,我们公司还在其基础上开发了一套动态表单,之后有机会的话可以介绍下动态表单的思路。ant-mobile是可以兼容web和RN的,也可使用TypeScript开发,同时也兼容preact哦。使用简单,官网有具体demo实现(https://mobile.ant.design/docs/react/introduce-cn)

dva

重点讲讲dva。 dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念。在没有 dva 之前,我们通常会创建 sagas/products.js, reducers/products.js 和 actions/products.js,然后在这些文件之间来回切换。比如我们会创建如下的结构,

dva最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起,比如:

import key from 'keymaster';

app.model({
  namespace: 'todos', //model 的 namespace
  state: [], // model 的初始化数据
  reducers: {
    add(state, { payload: todo }) {
      return state.concat(todo);
    },
  },
  effects: {
    *addRemote({ payload: todo }, { put, call, select }) {
      const todos = yield select(state => state.todos); // 这边的 state 来源于全局的 state,select 方法提供获取全局 state 的能力,也就是说,在这边如果你有需要其他 model 的数据,则完全可以通过 state.modelName 来获取
      yield call(addTodo, todo); // 用于调用异步逻辑,支持 promise 。
      yield put({ type: 'add', payload: todo }); // 用于触发 action 。这边需要注意的是,action 所调用的 reducer 或 effects 来源于本 model 那么在 type 中不需要声明命名空间,如果需要触发其他非本 model 的方法,则需要在 type 中声明命名空间,如 yield put({ type: 'namespace/fuc', payload: xxx });
    },
  },
  subscriptions: {
    keyEvent(dispatch) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
  }
};

介绍下这些 model 的 key :

  • namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值
  • state - 对应 reducer 的 initialState
  • subscription - Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
  • effects - 对应 redux-saga,Effect 被称为副作用,在我们的应用中,最常见的就是异步操作,Effects 的最终流向是通过 Reducers改变 State。
  • reducers - 不多做介绍了。

下图是具体的整个数据流走向。

dva的使用感受

首先,代码结构更清晰,reducer,effects,subscription集成在一个文件中,不需要在开发的时候几个文件来回切换了。其次,代码写的更优雅了,同样也是函数式编程的魅力,将所有逻辑都集中在model中,页面中(component)只需要根据监听(connect)redux中的state进行渲染。绝大数情况下页面中不需要有component的state,所以页面都可以继承PureComponent。同样的state渲染的结果应该是相同的,听上去很符合单元测试的逻辑。

路由

本项目中使用的路由是react-navigation。这个库可操作路由栈,可自定义返回按钮的点击事件,很好的解决了上个项目中的痛点(其实我是看官网上推荐这个库来着😂)。同样的,我们也封装了一个Page基类,其中包含对路由的操作。

其他

  • 项目中的全局loading使用的是dva-loading,它的逻辑是触发了effect就会改变global、model或者effects的loading状态,我做了简单的修改,通过添加参数可以控制是否要改变loading状态,比如在启动app时异步加载基础数据,这时候不需要显示loading状态,还有首页的effects不需要显示loading。
  • 项目中有个技术难点:通过不同的条件加载不同的表单,表单中的内容都是在后端做配置。app需要做表单渲染,表单验证,表单提交。我做成了插件式,也就是说表单的验证,表单的值转换等都是通过配置的方式。

分好几天写的,写的比较仓促,有讲的不对的地方可留言告知 🤗