由于一些原因,笔者最近变更到了RN的团队,回归到了hybrid app的开发的圈子中,固然是有蛮多新鲜感和新机遇的,不过遥想起以前在hybrid中各种view之前跳转的头疼等各种问题,笔者怀着忐忑的心情开始了一段波折的hybrid之旅。其实大概的结果之前的文章也有提及了,不过由于大部分只是以“记笔记”的形式描述的,所以难得想抽个时间,好好的总结一下,自己的心路历程。
众所周知,传统的webapp由于只能发挥native80%不到的机能,在性能和能力上一直为人所诟病,而传统的native app又需要耗费大量的人力,在这个互联网发展速度“过快”的时代,很少会有公司会持续这样“双份”的投资,所以hybrid app在一开始便以更高的性价比获得了业界的青睐,特别是进入react时代之后,react-native一经问世,便受到了业界大量的关注,然而几年过去,react已经更新到16.x,内部也迎来了极大的重构,fiber的推出,异步组件的提出,生命周期函数的unsafe,react似乎正在朝着越来越强大的方向不断进步,而react-native却依旧在0.5x.x的版本使劲挣扎,这里面却有一些不得不说的苦楚。
RN升级的辛酸
因为笔者团队维护了多个rn的app,但是由于一些历史原因,两个app几乎是独立的,投入度高的app rn版本能到0.4x,而另一个则仅有0.35.x,但是RN在升级的过程中,往往是伴随着一些破坏性更新的,很多旧的api在新的rn版本中是得不到支持的,但很多机型的兼容性问题或者说一些新的特性又只有新版本的RN才会提供,这也导致了RN升级的这件事是必须要实施的。也许对前端的同学来说,升级不就是换一下package.json的版本号就好了么?但实际上却相当的复杂:
1、首先,由于xcode本身开发上的一些设计,不管是第三方库还是官方库,都有通过pod或者通过Library的方式来引入,但是Library中的库中的有关引用的写法,随着版本的迭代大部分都不支持了,而且它那种组织方式本身就不是一种可复用的组织方式。
而且,在调试中你还需要不停地自己手动的清除xcode的缓存,xcode自带的clean并不会清除它的构建缓存,这将导致,你对组件的变更,可能根本没有生效!
解决方案:在iOS中,修改Library中有关库的引用方式,将它们全部迁移到通过pod来管理
2、然后,propTypes和createClass两者虽然早起都集成在react的包中(当然RN的包中也有集成),但是随着版本的迭代,两者都被移出的react的主包中,和react-dom一样,需要开发者手动引用,然而对于一个5w+行的项目,组件量达到300+,要进行这样的变更,是一件非常痛苦的事情。。
解决方案:只有手动的逐行变更代码,使用alias的一些小花招会令你的代码可读性变差
3、接着,你会发现,之前一直使用的fb提供的官方组件也没有了。。而且官方明确地表示,他们都被废弃了,需要开发者自己去引用react-native-deprecated-custom-components,比如范用度特别高的Navigator,但是为什么官方会废弃它们呢?笔者的个人观点是:“官方也没有提供一个令他们满意的解决方案”。比如刚刚提到的navigator,因为其自身在android上无可避免的会出现状态丢失的问题(虽然这是android的内存管理机制引起的),但是作为结果是,navigator就一定会导致在android上出现view状态丢失的问题,而且这种问题,不管是单vc还是多vc的环境下都会产生,且没有很好的修复办法(虽然从理论上来讲,推行全app的类redux状态管理器可以解决该问题),于是官方变废弃了该类组件。
解决方案:针对自己的业务场景,自己实现自己需要的功能,笔者的团队就采用了fork官方组件,再二次实现的方式(当然,最终仍然需要状态管理器来解决根本的android的bug)
4、然后,当你修好了官方组件的bug,觉得你的模拟器将要跑起来的时候,第三方的组件又开始大量报错了,当然,处理了上述那么多问题的你,这时已经想到了原因:因为react的版本和rn的版本都发生了大量的迭代了,其相关组件也跟着需要破坏性更新也是很正常的。但是现实是并不是所有的第三方组件作者都会持续更新。。所以,如果作者还在维护这个组件,那么你是幸运的,如果作者已经没有维护了,那么,要么你能在社区找到能够替换它的组件,要么你还得fork对应的组件,自己进行改动,并且成为维护人。
笔者也是如此碰到了一些令人头疼的问题:在笔者团队维护的一个app中,使用了一个历史比较悠久的第三方组件,而笔者的团队由由于一些原因,没有持续使用fb提供的packager或者metrobundler进行打包,而是使用了webpack,这导致了原作者可能并不了解前端的模块规范和打包方式,他写的组件能在metro中跑起来,但是当环境切换到webpack时。。整个组件却不能正常编译了。。
另外比如很多早起的组件在android中都会重载createJSModules的方法,但是到后续的rn版本中,该方法变成了虚方法,不需要重载直接实现就好了,而这些组件的维护者似乎都没有继续维护了,就需要使用者自己去fork组件来修改java代码。
解决方案:第三方组件,3分依靠作者对自己组件的责任心,7分依靠整个生态的热度,剩下90分,都得靠自己。
有关升级笔者碰到的问题大概就是这些了,当然这是除去IDE上使用的一些坑(小问题笔者便按下不表了,上文也只提到了严重影响笔者使用的地方),鉴于上文也提到了有关rn打包的事情,笔者接下来再聊聊rn打包的一些感受。
RN打包的一些感受
最开始笔者并不了解问什么fb非要重新造一个packager去进行打包,因为rn归根结底也不过是把写的jsx编译成一个es5的jsbundle,依赖对应平台的jsc来执行,同样是“打包”,为什么不使用webpack呢?也许是由于rn自己的特殊性吧?笔者如是想,但是不管是packager还是后来的metro bundler,做的事情几乎和webpack是一样的,而且他们都是用了babel社区的ast能力,去解析jsx文件,就连最后的注入require和__d的定义也和webpack注入webpack_require一样,为什么呢?也许是fb的工程师们在着手做这件事的时候,webpack的文档和社区并不像今时今日这样的强大吧?于是也导致了metro造了一个和webpack几乎类似的轮子,而且它的文档和它的前人一样差!且由于生态和环境相对闭塞,metro bundler的文档严重滞后了很多个版本。
话说回来,笔者之所以要更换打包工具,主要原因也是因为要做一些拆包的事情,但是由于metro-bundler自身生态的闭塞,且文档确实少得可怜,而业内对于rn拆包打包还是有一些例如haul这类基于webpack的实现的,于是笔者也借鉴了社区的一些智慧,进行该实现。
但是往往理想很美好,显示很残酷。虽然webpack的生态已经很成熟了,但是在使用webpack来打包rn的时候,还是不可避免的会碰到一些问题,比如:
1、编译环境的不同。因为webpack针对的不仅是是给浏览器的,还包括了node端,所以会带有一些编译平台所自由的方法或者类库,比如说console、Math、Date、crypto,但是rn由于是在native环境运行的,而fb一开始考虑的场景就和传统的web应用不完全一样,笔者在进行打包工具切换的时候就遭遇了原本metro能够运行的库但是在webpack的编译环境中会报错。
解决方案:对于编译环境的不同可以尝试使用webpack的target属性为webworker,这个属性设置的时候会更贴切rn的编译环境
2、图片处理的不同。rn对图片有个比较贴心的处理,它能够自动通过@2x @3x去自动适应2倍和3倍屏的图片,但是原本的webpack是不支持的。
解决方案:自己编写一个loader去解析图片
3、一些全局变量。相信进行rn开发的同学或多或少都会使用debugger,但是debugger会依赖rn挂载在全局的一些变量,比如require和__DEV__,而这些变量在webpack里则需要通过providePlugin或者其他的方式注入。
另外,笔者的项目由于一些历史原因,像比较流行的严格模式都会引起项目运行报错,还有一些json文件的读写,也是metro的编译比较友好(或者没有传统前端严格,比如json文件里的注释),而webpack的相关loader则不会让一些这些“友好”的写法通过编译。
总的来说,其实大部分使用RN的同学可能和笔者都会有同样的想法:
“原本美好的write once,run anywhere,但现实是write once,fix everywhere,而且根本没法fix完。”
其实rn的问题或者说特性还有很多,像那个依赖setState通信的动画,主线不断推送的事件,还有很多单平台才有的属性,都是rn开发的障碍, 所以就笔者而言:目前的rn更适合一些有native能力的团队在一些并不是很要求app性能的条件下使用的解决方案,在没有经过适当优化的基础上其性能表现并不一定有webapp好。