前言.
本文是从实践中积累下来的,从刚接触的一脸迷茫,到走通流程,遇到一些问题,拿来与大家分享。
本文主要基于code-push-server自建服务器实现热更新
<h1 id="1">1. 关于热更新要弄清的</h1>
允不允许的问题
主要担忧在ios,听说ios禁止热更新,但是这个也是口口相传,有更甚者煽风点火,到底如何:参考这篇文章
Q&A
Q: “苹果应用商店和android应用商店允不允许使用热更新?”
A: “都允许。”
苹果允许使用热更新Apple's developer agreement 条款3.3.2, 但是规定不能弹框提示用户更新,影响用户体验。苹果禁的是 rollout.io, JSPatch 这类具备修改原生代码能力的框架。 Google Play也允许热更新,但必须弹框告知用户更新。在中国的android市场发布时,都必须关闭更新弹框,否则会在审核应用时以“请上传最新版本的二进制应用包”驳回应用。
更新什么的问题
上面也说了。你所使用的更新方案,是不允许动态修改源码实现的。所以,大体上热更新就是更新rn模块代码和对应一些资源
怎么更新的问题
热更新的流程,开发者开发 》 发到服务器 》 用户下载 》 用户客户端app处理使用新的代码资源等。所以,重点在于,服务端,和客户端。
服务端:用户处理新推上来的“功能包”
客户端:要有下载,更新“功能包”的处理
更新场景
通过某种方式,动态的操作用户app代码资源等,达到不用发版本即可上新功能的效果。明显的例子,比如双十一,六一八,特殊节假日等app上的活动页面之类的。某些功能为什么能定点开启,定点关闭。如果是发新版本,那单单是苹果的审核,是不可能达到的。
1.1.原理
rnhotload.png1.更新:
下载最新JsBundle文件以及所需要的图片资源等,下载完成后解析最新JsBundle文件。
2.不更新:
判断本地是否还有缓存的JsBundle文件:
1).存在:
本地存在JsBundle,即有过热更新操作。那么App直接加载在缓存目录下的JsBundle文件。
2).不存在:
本地不存在JsBundle,即之前从未有过热更新操作。那么App只能使用初始化时打包在assets目录下的index.android.bundle文件。
1.2. 现状
现在rn热更新方案大概如下:
- 微软codepush
- 基于code-push-server自建服务器
- rn中文文档维护的react-native-pushy
- 纯自搭服务器:基于bsdiff和patch一些算法
2. 热更新方案
2.1. 方案选型
研究rn,主要还是用到rn的跨平台和热更新,关于热更新这块,刚入门,可能会考虑以下方面:
- 资源
- 第三方?流量控制,安全性等
- 自行实现,仿h5离线包更新
- 技术手段
- codepush
- react-native-pushy
- 自建服务器(基于code-push-server)
- 自建服务器(纯自建:bsdiff和patch。更像h5离线包的流程)
- 方案
- 全量
- 增量(补丁)
- 服务器
- linux
- windows
- mac
- 平台
- android
- ios
Q&A
Q: “为什么推荐code-push?”
A: ”非常好。除了满足基本更新功能外,还有统计,hash计算容错和补丁更新功能。微软的项目,大公司技术有保障,而且开源。
1).直接对用户部署代码更新
2).管理 Alpha,Beta 和生产环境应用
3).支持 React Native 和 Cordova
4).支持JavaScript 文件与图片资源的更新
5).CodePush开源了react-native版本,react-native-code-push托管在GitHub上。
其实就是在自己服务器上搭建一套codepush系统,这样比较简单也省事。除了服务器地址与微软的不一样,其他的完全一样。
也就是说,我们不放心把代码资源之类的放到微软平台上,那就放到自己的服务器上,还保留codepush其他所有优缺点。
Q: codepush支持增量更新?
A: react-native-code-push
这文章里面有这么句话:
The CodePush client supports differential updates, so even though you are releasing your JS bundle and assets on every update, your end users will only actually download the files they need. The service handles this automatically so that you can focus on creating awesome apps and we can worry about optimizing end user downloads.
默认:增量更新,这个服务器优化处理过的。每次上传全量bundle包和assets资源文件。服务器会计算diff包,客户端请求更新的时候只会下载需要的代码或文件。
所以:实践中可以发现,你打包上传上去后,在服务器文件存储中,包是很小的
结论:
基于Code-Push-Server 自建服务器:
1:资源安全性-资源是在自己服务器的
2:速度-自己服务器
3:操作简单高效-基于codepush
2.2. 更新内容
上面简单阐述了热更新要弄清楚的问题, 那么rn热更新都能更新哪些呢?
更新内容
app页面组成就两种:1 ui,2 数据
更新主要为:JS, HTML, CSS and images
js更新能满足大部分需求。但是如果想热更新本地视频。那就要另想法子了
在react-native-code-push官文中Supported Components部分也描述了:
组件 | 属性 |
---|---|
Image | source |
MapView.Marker | |
(Requires react-native-maps >=O.3.2) | image |
ProgressViewIOS | progressImage, trackImage |
TabBarIOS.Item | icon, selectedIcon |
ToolbarAndroid | |
(React Native 0.21.0+) | actions[].icon, logo, overflowIcon |
以下列表的组件的某些属性目前还不支持CodePush更新
组件 | 属性 |
---|---|
SliderIOS | maximumTrackImage, minimumTrackImage, thumbImage, trackImage |
Video | source |
如上,如果要热部署视频的话,并不是一个好的选择,视频一般不会很小,这跟差异更新理念相悖,会导致更新延迟较大。视频可以通过访问服务器下载。
2.3. 基于Code-Push-Server 自建服务器
3. 实践mac平台搭建服务器实现rn+android热更新
3.1. 搭建服务器
3.1.1. MySQL服务器
百度搜,官网自行下载
注意:下载 文末点击 <font color="#0000FF" face="STCAIYUN">No thanks, just start my download</font>
安装
安装后环境变量设置参考文章,注意,文章中文末的设置密码不用处理。看下面注意事项
注意:密码方式选择Use legacy password。。现在很多资料中的截图还是老的,MySQL installer 会有默认密码,但是这个比较新版的是你安装时候就自己设置的,记住就行了。另外还要注意:密码规范:最好是数字字母结合
将mysql的命令添加到系统中
- 进入/usr/local/mysql/bin,查看此目录下是否有mysql
- 执行vim ~/.bash_profile
在该文件中添加mysql/bin的目录
PATH=$PATH:/usr/local/mysql/bin
添加完成后,按esc,然后输入:wq保存。 - 最后在命令行输入source ~/.bash_profile
3.1.2. Navicat for MySQL
这个数据库的GUI,我给大家一个博客地址,自己去搜,最新版本的下载不了,就按时间线往后挨个点,看哪个能下载,毕竟博主要挣钱的。不用感谢我,实在的去文末打赏😙😘
建立连接
上面2步安装后,需要关联两者。建立连接部分参考文章,注意,其他像什么建立数据库的先不要做
3.1.3. 部署code-push-server
github源码,建议用INSTALL FROM SOURCE CODE。
服务器配置
- db
部署完code-push-server,需要初始化数据库。这个一般是会初始化失败的,需要给db配置初始密码:
$ ./bin/db init --dbhost localhost --dbuser root --dbpassword xx00(刚刚刚安 装的数据库密码)
报错:
initdberror.png配置初始化密码:
configcodepushserver.png- config
这个token是随机的,并没有什么用,所以,打开那个网址,复制粘贴就行了。
注意:服务器downloadurl配置,一定配置为你本机ip地址。
loginserver.png登录服务器
注意: 注意顺序,先启动服务器,再登录,拿到随机分配的token,输入到终端,登录成功
1: ./bin/www :先启动服务器
2: code-push login http://127.0.0.1:3000 : 再去获取token
3: code-push app logout :退出登录
下次再登录时候,还要再获取token,才能登录上
注意:如果以上实在登录不上:
1:删除登录记录 /Users/用户名/.code-push.config 文件 再登录试试
2:还不行,可以删除code-push-server服务重新部署一遍,😭
3.2. 热更新开发
3.2.1. 原生android开发
注册app
注册app,服务器会分配2个deployment_key。staging,production。这个key自己记一下,等会让配置客户端热更新是要用的。
code-push app add MyApp-android android react-native
MyApp-android android
MyApp-android
MyApp android
怎么组合试都不行,后来试windows平台的成功了。再回来试android/ios平台的就可以了,我这里并不知道为什么,硬试出来的,如下:
$ code-push app add xxt_react_native-android android react-native
失败例子:
$ code-push app add xxt-react-native-android android react-native
registeapp.png
os平台:
windows 微软window phone
android android
ios ios
注意点:
app名称-客户端类型:客户端类型必须小写
MyApp-android
MyApp-ios
PS.相关命令
$code-push app list 或则 ls 列出账号下面的所有app
$code-push deployment ls <APP_NAME> -k 查看Deployment Key
$code-push app add 在账号里面添加一个新的app
$code-push app remove 或者 rm 在账号里移除一个app
$code-push app rename 重命名一个存在app
$code-push app transfer 把app的所有权转移到另外一个账号
注意:
注册app时候,名称要注意一下,这个名字只是在微软服务器的名字,所以,随便就行。例:
如xxt-react-native 这种是无法注册上的,不合法,所以,不要误认为必须跟你rn工程名保持一致,随便一个名字就行
客户端集成 react-native-code-push
- yarn add react-native-code-push -save
- yarn install
- rnpm link react-native-code-push
在依赖react-native-code-push时候,报错,文件找不到之类的,总之无所谓了,手动配置就行了:
1):在 android/app/build.gradle文件
apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
2):在android/settings.gradle中
include ':react-native-code-push'
project(':react-native-code-push').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-code-push/android/app')
3):在android/app.gradle中
dependencies {
implementation project(':react-native-code-push')
}
第3步这个,在rnpm link react-native-code-push时候应该已经给你配置上了,自己检查一下,这个一定要依赖的
注意:react-native-code-push 版本
这个如果你已经依赖上了,对照版本发现不对,不要侥幸,还是重新依赖吧。不然出现问题,都不知道是哪的问题,因为rn现在版本更新很快,版本间改动很大
客户端环境配置
1:deployement_key
1):获取
上面注册app的时候,已经分配key了。如果找不到了,用命令查看:
运行 code-push deployment ls <appName> -k
xxt-zyj:~ zyj$ code-push deployment ls xxt_react_native-android -k
appdevkey.png
<h6 id='5'></h6>
2):配置deployment_key
在 app/build.gradle中
buildTypes {
release {
...
buildConfigField "String", "CODEPUSH_KEY", '"production deployment key"'
}
releaseStaging { //测试环境下的release 打包
...
//特别注意 。android studio 更新到3.0 以上的。
// 因为新版本的as 是用dex 进行打包的,那么特别需要加上如下的几行代码。
// 如果没有加上这么几句话那么就会存在说找不到文件路径,从而不能打包。
matchingFallbacks = ['debug', 'release']
buildConfigField "String", "CODEPUSH_KEY", '"staging deployment key"'
}
debug {
versionNameSuffix " Debug"
debuggable true
buildConfigField "String", "CODEPUSH_KEY", '""' //debug模式下没有热更新
}
默认的部署名是 staging,所以 部署秘钥(deployment key ) 就是 staging。
关于staging 和 production的细节:
production 和staging 的意思 (production 代表的是发布的版本。staging 代表的是测试的版本)。这个需要千万记住,因为后续还要用这两个来进行配置测试环境还是开发环境。不然就会出现在真实中也能收到测试环境下的热更新消息了,等下会对这个进行配置. 同时我们也可以进行查看我们的项目的key ,登录官网也可以查看, 通过命令行进行查看
code-push deployment ls appName -k
因为我们在codepush上发布是有两种版本的,staging 和release 版本。那么我们就要打包两种模式下的版本。同时还要设置codepush-key。如下图所示。然后在app目录下的build.gradle进行配置。要理解一点,staging 其实也是release 版本,不过是两种release 版本。那么就需要在这里进行配置,然后Staging 中的内容除了key 都要和release 一摸一样
2:初始化codepush客户端
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new NewMainReactPackage(),
new RnToNativePackage(),
new CookieManagerPackage(),
new CodePush(BuildConfig.CODEPUSH_KEY, XXXApplication.this, BuildConfig.DEBUG, "http://your server ip:3000/")
);
}
3.2.1.1. 需要注意的问题
CODEPUSH_KEY 可能报红找不到
1):应该是项目自动引入了 codepush的 BuildConfig 。那么将import BuildConfig 删除就行。。。(在application文件顶部import中找看有没有引入)
2):可能是打包类型中有些没有配置codepush_key
buildconfig3.png所以导致下面的问题:
buildconfig1.png buildconfig2.png所以,所有版本构建类型中,都要配置codepush deployment key
releaseStaging 打包配置问题
许多资料只是简单的描述了codepush_key的配置,容易让人产生以下问题:
1):releaseStaging还有其他什么配置吗
2):配置完了,sync project还有如下问题,是什么原因
在releasestaging版本构建配置中,一定要加上这句:
matchingFallbacks = ['debug', 'release']
staging构建类型名称问题
releaseStaging,staging打包类型名称也不是乱取的,必须要带上是releasexxx。看下源码
app版本versionName需要注意的问题
1:在 android/app/build.gradle中有个 android.defaultConfig.versionName属性,需要检查一下版本是不是x.x.x形式,如:项目版本为 1.0.0(默认是1.0,但是codepush需要三位数)
2:versionName必须严格x.x.x格式。如:1.1.1 beta 也是不行的,codepush包推不上去
初始化codepush客户端需要注意的问题
1:网络请求接口一般为两种类型,建立在传输层面的socket,基于socket上的一种应用层实现的web。。rn端热更新请求服务器,是cocket接口。所以,请求端口要注意的。。。一般资料都没有提到这点。。也可以参考git上code-push-server相关配置,也提到CodePush四个参数的配置
http://your server ip:3000/
initcodepush.png3.2.2. 客户端rn模块开发
需要注意的细节
sync()自动检测更新,deployment key 默认是根据版本构建类型对应额key。这个在入参时候可以不指定,如果你有特殊需求,比如:某用户是输入灰度范围内的,就可以用staging对应的key去更新测试包。
checkForUpdate()手动检测就需要指定deployment key了。如果不入参,则无法从server获取到版本信息,会提示:undefind deployment key。检测到新版本后,也是调用sync()更新,这个里面最好也要配置上deployment key,因为checkForUpdate()检测的是指定key的包版本信息,sync()更新也要指定一下更新环境的key。
HotLoad.js文件
import React, { Component } from 'react';
import {
Platform,
StyleSheet,
Text,
View,
Button,
NativeModules,
TouchableWithoutFeedback,
TouchableOpacity,
Image
} from 'react-native';
import CodePush from "react-native-code-push";
let CustomNativeModule = NativeModules.RnToNativeBridgeModule;
let codePushOptions = {
//设置检查更新的频率
//ON_APP_RESUME APP恢复到前台的时候
//ON_APP_START APP开启的时候
//MANUAL 手动检查
checkFrequency : CodePush.CheckFrequency.MANUAL
};
class HotLoad extends React.Component {
static navigationOptions = {
title:'热加载页面',
};
constructor(props) {
super(props);
this.navigation = props.navigation;
}
syncImmediate() {
CodePush.sync(
{
//安装模式
//ON_NEXT_RESUME 下次恢复到前台时
//ON_NEXT_RESTART 下一次重启时
//IMMEDIATE 马上更新
installMode : CodePush.InstallMode.IMMEDIATE ,
// deploymentKey: 'ItJpHCSJi8MtQsqsLqjZVxh0Pw7Z4ksvOXqog',
//对话框
updateDialog : {
//是否显示更新描述
appendReleaseDescription : true ,
//更新描述的前缀。 默认为"Description"
descriptionPrefix : "更新内容:" ,
//强制更新按钮文字,默认为continue
mandatoryContinueButtonLabel : "立即更新" ,
//强制更新时的信息. 默认为"An update is available that must be installed."
mandatoryUpdateMessage : "必须更新后才能使用" ,
//非强制更新时,按钮文字,默认为"ignore"
optionalIgnoreButtonLabel : '稍后' ,
//非强制更新时,确认按钮文字. 默认为"Install"
optionalInstallButtonLabel : '后台更新' ,
//非强制更新时,检查到更新的消息文本
optionalUpdateMessage : '有新版本了,是否更新?' ,
//Alert窗口的标题
title : '更新提示',
// optionalIgnoreButtonLabel: '稍后',
// optionalInstallButtonLabel: '立即更新',
// optionalUpdateMessage: '有新版本了,是否更新?',
// title: '更新提示'
} ,
// updateDialog: false,
} ,
);
}
async checkUpdate() {
let deploymentKey = "";
try {
deploymentKey = await CustomNativeModule.rnGetCodePushKeyWithPromise();
} catch (error) {
alert("同步获取key失败" + error);
}
deploymentKey = "ItJpHCSJi8MtQsqsLqjZVxh0Pw7Z4ksvOXqog";
// alert("同步获取到的key" + deploymentKey);
CodePush.checkForUpdate(deploymentKey)
.then((update) => {
if (!update) {
alert("提示", "已是最新版本--", [
{
text: "Ok", onPress: () => {
alert("点了OK");
}
}
]);
} else {
CodePush.sync({
deploymentKey: deploymentKey,
// updateDialog: false,
installMode: CodePush.InstallMode.IMMEDIATE,
},
(status) => {
switch (status) {
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
alert("DOWNLOADING_PACKAGE");
break;
case CodePush.SyncStatus.INSTALLING_UPDATE:
alert(" INSTALLING_UPDATE");
break;
}
},
(progress) => {
alert(progress.receivedBytes + " of " + progress.totalBytes + " received.");
});
}
}).catch((error) => {
alert(deploymentKey + error);
});
}
componentWillMount() {
CodePush.disallowRestart();//禁止重启
// this.syncImmediate(); //开始检查更新
}
componentDidMount() {
CodePush.allowRestart();//在加载完了,允许重启
}
render() {
return (
<View>
<Button
title="buttonCheckUpdate"
onPress={() => this.checkUpdate()}
/>
<Image
source={require('./image/9.jpg')}
/>
</View>
);
}
}
HotLoad = CodePush(codePushOptions)(HotLoad)
export default HotLoad;
3.2.3. 热更新操作
NOTE: CodePush updates should be tested in modes other than Debug mode. In Debug mode, React Native app always downloads JS bundle generated by packager, so JS bundle downloaded by CodePush does not apply.
注意调试热更新的时候,不要用debug模式,因为debug模式走的是package server的包,不会走codepush的jsbundle包。
debug模式调试,具体有没有方案能走通,没有深入测试,,最好用releaseStaging吧,调试完后,直接prompt拷贝到production正式环境中
打包
react-native bundle --entry-file index.js --bundle-output ./bundle/android/index.android.bundle --assets-dest ./bundle/android/ --platform android --dev false
发布
code-push release-react xxt_react_native-android android -o ./bundle/android/ --t 7.3.2 --d Production --dev false --des "这是更新图片资源包" --m false
回滚
code-push rollback xxt_react_native-android Staging --targetRelease v31
查看各个更新版本的更新进度
code-push deployment h xxt_react_native-android Staging
命令详解
$ code-push release-react
Usage: code-push release-react <appName> <platform> [options]
选项:
--bundleName, -b Name of the generated JS bundle file. If unspecified, the standard bundle name will be used, depending on the specified platform: "main.jsbundle" (iOS), "index.android.bundle" (Android) or "index.windows.bundle" (Windows) [字符串] [默认值: null]
--deploymentName, -d Deployment to release the update to [字符串] [默认值: "Staging"]
--description, --des Description of the changes made to the app with this release [字符串] [默认值: null]
--development, --dev Specifies whether to generate a dev or release build [布尔] [默认值: false]
--disabled, -x Specifies whether this release should be immediately downloadable [布尔] [默认值: false]
--entryFile, -e Path to the app's entry Javascript file. If omitted, "index.<platform>.js" and then "index.js" will be used (if they exist) [字符串] [默认值: null]
--gradleFile, -g Path to the gradle file which specifies the binary version you want to target this release at (android only). [默认值: null]
--mandatory, -m Specifies whether this release should be considered mandatory [布尔] [默认值: false]
--noDuplicateReleaseError When this flag is set, releasing a package that is identical to the latest release will produce a warning instead of an error [布尔] [默认值: false]
--plistFile, -p Path to the plist file which specifies the binary version you want to target this release at (iOS only). [默认值: null]
--plistFilePrefix, --pre Prefix to append to the file name when attempting to find your app's Info.plist file (iOS only). [默认值: null]
--rollout, -r Percentage of users this release should be immediately available to [字符串] [默认值: "100%"]
--privateKeyPath, -k Specifies the location of a RSA private key to sign the release with [字符串] [默认值: false]
--sourcemapOutput, -s Path to where the sourcemap for the resulting bundle should be written. If omitted, a sourcemap will not be generated. [字符串] [默认值: null]
--targetBinaryVersion, -t Semver expression that specifies the binary app version(s) this release is targeting (e.g. 1.1.0, ~1.2.3). If omitted, the release will target the exact version specified in the "Info.plist" (iOS), "build.gradle" (Android) or "Package.appxmanifest" (Windows) files. [字符串] [默认值: null]
--outputDir, -o Path to where the bundle and sourcemap should be written. If omitted, a bundle and sourcemap will not be written. [字符串] [默认值: null]
--config, -c Path to the React Native CLI configuration file [字符串] [默认值: null]
-v, --version 显示版本号 [布尔]
示例:
release-react MyApp ios Releases the React Native iOS project in the current working directory to the "MyApp" app's "Staging" deployment
release-react MyApp android -d Production -k ~/.ssh/codepush_rsa Releases the React Native Android project in the current working directory to the "MyApp" app's "Production" deployment, signed with the "codepush_rsa" private key
release-react MyApp windows --dev Releases the development bundle of the React Native Windows project in the current working directory to the "MyApp" app's "Staging" deployment
bundle要求的app最小版本
如果bundle依赖了一个native的一个新的接口,这个接口在v3版本的app中才发布,如果v2版本的app升级了这个bundle,那么必然会报错,严重的可能会导致app的崩溃。
3.2.4. staging 》 production
测试到正式的升级迁移,一般流程如下:
1:Release a CodePush update to your Staging deployment using the code-push release-react command (or code-push release if you need more control)
2:Run your staging/beta build of your app, sync the update from the server, and verify it works as expected
3:Promote the tested release from Staging to Production using the code-push promote command
4:Run your production/release build of your app, sync the update from the server and verify it works as expected
1.使用code-push release-react 命令去发布一个CodePush更新到你的Staging 部署环境下。
2.运行 你的stagging或测试版本应用,同步服务器的更新,验证其是否正常工作
3.使用code-push promote命令 将测试版本 从Stagging 升级到Production
4.运行 你的production或发布版本应用,同步服务器的更新,验证其是否正常工作
应用创建时有两个环境,一个是Staging,一个是Production,开发阶段用Staging,开发完成可以用code-push promote 将应用迁移升级到Production中。
注意:更给力的是,你可以选择“分阶段推送”作为第3步的一部分,以减轻额外的风险,因此,你可以先向一定百分比的用户推送更新,(例如:code-push promote <APP_NAME> Staging Production -r 20%)然后查看在一定时间内是否发生崩溃,和客户反馈,再根据情况去进行整体推送code-push patch <APP_NAME> Production -r 100%
codepush提供的灰度百分比发布,并不那么具体。如果是要灰度指定的组织或用户怎么办?这个还需要结合自己的服务器实现。比如通过userid,groupid去匹配是否是灰度内用户。那么,codepush提供的灰度就不再那么实用了。
场景
1:可以对一小部分用户进行升级测试:
code-push promote 应用名 Staging Production -r 20%
2:发布更新之后,比如上面,我们发布更新到了1.20.1的测试版本,更新百分之20的用户,可是我们发布完成之后,想更新百分之30的用户:
code-push patch wanna 1.20.1 --rollout 20
当然还可以改变其他参数啦 patch命令就是更改参数的
3:软件稳定后,可以全面发布:
code-push patch 应用名 Production -r 100%
4:回滚
code-push rollback <appName> <deploymentName>
code-push rollback MyApp Production
5:查看app各个版本包的更新情况
code-push deployment history <appName> <deploymentName>
Active:表示安装你发布更新的,并当前活跃的,Total就是一共多少人安装了这一版,Rollbacks
是安装过程中遇到问题的数量,也就是安装失败的数量
4. 实践mac平台搭建服务器实现rn+ios热更新
后续跟进~
网友评论