搜车 React Native 依赖管理方案

芋头  |  2017. 05. 24   |  阅读 3852 次
无线开发 react native

本来准备写一篇完整的文章介绍下我司的 React Native 整体方案的实践,但是下笔之后发现话题实在太多太长,很难写成一篇观点集中的文章,所以计划分几篇文章来介绍,希望对正在或者准备在 RN 上搞事情的同学有所启发,另外一定要根据自己的业务场景来考虑问题,切勿人云亦云,不知所以。所以本文除了介绍我们最终是怎样做的之外,我会着重强调“为什么这样做”。

本文主要讲的是 RN 版本依赖和热更新相关的话题,这是我们团队的 RN 方案和业界流行的方案差异最大的地方,也是我们耗费心力最多的部分。本文会先讲一下方案的主要思想,然后介绍下场景和缘由,最后详解一些细节的实现

下文以 SRN 代表我们团队的 React Native 方案(souche react native)。

首先介绍核心思想,在我们团队内部,最开始对 React Native 的版本依赖就达成这样一个共识:

  • 客户端版本和 RN 业务版本强依赖,特定客户端版本会锁定特定版本的RN业务,RN 业务迭代需要客户端发版。
  • 同时提供热更新能力,但是热更新只能用来 hotfix 修复bug。

其实我们整个方案都是围绕这两个重点来实现的,很多同学看到这两个核心点可能会非常困惑,可能就此结束阅读此文,但是我是希望大家能够继续看下去,看一下为什么我们要这样做再做决定。

为什么发布 RN 业务需要发版?

要讨论这个话题,其实首先需要讨论一个终极问题:为什么要引入 RN?不同的团队可能会给出不同的答案,这都无可厚非,毕竟每个团队的问题和场景都不同,自然引入一个新技术的理由也不同,唯一最差的理由可能就是“没有理由”。

对于我们团队来说,引入 RN 并不是因为它的代码可以热更新这个特性,H5代码自带远程属性,为什么不用H5而是要用RN?我们的场景,偏B端,重交互而轻营销,产品关注的主要是稳定性,而不是随时变化的能力。对于需要灵活变化的场景,用H5完全可以满足需求,交互和体验/开发效率都没有太大问题。所以我后来一直跟大家强调,RN 绝不是用来替换 H5 的。那为什么要引入 RN 呢,其实在我们正式引入 RN 之前,我们已经在 RN 上积累了半年多,甚至组件库都做出来了,但是从来没有在业务中尝试。后来,有一个契机,公司在C轮之后走向了团队规模极速扩大的道路,之前我们公司很多B端产品都是重客户端的,这时候团队的瓶颈开始凸显,业务的客户端开发成本很高,Native的一些缺点开始暴露,例如 开发不够灵活/跨端技术栈发展不统一/人力成本double(这是最突出的问题),对于需要快速试错快速迭代的公司阶段来说,RN 的优势凸显出来了,而这,就是我们引入 RN 的最重要的原因。

想清楚了为什么要引入 RN 之后,应该如何使用 RN 也就很清楚了,我们并不特别关注 RN 的热更新能力,而最关注的主要是两点:

一、如何将 RN 的开发工程化,不管是客户端开发还是前端开发,都可以用这套方案快速开发普通的业务。

二、稳健性。包含多个方面,底层的稳定,代码健壮性,版本依赖管理。

第一点,我们之后再讲,这是我们整套体系中的一部分,做了很多事情来保证业务可以被快速生产,包括脚手架和开发框架/组件库等。而第二点,就是文章开头我们提到的核心思想。

为什么要强依赖?什么是强依赖?

我先解释下,什么叫做强依赖。一句话表达就是:特定版本的app中某个 RN 业务的bundle的版本也会被锁定在某个版本,区别于覆盖式热更新和H5的实时访问机制。

