一次编写,到处运行”(Write once, run anywhere )是很多前端团队孜孜以求的目标。实现这个目标,不但能以最快的速度,将应用推广到各个渠道,而且还能节省大量人力物力。
React Native的推出,为跨平台的开发带来了新的曙光。 虽然Facebook官方blog的说法React Native支持“Learn once, write anywhere.”。但经过开源社区的不断努力,React Native已经可以达到“一次编写,到处运行”的目标。可以说超过了Facebook的预期。作者在最近的几个项目中,运用React Native技术,成功实现跨越iOS,Android,Web三端的前端架构。这里将使用到的技术和过程中遇到的困难和问题揭示出来,供读者探讨。
技术选型
我们的目标是希望一套代码同时支持iOS,Android App和微信公众号内的网页(同时保留将来支持桌面浏览器的能力)。在开始重构之前,我们盘点了目前可用的一些技术:
① SPA:single page web application,就是只有一张html页面的应用。仅在该Web页面初始化时加载相应的HTML、JavaScript、CSS。一旦页面加载完成,SPA不会因为用户的操作而进行页面的重新加载或跳转,而是利用JavaScript动态的变换HTML(采用的是div切换显示和隐藏),从而实现UI与用户的交互。
② MPA: multipage web application, 相对于SPA,MPA有多个html页面。页面间跳转刷新所有资源,公共资源(js、css等)需选择性重新加载。
本人于2012年开始接触Cordova \u0026amp; Ionic,应该说Cordova 在React-Native出现之前确实是跨平台的主流技术。但是现在是2018年,Cordova 在性能上肯定达不到我们的要求,首先被pass掉。
Vue.js也是我们团队的备选前端框架,主要用于桌面浏览器展示的项目。缺乏原生移动解决方案,以及实际用下来感觉template表现力比不上JSX。另外我们用到了蚂蚁金服优秀的前端控件库ant design mobile, 暂时不支持Vue。
2018年7月份我们对Flutter(0.5.1) 和React-Native(0.51.0)进行了一次性能比较测试。我们在Android上用Flutter和React-Native分别实现了一个含图文的新闻客户端,比较了页面加载,图片加载,页面跳转等关键性能。实测下来Flutter在List加载,跳转到详情页时都有明显掉帧。另外代码无法移植到web上。这些原因导致我们放弃了Flutter。
最终我们选择了React-Native作为我们项目的实现技术,除了上述的一些优点之外,我们在如下一些方面收益颇多。
项目架构
我们在项目中用到的前端整体架构如下图:
以下对上图中一些技术点进行介绍:
应用支持层
作为应用和后台服务\u0026amp;原生App之间的桥梁,应用支持层需要处理诸如端到端通讯,数据加密解密,数据缓存,数据拦截,原生应用功能访问等基础服务。最大限度的屏蔽掉平台间差异,让位于其上的层尽量做到平台无关。
原生模块封装
React-Native 可以方便的封装原生应用模块。对于有UI的原生模块,既支持在一个新的ViewController(Activity)中展示, 也支持将其封装成一个View,嵌入到React-Native的上下文中。 这也是React-Native最接地气的特性,远超Cordova。在一些场景下需要等待原生模块中的事件,诸如用户操作等异步事件之后才能返回,这时需要用到Promise作为原生模块的参数。
比如通过调用手机摄像头,对银行卡进行扫描,这时会调用原生第三发控件的ScanCardViewController进行扫描,扫描结果通过代理函数回调。整个调用和回调的流程无法直接在一个函数中完成,这时可以用React native的Promise 实现对JS端Promise的无缝对接。
@protocol RCTBankCardScannerDelegate \u0026lt;NSObject\u0026gt;-(void)onScanCardResult:(NSDictionary *) result;@end @interface RCTBankCardScanner()\u0026lt;RCTBankCardScannerDelegate\u0026gt;@property(nonatomic, strong) RCTPromiseResolveBlock resolveBlock;@property(nonatomic, strong) RCTPromiseRejectBlock rejectBlock;@end @implementation RCTBankCardScannerRCT_EXPORT_MODULE();RCT_REMAP_METHOD(scan, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject){ //异步调用,函数本体不返回,需要保留resolve,和reject函数指针 self.resolveBlock = resolve; self.rejectBlock = reject; //跳转到扫描银行卡控件的ViewController ScanCardViewController * viewController = [ScanCardViewController new]; UIViewController *rootViewController = RCTPresentedViewController(); [rootViewController presentViewController:viewController animated:YES completion:nil];} #pragma mark RCTBankCardScannerDelegate-(void)onScanCardResult:(NSDictionary *) result{ // 在原生ViewController回调处,再返回Promise的处理结果 if(result != nil \u0026amp;\u0026amp; [[result objectForKey:@\u0026quot;code\u0026quot;] isEqualToString:@\u0026quot;0\u0026quot;]){ if(self.resolveBlock != nil){ self.resolveBlock(result); } }else if(result != nil){ if(self.rejectBlock != nil){ self.reject