一、缘由
随着 react-native 在 App 中服务的业务不断增加,业务类别不断拓展。原有的一个 main.jsbundle 在做一次热更新,可能会对所有 react-native 服务的业务造成影响,所以 multi bundle code push 势在必行。
二、现状
使用微软官方开源的 react-native-code-push@5.6.1 版本 + lisong 开源的 code-push-server 。因为微软官方在9月份发布了 react-native-code-push@5.7.0 ,这个版本对 api 接口重新进行了设计,为了避免这种不可控因素对业务的影响。在看到这个版本后,对依赖进行了clone,并发布在公司的私有 npm 上。
三、调研
网上这方面的资料并不多,没有官方的解决方案。唯一可以参考的是 haven_chen 发表的这篇文章 同app下多个react-native jsBundle的解决方案,但是作者最终其实是利用同一个 deployment key 对多个 jsbundle 文件进行更新,且最终仍有隐患,故未采用此方案,但是这篇文章中如何在 App 中嵌入多个 jsbundle 文件有挺好的参考作用。
四、探索
4.1. 支持2个deployment key的方案
要想对2个项目进行更新,一定需要2个deployment key,那么是否可以对 code-push-server 发起携带不同的 deployment key 的 checkForUpdate 请求便是一个重要的环节。
或许微软在设计 sdk 的时候,有过这方面的考虑,所以我们可以从 react-native-code-push/CodePush.js 中找到答案。

只需要在此处,传入一个 deploymentKey 便会替代 nativeConfig 中的对应字段,所以我们在传入 CodePush 的 config 中传入 deploymentKey 字段即可~。(具体调用流程可仔细阅读集成文档&源码,此处不再详述。如:config 是什么?为什么在config中传入 deploymentKey 此处会生效等?)
4.2. 客户端如何处理最新的包
既然发出更新请求的问题迎刃而解,下一个问题就是客户端如何使用、存放最新的 jsbundle。
很多前端的同学可能到这一步会被卡住,因为我们经常是使用脚手架去生成整个项目,而没有直接在现有 App 中去集成 react-native 这里详细讲一下自己的心得体会。
4.2.1 iOS
1). 搭建环境,新建项目
新建好一个 Hello world 项目想来对大家绝非难事,由于公司还在使用 Objective-C,所以我新建了一个使用 Objective-C 的项目,了解 Swift 的同学可以参考,对照一下。
我们的需求是,需要能够跳转到 2 个不同 react-native 的项目,很多集成到现有的原生应用的教程,开始教大家在项目里建 RN 文件夹了,其实文件夹并不是关键,pod 需要的依赖才是关键。
不知道 pod 的小伙伴可以先搜索一下,我们一起来看 PodFile 文件

可以看到,其实你在根目录下建什么文件并不重要,ios 真正想要的是你的 node_modules 下的东西,用来做项目依赖。所以正常情况下,只需要你有 package.json,并装好了 node_modules 。接下来仅仅是调整路径的问题。
只要你执行 pod install ,react-native 就集成进了你的原生应用中,你甩都甩不掉。
2). 开发界面
有些同学一看到 OC 的代码就觉得头疼,总感觉有莫名其妙的 [], @,让代码看起来非常难懂。
其中比较重要就是大家需要知道 [object setName: @"juejin"]; 这个写法的意思就是 object.setName("juejin"); ,接着你就不会对这些符号感到恐惧,可以非常顺利的读懂代码。接下来补充一下 OC 的基础知识,看看菜鸟教程应该就够用了。
新建好的项目里有一个 ViewController.m,一看就是界面的扛把子,你只需要搞定他就ok了。
在网络上简单的搜索了一下怎么添加一个按钮,就很容易写出接下来的代码。

主要的作用就是新建一个按钮,并设置位置,设置点击事件,添加到主屏幕上。
3). 跳转RN
按钮已经制作好了,接下来需要写跳转到 react-native 的代码了。
在网上我们比较容易找到集成离线 bundle 的 OC 代码

主要是利用 [NSBundle mainBundle] URLForResource 这个方法,接下来我们看一下 CodePush 怎么做到的。

