著做权归做者全部。商业转载请联系 Scott 得到受权,非商业转载请注明出处[务必保留全文,勿作删减]。线下越重,线上须要越轻,这个轻指的是轻便轻巧和简洁易用,B2B2C 生鲜领域在线下是如此之重,那么在交易场景线上化的过程当中,端的移动化就势在必行,试想一下,让菜市场摊位老板人手一台笔记本点开网页选购支付,让采购销售抱着电脑去拜访客户,一边聊蔬菜行情,一边打开笔记本进行记录,有没有一种回到世纪初的感受。javascript
产品的移动化,这将是咱们展开这篇文章的背景,咱们会先了解小菜的产品托管在哪些端上,而后感觉这些端带来的挑战,最后是咱们聚焦如何作移动端的框架封装,包括必要的基建部分。前端
小菜早期围绕着蔬菜销地以客户集单批发的模式摸爬滚打几年,从上游的蔬菜供应商到下游批发市场的摊位老板,在这个长长的链路中,咱们诞生了这样几款线上产品来服务于不一样的人群和场景,以前文章中也有介绍,这里再汇总一下,共 9 款 App:java
前 7 款 App 都是基于 ReactNative 开发的 iOS/Android App,最后两个是微信小程序,它们涵盖了公司几乎全部的协同场景和工做流。react
古典互联网时代,由于要兼容 IE678 而痛苦不堪,Hack 黑魔法经验基本表明前端水平,现在互联网早已移动化,咱们理想中的移动端开发,看上去是能够大胆使用新语法特性,只须要作好尺寸兼容就行了,但事实并不是如此,不只在移动端的浏览器不是如此,在移动端开发 RN App 也是如此,这是咱们某一款 App 一段时间内,所收集上来的手机厂商分布:ios
能够发现 Android 的碎片化很是严重,每个厂商下面有不一样时期推出的不一样型号的手机,这些手机有着不一样版本的操做系统,不一样的分辨率和用电策略,不一样的后台进程管理方式和用户权限,要让一款 App 在哪怕头部 40% 的手机上兼容,都是一件艰难的事情,这个客观物理现状叠加下面的社区现状,App 质量保证这件事情会变得雪上加霜。git
回到本文的开头,咱们在长链路的 B2B 生鲜场景中,为了更快更轻,开发出了 7 款 App,并且未来随着业务场景的拓展会诞生更多独立 App 甚至是集大成的 App,因此技术选型不太可能选择原生的 Java/Object-C 开发,尤为对于创业公司,7 款 App 得须要多少名原生开发工程师才能搞定,高频繁重的业务变化又怎样靠堆人来保证?github
想清楚这些,一开始咱们就调研 ReactNative,并最终所有从原生切换到了 RN。经过跑过来的这 4 年来看,使用 RN 为公司节约了大量的人力成本同时,也尽量的知足到了几乎全部的须要快速迭代的业务场景,又快又轻,成为宋小菜大前端团队作事的一个典型特征。面试
但换一个角度看,就是带来的问题。又快又轻的背后是 RN 版本的飞速迭代,截止到目前,也就是 2019 年 4 月份,RN 尚未推出一个官方的正式的长期维护的稳定版本,什么意思?就是 RN 目前依然处在不稳定的研发周期内,咱们依然站在刀尖上起舞,用不稳定的 RN 版本试图开发稳定的应用。四年走来,咱们在 RN 的框架里,多少次面对旧版本局限性和新版本不稳定性都进退不得,旧版本的 Bug 可能会在新版本中修复,新版本引进则会带来新版本本身的问题。npm
除了 RN 自身版本,还有第二个问题,围绕着 RN 有不少业界优秀的组件,但这些社区组件甚至官方组件,都不必定能及时跟进最新的 RN 版本,同时还能兼容到较老的 RN 版本,因此 RN 升级致使的组件不兼容性,会引起你 Fork 修改组件的冲动,但这样会带来额外的开发成本和版本维护成本,取舍会成为版本升降的终极问题。redux
在国内开发,还有第三个问题,就是中文文档缺少,社区资源匮乏,参考文献陈旧,可拿来主义的开源工程方案甚至社区线上线下会议分享都很缺少,一个不当心就会踩坑,这就是 RN 社区的现状,咱们在刀尖浪花上独步,App 选型背后的技术栈稳定性则成为悬在头上的一把铡刀,你不知道何时会咔嚓一声。
咱们知道有一个词叫作主观能动性,表示没有条件创造条件也能够上。这个词的主体就是人,聊完移动端设备现状和社区现状后,咱们来聊聊人的问题。RN 在国内真正开始普及使用,是从 2015 年开始,也就意味着,到 2019 年,一个 RN 工程师最多也就只有 4 年的工做经验,而 RN 的 “Learn once, write anywhere” 也刺激着一切 Care 人员开支, Care 产品研发投入性价比的公司纷纷跳水研究 RN,争抢 RN 人才,RN 是前端中的移动前端,前端有多抢手,那么 RN 工程师就比它还要抢手。
这就致使基本上 RN 工程师很难靠外部招聘,只能靠内部培养。这也是小菜前端的成长历程,咱们有 2 名资深 RN 工程师,一个是从服务端 Java,一个是从原生 Android 开发转过来的。若是 RN 人手不足,产品支持的力度和速度就必定会遇到瓶颈,这就是咱们曾经面临的问题,就是人才现状,外招数量不足,内培速度有限,RN 工程师的数量和能力就时不时成为公司业务扩张的瓶颈。
做为工程师,咱们有很强的自尊心和不容挑战的代码洁癖,但在一个创业公司里面,甚至大公司的一个创业团队里面,咱们须要对接一些关键的业务节点,冲刺一些特定的时间窗口,而且要及时响应多变的业务,和业务背后多变的产品形态,这都会带来很是密集的需求队列。
这些密集的需求队列对咱们的代码质量有很是高的挑战,一个组件用 5 分钟思考如何抽象和用 50 分钟思考,实现后的稳定性、兼容性都是不一样的。如何保证产品定期交付上线,会是摆在咱们面前一个很是关键的命题,而这个难题以外,还有一个更难的命题等着咱们,那就是如何保证交付不延期的同时,还能保证交付质量。
要知道,若是一个项目代码赶的太毛糙,后期维护起来的成本会是巨大的,甚至只能用更高的成本重构重写。本质上,再次重构就必定是公司在为早期的猛冲买单,为这些技术债买单,如何不去买单或者如何用最小的成本买单,这跟咱们早期的业务密集程度,交付周期,质量把控有很大的关系。
综上,移动端碎片化所带来的兼容难度,RN 框架的局限性,版本间差别带来的不稳定性,技术社区资源的匮乏和前端团队技术能力掣肘,再叠加上高密度的业务排期,让前端开发这个原本很酷的事情,变得晴雨不定。
这些避不开的现实,是绕不过去的坎儿,必须经过人才储备和技术基建来缓解,接下来咱们进入到本文的重点 - RN 框架的封装。
RN 的 App 工程骨架,所有抽象完毕,再搭配上组件化,就能够称为一个基于 ReactNative 定制的 App 框架了,而 RN 涉及到原生层面的技术细节太多,咱们暂不作讨论,只专一在工程与业务的封装上。
咱们在构建 RN App 工程时须要关注这几个关键要素:
配置管理是指能够灵活合理的管理 App 的内部环境,主要包括:
咱们在构建工程时尽可能将全部的配置抽象统一放置在一个地方,这样便于查找和修改。可是因为大多数配置都统一放在同一个地方,那么就不免有部分文件要使用某个配置时其引用路径比较长,好比:
import { pluginAConfig } from '../../../../../config'
这样就形成了阅读性不好且代码不美观,所以咱们可使用 Facebook 的 fbjs
模块提供的一个功能 providesModule
:
//config.js /** * config for all * @providesModule config * 使用 providesModule 将 config 暴露出去 **/ import pluginAConfig from './plugin_a_config' export default { pluginAConfig } // 而后在其余文件中调用 // A.js import { pluginAConfig } from 'config'
这样就能很方便地在 App 的任意一处使用 config 了,可是咱们要避免滥用 providesMoudle
,由于使用了 providesMoudle
进行声明的模块的源码,想要在编辑器中使用跳转到定义的方式去查看比较困难,不利于团队多人合做。
静态资源泛指会被屡次调用的图片或 icon,咱们通常在 RN 使用图片时是直接引用的:
import { Image } from 'react-native' render(){ return ( <Image source={{uri: './logo.png'}} /> ) }
当图片须要在多处使用时,咱们可能会将这些可能会被反复使用的图片统一管理到 assets
文件夹中,统一管理和使用,可是当须要使用图片资源的文件嵌套较深时,引用图片就变得麻烦:
render(){ return ( <Image source={{uri: '../../../../assets/logo.png'}} /> ) }
这个问题与配置管理的问题同样,能够首先将图片资源按照类型进行分类,好比 assets 文件夹下有 button/icon/img/splash/svg 等,每个类型的结构以下:
- icon/ - asset/ - index.js
其中 asset
文件夹保存咱们的图片资源,在 index.js
中对图片进行引用并暴露为模块:
// index.js export default { IconAlarmClockOrange: require('./asset/icon_alarm_clock_orange.png'), IconAvatarBlue: require('./asset/icon_avatar_blue.png'), IconArrowLeftBlue: require('./asset/icon_arrow_left_blue.png'), IconArrowUpGreen: require('./asset/icon_arrow_up_green.png') }
而后再在 assets
文件夹下编辑 index.js
,将全部的图片资源做为 assets
模块暴露出去,为了不和其余模块冲突你能够修改模块名为 xxAssets
// assets/index.js /** * @providesModule myAssets **/ import Splash from './splash' import Icon from './icon' import Img from './img' import Btn from './button' import Svg from './svg' export { Splash, Icon, Img, Btn, Svg } // A.js import { Icon } from 'myAssets' render(){ return ( <Image source={Icon.IconAlarmClockOrange} /> ) }
这样,咱们就能很方便地将分散在项目各处的图片资源统一到一个地方进行管理了,使用起来也很是方便。
网络请求这块,react-native 使用 whatwg-fetch,咱们也能够选择其余的三方包如 axios 来作网络请求。但有时候咱们会在开发中遇到一个问题,那就是咱们明明已经在代码里已经修改了 cookie, 可是每次请求可能仍是会带上以前的 cookie 从而形成一些困扰,因此这里推荐一个实用的组件 Networking
:
import { NativeModules } from 'react-native' const { Networking } = NativeModules // 手动清除已缓存 Cookie,这样就能解决上述的问题了 Networking.clearCookies(callBack)
固然,Networking
的功能不止于此,还有不少其余有趣的功能能够发掘,能够直接用它来包装本身的网络请求工具,还支持 abort
,能够参考 源码 来具体把玩。
使用 RN 开发 App 自己效率就比较高,若是想要继续进阶就要考虑组件化开发,一旦涉及到组件化开发,就不可避免地会涉及到组件管理的问题,这里的组件管理比较宽泛,它实际上应该指的是:
组件规范指的是 UI 设计规范,咱们能够与设计同窗交流规定好一套特定的规范,而后将通用的样式属性(如主题颜色,按钮轮廓,返回按键,Tab 基础样式等)定义出来,便于全部的组件让开发者在开发时使用,而不是开发者各自为政在开发时重复写样式文件,这里推荐一个比较好用的用于样式定义的三方插件 react-native-extended-stylesheet ,咱们可使用这个插件定义咱们的通用属性:
// mystyle import { PixelRatio, Dimensions } from 'react-native' import EStyleSheet from 'react-native-extended-stylesheet' const { width, height } = Dimensions.get('window') const globals = { /** build color **/ $Primary: '#aa66ff', $Secondary: '#77aa33', $slimLine: 1 / PixelRatio.get(), /** dimensions **/ $windowWidth: width, $windowHeight: height } EStyleSheet.build(globals) module.exports = { ...EStyleSheet, create: styleObject => EStyleSheet.create(styleObject), build: (obj) => { if (!obj) { return } EStyleSheet.build(_.assign(obj, globals)) } } // view.js import MyStyleSheet from 'mystyle' const s = MyStyleSheet.create({ container: { backgroundColor: '$Secondary', width: '$windowWidth' } }) render....
这样,咱们就能在开发的任意插件或者 App 中直接使用这些基础属性,当某些属性须要修改时只须要更新 mystyle
组件便可,另外还能够衍生出主题切换等功能,使得开发更加灵活。
关于组件类型咱们会抛开三方组件以及原生组件,由于一旦涉及到这二者,须要写的东西就太多了,咱们将组件按使用范围分为通用组件和业务组件两大类。
首先什么是业务组件?即咱们在开发某个业务产品经常使用到的组件,这个组件绑定了与业务相关的一些特殊属性,除了这个业务开发之外,其余地方都不适用,可是在开发这个业务时多个页面会频繁地使用到,因此咱们有必要将其抽象出来,方便使用。
什么是通用组件?便可以在 App 范围内使用甚至于跨 App 使用的组件,这里能够对这个类别进行细分,咱们将能跨 App 使用的组件上传到了本身的搭建的私有 npm 仓库,方便咱们的 App 开发者使用,同时,具备 App 本身特点的组件则放到工程中统一管理,一样适用 providesModules
暴露出去。
制定一整套组件开发标准的是很重要的,由于不少组件开发多是多人维护的,有一套既定的规范就能够下降维护成本,组件使用的说明文档的完善也一样重要。
开发 App 就不可避免地会遇到如何管理页面以及处理页面跳转等问题,也就是路由管理问题,自从 Facebook 取消了 RN 自己自带的 Navigator 之后,许多依赖于这个组件的开发者不得不将目光投向百花齐放的社区三方组件,FB 随后推荐你们使用的是 react-community 推出的 react-navigation ,如今这个路由组件已经独立出来了。咱们在开发时就是使用的这个组件做为路由管理组件,只不过是在其基础上作了一些定制 ,使得使用更加简单,部分跳转动做更加符合咱们的产品场景,推荐你们使用这个组件。固然,除去这个组件还有不少其余的组件可供选择:
react-navigation
进行深度定制的 react-native-router-flux 路由管理做为整个 App 的骨架,它是这几个部分中最重要的一部分,合理地定制和使用路由管理能够极大地简化咱们的开发复杂度。
通常状况下须要缓存的数据基本上就多是咱们会在 App 不少地方都会使用到的全局数据,如用户信息,App 设置(非应用层面的设置)等,RN 提供一个 AsyncStorage 存储引擎,一般的使用方式是对这个数据引擎进行包装后暴露出符合咱们要求的读写接口。这里推荐另一种使用方式:
既然须要缓存的数据多是会在 App 不少地方使用到的全局数据,那么咱们能够将这些全局数据使用 redux 来进行管理,而利器 redux-persist 则能让咱们很优雅地读写咱们的缓存数据。
同时,若是对 react-navigation
进行合理的定制,接管其路由管理,那么咱们还能实现保存用户退出 App 以前最后浏览的页面的状态,用户在下次打开 App 依然能够从以前浏览的地方继续使用 App,固然,这个功能要谨慎使用!
App 的版本更新,RN 除了传统的 App 更新外还有一个热更新的可选项(传统 App 更新也有热更新,其原理就不太同样了),社区大多数人都推荐使用 codepush 来进行热更新,至于其后端解决方案 貌似已经有了一个 code-push-server ,咱们是使用本身的热更新方案,其原理就是在不更新原生代码的基础上更新 JS 代码和静态资源文件。
搜集的 App 使用数据(包括异常数据)并对此分析,根据分析来定位问题是保证 App 质量的有效手段之一。你能够选择本身搭建一套数据搜集服务,包括客户端 SDK 和服务端搜集服务,或者选择市场上已有的工具,目前较为成熟的收据搜集工具比较多,如友盟,mixpanel, countly 等等,在此不做赘述。
React只是视图层的解决方案,对于复杂应用,须要涉及状态之间的共享、各层级组件之间的通讯、多接口之间调用的同步等等,就须要进行应用状态管理,Facebook最先提出了Flux架构思想,后来社区又涌现了Redux、Mobx等不少种模式。通过调研比较,咱们选择了Redux进行应用状态管理,Redux的核心概念主要是经过Store、Action、Reducer、Dispatch实现单向数据流动,具体概念请参考官方文档。Redux经过middleware机制,能够对Redux进行各类能力加强,这个加强实际上是在action分发至任务处理reducer以前作一些额外的工做,dispatch发布的action先依次传递给中间件,而后最终到达reducer,因此使用middleware机制咱们能够拓展不少能力,例如咱们使用了状态持久化插件redux-persist,状态记录和重播插件redux-logger,而异步操做插件咱们经历了两轮技术选型redux-thunk和redux-saga。
支持函数action的redux-thunk经过简单的几行代码使得只处理plain object的action支持异步操做。
if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action);
Redux-thunk的实现很是简单,使用也很是灵活。咱们能够在action中处理各类异步操做,也能够作任何事情,可是它的缺点是它缺少对异步的直接处理,异步操做分散在各个action 中,而同步接口等操做依赖使用者本身的实现。
因而咱们进而选择了支持generator的redux-saga。Redux-saga经过一个相似于独立线程的方式管理你的应用程序中的反作用,这意味着你能够经过普通的redux action开始、暂停或者取消saga线程。Redux-saga使用ES6的generator来管理异步流,使得业务逻辑的读写和测试变得更简单。在咱们最新的架构中,咱们其实使用的是蚂蚁金服开源的dva-core。之因此选用dva-core,主要是由于dva-core整合了redux和redux-saga,而且使开发者能够经过一个命名的model文件集中管理一个业务逻辑的state,经过定义的effects管理反作用操做,经过定义reducers管理其余处理函数。一个完整的model大概是这样的:
export default { namespace: 'order', effects: {...}, reducers: {...}, subscription: {...} }
最后,关于应用状态管理,还有一个话题能够讨论,就是状态的不可变性immutable。在redux中状态是不可变的,每一个reducer都会产生新的不可变状态。那么这个不可变性是否须要不可变js库(好比immutable.js)的支持呢?简单来讲,immutable.js能够带来计算效率和存储效率的提高,可是它须要使用库支持的数据类型,因此若是从头构建一个应用,能够选择。若是是对于一个已有的复杂应用进行重构,那就须要综合考虑一下了。
总结一下,一个 RN App 架构应该要保证 App 的运行稳定以及开发的便捷。运行稳定这一方面,除了从 JS 层面(如单元测试,JS 错误上报等)保证以外,很大程度上还要依赖于原生层面的处理,因此团队里面要有同窗的精力能够投在原生研究上面,至于开发便捷,咱们尽可能将复杂重要或者简单繁琐的操做在构建工程时就作掉,这样也能够大幅度提升咱们的开发效率,下降开发者之间的合做沟通成本。
:::info
Scott 近两年不管是面试仍是线下线上的技术分享,遇到许许多多前端同窗,因为团队缘由,我的缘由,职业成长,技术方向,甚至家庭等等缘由,在理想国与现实之间,在放弃与坚守之间,摇摆不停,心酸硬扛,你们能够找我聊聊南聊聊北,对工程师的宿命有更多的了解,有更多的看见与听见,Scott 微信: codingdream,也能够来 关注 Scott 语雀跟进最新动态,本文未经许可不准转载,得到许可请联系 Scott,不然在公众号上直接转载,尤为是裁剪内容后转载,我都会直接进行投诉处理。
:::