为什么需要这么做?有以下几个因素考虑:

  • 其一,跨端依赖。RN 中会大量依赖客户端的功能,我们的 RN 业务都是内嵌到已经很成熟的APP中(我们公司现在有4个主要的APP),除了 SRN 内部封装的一些固定的 Native 功能的调用,最主要的是会依赖一些 Native 的协议跳转,例如 从某个 RN 页面打开 Native 一个页面,然后这个 Native 页面可以再打开另外一个 RN 业务的页面,具体如何实现的可以改天讲,一句话来说就是通过 scheme 协议来做 RN 和 Native 互相的跳转,这个协议其实就是一个强依赖,如果 RN 新发布了功能,但是客户端不支持新引入的一个协议,线上客户端更新了这个 RN 的包,跳转就会出现问题,此为其一。
  • 其二,底层升级。RN 官方还在快速迭代版本,很多重要功能也在不断发布,例如 0.43 的 FlatList,如果没有版本锁定的功能,可能很难升级底层 SDK 的版本,之前在听一些分享的时候很多公司都会困扰与此,但是对于我们团队来说,这里完全没有问题,基本可以随时升级 底层 SDK 的版本,最近我们刚升级到 0.43。当我们要给一个 app 升级底层 SDK 的时候,只需要保证新版本的业务包和SDK兼容即可,无需关心线上已经存在的版本,因为他们依赖的RN bundle 被锁定在了之前的版本。
  • 其三,功能升级。SRN 出炉后,我们快速上线了十几个业务,但是后来我们给整个方案加入了很多新功能,例如 Native Bridge 新增方法/bundle 拆分等,这些功能都是不能做到向上兼容的,而对于我们实现的机制,我们也无需考虑向上兼容。另外,最近我们整个发布集成的流程做了很大的改造,但是这种更新我们无需考虑各种版本依赖的问题,只需要考虑新版本 RN 包兼容新方案新SDK即可。

不知道大家在使用覆盖式热更新的时候有没有遇到这些问题,希望能有所启发。

既然强依赖了,为什么还有热更新?

强依赖是个好东西,但是热更新也是个好东西,我们不能舍本逐末,我们一直在弱化热更新的优势,但是不能否认它在某些场景下的必要性,例如紧急修复线上bug,例如一些偏营销的业务场景,也会通过热更新带来快速发布的能力。

但是,我们在热更新和强依赖之间做出了一个平衡,将二者结合在一起,业务方可以将某个版本的app依赖的某个RN业务锁定在某个版本,但是这个版本同时也具备热更新能力,可能很多同学已经猜到了,我们是用两个版本号来维护这个逻辑的,准确来说并不是两个版本号,而是一个业界统一的版本格式:语义化版本控制规范(SemVer)。

格式如 ${major}.${feature}.${patch},遵循 semver 规范的版本号
    选择需要递增的版本号
        major: 主版本号,用于断代更新或大版本发布
        feature: 特性版本号,用于向下兼容的特性新增
        patch: 修订版本号,用于 bug 修复
递增位的右侧位需要清零,如 1.1.2 => 1.2.0

接下来我们就讲讲这里的依赖逻辑。

如何锁定依赖?如何热更新?

接着上面语义化版本号来讲我们的具体实现,可以发现我们需要版本锁定和热更新的需求,其实和*SemVer的诉求完全一致,自然而然的,我们用 *${feature} 来锁定版本,用 ${patch} 来热更新,从语义上来说,锁定版本后需要更新功能需要升级 ${feature} 版本,这就是一个 feature,需要热更新就是修复问题,升级 ${patch} ,这就是一个 hotfix(or patch)。

最终,其实就是我们的app在用户手机上运行时,会发起一个热更新的请求,而这个热更新的具体逻辑只会判断“相同 ${feature} 下的最新的 ${patch} 版本号”,本文整篇的精华就是这句话,大家细细体会下。

具体到 SRN 中,其实这个逻辑不是放在客户端,也不是放在RN代码中。而是放在一个专门维护版本依赖关系的Node服务中,将整个过程解耦,这样依赖逻辑可以随时统一更换。

每个RN业务发布的时候都会讲包先上传到 CDN,然后将这次发布的信息记录到这个 Node 服务中(如果不小心发错了版本,在Node服务中操作一下就可以删掉一次发布),然后每个用户app启动的时候,会把用户本地的所有RN业务包和他们的版本发送给Node服务,Node服务会在服务端判断 我们刚才提到的这个逻辑,返回“相同 ${feature} 下的最新的 ${patch} 版本号”,然后客户端就会启动热更新逻辑去更新Node服务返回的最新版本的bundle。