对比第一行,是不是发现如出一辙,而且这里可以定义 jsbundle 的名字,如果觉得 index.ios.jsbundle 不好听的小伙伴可以顺利换名字了。
4). 改造 react-native-code-push 原生代码(重要)
终于来到改造的重点了,讲如何改造前,先说一下,如果不改造 code-push 是如何存储大家的增量更新包的。

首先,从 code-push-server 知道有更新文件的时候,他会把此文件下载下来,假设就是如图中的 12aXXX
然后,他会写入 codepush.json 这个文件中
{
"currentPackage": "12aXXX",
"previousPackage": ""
}
下一次,从 code-push-server 又知道有更新文件,假设是如图中的 54eXXX
下载,然后写入 codepush.json 这个文件中
{
"currentPackage": "54eXXX",
"previousPackage": "12aXXX"
}
正是因为这个机制,他可以永远使用最新的代码,却也无法帮你同时维持 2 份最新代码。我们正是要对这个写入的机制进行改造。如果我们可以把这个 key 值改造成 ${deploymentKey_currentPackage} 格式的,不就成功了嘛。
经过对源码进行阅读,最终发现规律,所有进行读写的地方都集中在 CodePushPackage.m 这个文件中,且都有很规整的 info[@"currentPackage"] 或是 info[@"previousPackage"]。这个时候觉得是天助我也,我们只需要在这个常量前面拼对 deploymentKey 就ok了。
展现其中一处是如何改造的:

然后我们只需要知道当前打开的页面是哪个 deploymentKey 就可以了,以下是个人的一个方案,并不优雅,对 OC 比较了解的同学可以自行改造。
CodePush.h 中增加全局变量 extern currentDeploymentKey;
CodePush.m 中对 currentDeploymentKey 初始化,并在 setDeploymentKey 中对此值进行赋值

跳转到页面前,调用 [CodePush setDeploymentKey: @"真正的deploymentKey"];

4.2.2 Android
1). 环境配置
java 代码其实比较好懂,十分规整,都是强类型,这里先简单介绍一下。
-
和
ios一样,准备好package.json, 装好依赖 -
关注
app/build.gradle和root(项目根目录)/build.gradle和root(项目根目录)/settings.gradle



- 有配置文件或目录变动,记得要 sync

2). 集成RN

这是一个 demo 的目录结构,大家可以参照一下脚手架生成的项目,看一下不同点。
脚手架生成的项目里只有一个 ReactNativeHost ,我们在这需要定义 2 个,并依靠一个 type 对加载哪个 jsbundle 进行控制。
脚手架项目中 MainActivity 直接集成的是 ReactActivity,我们这 MainActivity 是原生的 Activity

我们需要定义多个 RN 的 Activity文件,拿其中一个举例子。这里的 MODULE_NAME 就是 RN 脚手架生成的项目 app.json 中配置的值。

界面的开发不详述了,因为我自己也是凑出来的,大家可以自行研究,主要有 id 属性,并能在 MainActivity 中增加跳转事件即可,不会绑定的同学,可以看看 MainActivity 的截图。
3). 改造 react-native-code-push 原生代码(重要)
原理已经在 ios 部分介绍过了,改造 Android 的时候,就直接搜索了 currentPackage,最终在 CodePushConstants 中找到了他们。

然后又是一顿搜索大法,把用到这两个key值的地方全部换了一遍(都在CodePushUpdateManager文件中),例如

在跳转对应页面前对类型和 deploymentKey 进行设置,即可达到最终效果。(这里 currentDeploymentKey 设置在 CodePush 类的一个公共变量上了,大家可以按自己想法放置)

4.3. 后续事宜
将以上改造,发布一个包上传到公司私有库,可以叫 react-native-multi-code-push (仅供参考)。
并与 Android & iOS 开发同学沟通调用改动,让原生开发同学帮忙 review 一下代码。
增强一下健壮性,一些用死值的地方,尝试使用类去管理。
五. 小结
其实多个 jsbundle 更新在原有的基础上改动并不算十分复杂,更多的是要克服对环境搭建,不熟悉的语言的恐惧。
想一种对原有代码侵入不大的解决方案,对代码进行改造。