这里还有一点需要提到,其实我们对 ${major}.${feature}.${patch} 还做了扩展,以便支持测试/开发/预发 环境上的版本,例如测试环境上的版本号会是这样:0.1.2-beta.8,也就是发布测试版本不会增加 ${patch} 版本,而是会在扩展字段上增加 ${beta} 版本,对于扩展字段,因为是开发环境,处理规则是和 ${patch} 版本一样,直接热更新,所以除了线上环境,所有其他环境更改代码都不需要重新定义依赖。

另外一个问题就是这么复杂的版本规则,如何友好的暴露给开发者?其实对于开发者来说,根本不需要自己维护版本号,也不需要知道这个规则,在业务发布前,脚手架会给选择,“环境:测试、预发、线上”,“发布类型:功能迭代、bug修复”,这些都是交互式命令,选择对应命令后,会自动升级当前业务的版本号,自动发布和打tag,无需开发手动维护。

用逻辑图表示:

alt

等等,是不是少了点什么?

关于版本依赖锁定和热更新其实到这里基本结束了的,但是仔细深入思考的同学应该发现好像少了点什么,的确,有这些还不够,你的整个流程还跑不起来,少了最关键的一步:依赖声明和本地集成

特别是第一次发版的时候,客户端如何知道当前版本的app依赖了哪些业务和锁定这个业务的哪个 ${feature} 版本呢?

在开始的时候,我们的方案是在app的工程里维护一个package.json 一样的文件,内部声明好 dependence,和 npm 包的 package.json 格式类似。然后在jenkins或者ide里打包app的时候,会执行一段脚本,这个脚本会将这个package.json的内容上传到刚才提到的 Node 服务中,服务会走一个跟线上热更新同样的逻辑,将当前锁定的${feature}版本的最新${patch}版本返回,然后这个脚本会去下载这个最新的RN bundle,将其直接集成到工程代码中,并且更新 package.json 内的依赖声明。

当发布了一个线上bundle的时候,如果选择了“功能迭代”,会自动给业务的 ${feature} 加一,这时候需要发布新版本的客户端,就要手动去改一下 package.json 中的这个 bundle 依赖的版本号,例如业务从 1.2.3 到 1.3.0,就把package.json中的版本号改为 1.3.0 ,如果后面你发现有bug又发布了一次,不需要再改 1.3.1 了,打包的时候运行的脚本会自动帮你改掉。也就是每次发版后,这个版的app内都会默认跟一个业务的最新版的bundle。

这样做看起来挺完美的,但是后来我们发现还是有不少问题的,例如,我们的脚手架没有把这个版本号的规则太多的暴露给开发者,但是开发者却需要在每次发版前维护客户端中的这个依赖,我们的app以及业务发版都很频繁,造成整个流程不顺畅。另外打包的脚本稳定性和运行性能都会影响打包的过程,这个侵入对客户端和测试同学体验不好。

现在,我们马上就会上一个新的方案,原理是将所有RN的包从概念上包装成 Native的包,例如iOS的pod包,Android的maven包。每次发布的时候,除了之前的流程,现在最后一步,我们会把js bundle分别包装成一个 iOS 的 pod包,Android 的 maven 包。然后客户端直接将 RN 的业务当做一个正常的Native业务来使用。

这样,去掉了两个动作:app打包时不需要执行集成脚本了,对 app 打包流程没有任何侵入;另外,不需要在客户端声明额外的依赖了,客户端只需要用传统的方式,在 podfile 或者 gradle 里声明依赖就可以,pod包和maven包的版本和RN业务的版本完全一致,规则也一致。

当然,这里省略了很多细节,如果大家有需要详细了解的,可以私下联系我。

总结

因为有版本依赖锁定,最近我们的 SRN 整个升级到 0.43 以及 集成 bundle 拆分的功能 还有整个集成过程的彻底改变,都非常轻松,升级过程完全无痛,如果大家有遇到同样的困惑,可以参考一下。后续其实我们也打算在某些偏营销的场景中,可以不通过发版集成直接在一个app中访问一个RN业务,其实在现有的机制上做一点小改动即可,还是那句话,看场景,不要彻底否定某些方案,可能只是大家遇到的场景不同,而技术的价值正式解决业务场景中的问题,而不是为了技术而技术。

有问题欢迎加我微信:mier963 咨询,感谢阅读。

分享到

   
ReactNative源码篇:启动流程