1. 首先声明下环境:因为环境而造成的坑太多了,我都忍不住吐槽N遍了,所以在开始整活之前先搞清楚我们现在用的环境。
2. 首先查看下所使用的 ndoejs 版本 nodejs 16.10.0
E:\ReactLove\rn_hello\rnHello_CodePush_060>node -v
v16.10.0
E:\ReactLove\rn_hello\rnHello_CodePush_060>nvm list
* 16.10.0 (Currently using 64-bit executable)
12.18.0
12.2.0
注意:这里使用的 nvm 是为了方便管理和使用不同的 nodejs,因为不同版本的ReactNative可能会需要不同版本的 nodejs 环境, 否则在构建项目的时候,会发生各种令人吐槽的事情。
3. 查看下 ReactNative 版本
E:\ReactLove\rn_hello\rnHello_CodePush_060>react-native -v
react-native-cli: 2.0.1
react-native: 0.60.0
这里我们可以看到,我们已经安装了 react-native-cli 命令行,然后我们开始准备使用该命令行创建我们新的ReactNative项目,版本是0.60.0。
4. 创建 ReactNative 项目
E:\ReactLove\rn_hello>npx react-native init rnCodePushDemo_060 --version 0.60.0
5. 创建好项目之后,需要修改三个地方,然后才能正常使用,否则直接执行命令 react-native run-android 会报错的,这是经过实战经验所得。
5.1 修改项目根目录下的 babel.config. js 文件
module.exports = {
presets: [[
'module:metro-react-native-babel-preset',{
unstable_disableES6Transforms: true
}
]],
};
5.2 修改项目根目录下的 package.json 文件
{
"name": "rnCodePushDemo_060",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "react-native start",
"test": "jest",
"lint": "eslint ."
},
"dependencies": {
"react": "16.8.6",
"react-native": "0.60.0"
},
"devDependencies": {
"@babel/core": "7.19.3",
"@babel/runtime": "7.19.4",
"@react-native-community/eslint-config": "0.0.3",
"babel-jest": "24.9.0",
"eslint": "5.16.0",
"jest": "24.9.0",
"metro-react-native-babel-preset": "0.59.0",
"react-test-renderer": "16.8.6"
},
"resolutions": {
"@babel/traverse": "7.16.7"
},
"jest": {
"preset": "react-native"
}
}
"devDependencies": {
....
"metro-react-native-babel-preset": "0.59.0",
},
"resolutions": {
"@babel/traverse": "7.16.7"
},
如上红色标注的部分,就是在 package.json 文件中修改的内容。
5.3 修改项目 \node_modules\metro-config\src\defaults 目录下的 blacklist.js 文件中的内容
var sharedBlacklist = [
/node_modules[\/\\]react[\/\\]dist[\/\\].*/,
/website\/node_modules\/.*/,
/heapCapture\/bundle\.js/,
/.*\/__tests__\/.*/
];
黄色部分就是要修改的部分内容,否则在项目直接进行编译和启动的时候,会存现nodejs 服务启动之后就一闪而过,就结束 nodejs 服务了,自然也没法进行项目开发了。
6. 在修改以上三个地方的内容之后,就可以尝试编译和启动React Native项目了
E:\ReactLove\rn_hello\rnCodePushDemo_060>react-native run-android
最终就可以顺利启动项目了, 展示如下:
终端:
nodejs 服务端:
7. 在顺利编译并启动项目之后,我们就开始准备热更新的部分。
热更新其实就是将ReactNative项目中的js打包成一个bundle文件,然后放到服务器上,在应用需要热更新的时候,从服务器上下载这个新的bundle文件,然后将这个bundle文件载入ReactNative框架中,然后进行更新,这样就无须下载新的 Android apk 包,实现业务的更新或者Bug的修改,这也是React Native 被好多大厂所看好的原因,目前除了大厂自研的Android原生热更新框架,下面就是这种类型的热更新是最好的了,其他要不是没有人用,要不就是大厂自己搞着自己玩的,也不开源,没啥意思,能让大家用的也就是React Native。Flutter 除了几家大厂推出来的框架,也不成气候,没啥大意思,估计过几年也就没有前途了,只有用的人多才是有前途的跨平台框架和热更新框架。CodePush 是 微软针对 React Native 推出的热更新插件,可以将新的Bundle文件推送到CodePush 平台上,然后供大家进行下载、加载进行热更新。
8. 在了解了什么是热更新之后,我们正式开始热更新插件的接入:
8.1 首先安装 code-push-cli 命令行工具,如下是全局安装
E:\ReactLove\rn_hello\rnHello_060>npm install -g code-push-cli
8.2 注册 CodePush
E:\ReactLove\rn_hello\rnHello_060>code-push register
这里会弹出一个网站,然后我这里选择使用Github进行登录,然后授权之后,就会弹出包含如下内容的弹框:
Authentication succeeded
Please copy and paste this token to the command window:
558aa5b330e8262fe292a58c4366847a2ff3a3b7
After doing so, please close this browser tab.
将这串字符粘贴到刚才的命令行窗口中,你会得到如下内容:
Enter your token from the browser: 558aa5b330e8262fe292a58c4366847a2ff3a3b7
Successfully logged-in. Your session file was written to C:\Users\PCWin10\AppData\Local\.code-push.config.
You can run the code-push logout command at any time to delete this file and terminate your session.
8.3 注册成功后,应该不会再使用这个 code-push register 命令,除非你想推出重新登录。
8.4 向CodePush服务器注册新的 App
//AppName 就是你要创建的应用名称
//platform 可以使 ios 或者 Android
code-push app add <AppName> <platform> react-native
向CodePush服务器注册一个新的App,名称为 CodePushDemo,命令如下:
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push app add CodePushDemo Android react-native
命令执行完成之后,会看到如下结果:
│ Name │ Deployment Key │
├────────────┼───────────────────────────────────────┤
│ Production │ mcNDEqhhKH2fvm_NNVSzwC1JVPgpaSLFU9WAj │
├────────────┼───────────────────────────────────────┤
│ Staging │ 4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_ │
上面是一套部署的Key, 分为 Staging 开发版本和 Production 版本,我们这里使用的是开发版本。
注册的App 应用情况可以通过 https://appcenter.ms/apps 来查看,如下:
其中,CodePushDemo 就是我们刚刚创建的项目。
8.5 集成 CodePush SDK ,添加 code-push 包
E:\ReactLove\rn_hello\rnCodePushDemo_060>npm install --save react-native-code-push
8.6 如果8.5 没有提示说输入 Deployment Key,那么我们需要手动配置,如果忘记 Deployment Key,可以通过如下命令来查看:
code-push deployment ls CodePushDemo -k
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push deployment ls CodePushDemo -k
(node:8212) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
┌────────────┬───────────────────────────────────────┬─────────────────────┬──────────────────────┐
│ Name │ Deployment Key │ Update Metadata │ Install Metrics │
├────────────┼───────────────────────────────────────┼─────────────────────┼──────────────────────┤
│ Production │ mcNDEqhhKH2fvm_NNVSzwC1JVPgpaSLFU9WAj │ No updates released │ No installs recorded │
├────────────┼───────────────────────────────────────┼─────────────────────┼──────────────────────┤
│ Staging │ 4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_ │ No updates released │ No installs recorded │
└────────────┴───────────────────────────────────────┴─────────────────────┴──────────────────────┘
8.7 手动配置,首先用AndroidStudio 打开 android 项目,然后再 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')
8.8 在 app/build.gradle 文件中添加如下内容:
apply from: '../../node_modules/react-native-code-push/android/codepush.gradle'
//下面这行如果有,则不加
apply from: "../../node_modules/react-native/react.gradle"
8.9 在 MainApplication.java 文件中,添加如下内容:
private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
@Override
public boolean getUseDeveloperSupport() {
return BuildConfig.DEBUG;
}
@Override
protected List<ReactPackage> getPackages() {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
return packages;
}
@Override
protected String getJSMainModuleName() {
return "index";
}
@Nullable
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
};
@Nullable
@Override
protected String getJSBundleFile() {
return CodePush.getJSBundleFile();
}
红色标记部分即为要添加的内容,注意引入 import CodePush 类。
8.10 在 res/values/strings.xml 中添加 CodePushDeploymentKey,这里内容填写的就是前面获取到的 StagingKey。
<resources>
...
<string name="CodePushDeploymentKey">4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_</string>
</resources>
8.11 在 app/build.gradle 文件中,添加如下内容:
android {
....
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://facebook.github.io/react-native/docs/signed-apk-android.
//填入 PRODUCTION_KEY
buildConfigField "String", "CODEPUSH_KEY", '"mcNDEqhhKH2fvm_NNVSzwC1JVPgpaSLFU9WAj"'
signingConfig signingConfigs.debug
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
releaseStaging {
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
//填入 STAGING_KEY
buildConfigField "String", "CODEPUSH_KEY", '"4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_"'
}
}
...
}
8.12 如上,就已经完成了原生部分的代码,下面开始JavaScript 部分的配置:
8.13 在ReactNative项目中的 App.js 文件中进行如下修改:
import React from 'react';
import {
View,
Text,
} from 'react-native';
import CodePush from 'react-native-code-push';
const codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
updateDialog: true, //发现可更新时是否显示对话框
installMode: CodePush.InstallMode.IMMEDIATE,
};
class App extends React.Component {
render() {
return (
<View>
<Text>版本号1.0</Text>
<Text>我是1.0版本,希望大家能够喜欢</Text>
</View>
);
}
update = () => {
CodePush.sync({
// 安装模式
// ON_NEXT_RESUME 下次恢复到前台时
// ON_NEXT_RESTART 下一次重启时
// IMMEDIATE 马上更新
installMode: CodePush.InstallMode.IMMEDIATE,
// 对话框 如果打包和上传没有设置强制更新的话,那么默认就是非强制更新,显示的都是非强制更新配置的内容
updateDialog: {
// 是否显示更新描述
appendReleaseDescription: true,
// 更新描述的前缀。 默认为"Description"
descriptionPrefix: '更新内容:',
// 强制更新按钮文字,默认为continue
mandatoryContinueButtonLabel: '立即更新',
// 强制更新时的信息. 默认为"An update is available that must be installed."
mandatoryUpdateMessage: '必须更新后才能使用',
// 非强制更新时,按钮文字,默认为"ignore"
optionalIgnoreButtonLabel: '稍后',
// 非强制更新时,确认按钮文字. 默认为"Install"
optionalInstallButtonLabel: '后台更新',
// 非强制更新时,检查到更新的消息文本
optionalUpdateMessage: '有新版本了,是否更新?',
// Alert窗口的标题
title: '更新提示',
},
}).then(r => {
});
};
async componentWillMount() {
CodePush.disallowRestart(); //禁止重启
this.update();//开始检查更新
}
componentDidMount() {
CodePush.allowRestart();//加载完了,允许重启
}
}
App = CodePush(codePushOptions)(App);
export default App;
在 index.js 文件中进行如下修改:
import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
//添加如下关闭黄色警告
console.disableYellowBox = true; //关闭全部黄色警告
console.ignoredYellowBox = ['Warning: BackAndroid is deprecated. Please use BackHandler instead.'];
AppRegistry.registerComponent(appName, () => App);
//添加如下关闭黄色警告
console.disableYellowBox = true; //关闭全部黄色警告
console.ignoredYellowBox = ['Warning: BackAndroid is deprecated. Please use BackHandler instead.'];
8.14 执行 react-native run-android 命令,编译并启动 项目:
E:\ReactLove\rn_hello\rnCodePushDemo_060>react-native run-android
结果如下,即没有进行新的内容修改之前的展示内容:
8.15 对页面的内容进行修改
<View>
<Text>版本号1.1</Text>
<Text>我是1.1版本,此版本新增加了许多新的功能,欢迎体验!</Text>
</View>
8.16 打包并上传 bundle 文件,命令如下,CodePushDemo 就是前面设置的App项目名称, android 为平台类型。
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android
(node:7820) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
Detecting android app version:
Using the target binary version value "1.0" from "android\app\build.gradle".
Running "react-native bundle" command:
node node_modules\react-native\local-cli\cli.js bundle --assets-dest C:\Users\PCWin10\AppData\Local\Temp\CodePush\CodePush --bundle-output C:\Users\PCWin10\AppData\Local\Temp\CodePush\CodePush\index.android.bundle --dev false --entry-file index.js --platform android
Loading dependency graph, done.
info Writing bundle output to:, C:\Users\PCWin10\AppData\Local\Temp\CodePush\CodePush\index.android.bundle
info Done writing bundle output
info Copying 2 asset files
info Done copying assets
Releasing update contents to CodePush:
Upload progress:[==================================================] 100% 0.0s
Successfully released an update containing the "C:\Users\PCWin10\AppData\Local\Temp\CodePush\CodePush" directory to the "Staging" deployment of the "CodePushDemo" app.
E:\ReactLove\rn_hello\rnCodePushDemo_060>
8.17 在上传完成之后,可以查看 CodePush 平台上的发布历史记录,刚才发布的也在其中,命令如下:CodePushDemo 是App 项目的名称
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push deployment history CodePushDemo Staging
(node:14968) Warning: Accessing non-existent property 'padLevels' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
┌───────┬───────────────┬─────────────┬───────────┬─────────────┬──────────────────────┐
│ Label │ Release Time │ App Version │ Mandatory │ Description │ Install Metrics │
├───────┼───────────────┼─────────────┼───────────┼─────────────┼──────────────────────┤
│ v1 │ 8 minutes ago │ 1.0 │ No │ │ No installs recorded │
└───────┴───────────────┴─────────────┴───────────┴─────────────┴──────────────────────┘
E:\ReactLove\rn_hello\rnCodePushDemo_060>
8.18 在发布到平台完成之后,我们可以开始测试,首先将当前正在运行的该项目App杀掉,然后进行重启,会发现弹出如下弹框:
8.19 我们选择 后台更新,然后过几秒之后,页面就会重新刷新,变成如下内容:
8.20 杀掉App进程,重新启动, 会发现此次进入是没有弹出新的版本提示,说明当前已经是最新的版本。
9. 如何正式部署 Release 版本
因为执行以下命令默认是 Staging 版本的,即推送到 CodePush 平台之后,是在Staging环境中的,而不是 Production 环境的。
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android
但是其实跟发布到哪种环境没有关系,其实都是可以的,我们先考虑 Staging 环境。比如我们先前已经打包好一个 Release 包,给用户安装到手机上了,那么请问,此时客户手机上的 deployment key 是什么类型的呢?如果没有改动,依据前面的是配置,我们在打release 包的时候,会发送报错,修改方式如下:
buildTypes { debug { signingConfig signingConfigs.debug } release { // Caution! In production, you need to generate your own keystore file. // see https://facebook.github.io/react-native/docs/signed-apk-android. //PRODUCTION_KEY buildConfigField "String", "CODEPUSH_KEY", '"mcNDEqhhKH2fvm_NNVSzwC1JVPgpaSLFU9WAj"' signingConfig signingConfigs.release minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } releaseStaging { minifyEnabled enableProguardInReleaseBuilds signingConfig signingConfigs.release proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" //STAGING_KEY buildConfigField "String", "CODEPUSH_KEY", '"4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_"' matchingFallbacks = ['release', 'debug'] } }
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
release {
storeFile file(STORE_FILE)
storePassword STORE_FILE_PASSWORD
keyAlias KEY_ALIAS
keyPassword KEY_ALIAS_PASSWORD
}
}
// android/gradle.properties
STORE_FILE=./keystore/CodePushDemo.keystore
KEY_ALIAS=shudan-alias
STORE_FILE_PASSWORD=123456
KEY_ALIAS_PASSWORD=123456
此时,如果没有修改 res/values/strings.xml 中的内容,那么依旧是 staging key 。
打包 release 包,命令如下:
E:\ReactLove\rn_hello\rnCodePushDemo_060\android>gradlew assembleRelease
然后安装到用户手机上,之后,修改 App.js 内容,再次将 bundle 文件推送到 CodePush 平台,执行的命令如下:
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android
因为默认情况下是推送到 Staging 环境中的,因此此时 CodePush 最新的版本是在Staging环境中的,而用户手机上此时App的也是 Staging key,那么CodePush 服务器和 App 上的key 就能对的上,那么就能收到最新版本的提示,就能热更新。
如此说来,前面在 app/build.gradle 文件中定义的 buildConfigField "String", "CODEPUSH_KEY" 好像是没有用到,全靠的是 res/values/strings.xml 中定义的
<string name="CodePushDeploymentKey">4O616rtg7BGZTaH_ynL0QIZynLjIzQvd0WNn_</string>
这个内容决定了手机App上使用的是那种类型的key, 如果这里采用了Production Key,那么在打包资源和推送到 CodePush 服务器的时候,命令行需要改成如下:
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android -d Production
-d : 表示指定上传的环境,Production 是生产环境,Staging 是开发环境。
结论:关于如何部署最新的版本的问题就看用户手机此时App中的是那种 Key, 此种 key 是配置在
app/res/values/strings.xml 中的 CodePushDeploymentKey 中。
1. 如果是 Staging Key, 那么打包推送的命令是:
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android -d Staging
2. 如果是 Production Key, 那么打包推送的命令是:
E:\ReactLove\rn_hello\rnCodePushDemo_060>code-push release-react CodePushDemo android -d Production
10. 自己部署CodePush服务器
如果不想用微软提供的CodePush服务器来做热更新,那么可以考虑自己搭建环境,具体资料将来自己搞的时候再去查吧,暂时到此为止,知道如何将新的 bundle 文件更新到CodePush 服务器上即可。
11. 最后一个问题
既然是更新的javascript 的内容,那么如果新的内容如果是加载本地的图片资源,那么是否会一起打包到 bundle 文件中呢?
答案是可以的,可以将图片资源一块进行打包到bundle文件中,然后进行热更新。测试代码如下:
import React from 'react';
import {
View,
Text,
Image,
StyleSheet,
Dimensions
} from 'react-native';
import CodePush from 'react-native-code-push';
const codePushOptions = {
checkFrequency: CodePush.CheckFrequency.ON_APP_RESUME,
updateDialog: true, //发现可更新时是否显示对话框
installMode: CodePush.InstallMode.IMMEDIATE,
};
class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Image style={styles.image} resizeMode={"contain"} source={require("./images/laptop_phone_howitworks.png")}/>
<Image style={{width: 50, height: 100}} resizeMode={"contain"} source={require("./images/laptop_phone_howitworks.png")}/>
<Image style={styles.image} resizeMode={"contain"} source={{uri:"https://sample-videos.com/img/Sample-jpg-image-50kb.jpg"}}/>
<Text>版本号1.4</Text>
<Text>我是1.4版本,测试本地Staging 是否能够匹配服务器上的Production</Text>
</View>
);
}
update = () => {
CodePush.sync({
// 安装模式
// ON_NEXT_RESUME 下次恢复到前台时
// ON_NEXT_RESTART 下一次重启时
// IMMEDIATE 马上更新
installMode: CodePush.InstallMode.IMMEDIATE,
// 对话框 如果打包和上传没有设置强制更新的话,那么默认就是非强制更新,显示的都是非强制更新配置的内容
updateDialog: {
// 是否显示更新描述
appendReleaseDescription: true,
// 更新描述的前缀。 默认为"Description"
descriptionPrefix: '更新内容:',
// 强制更新按钮文字,默认为continue
mandatoryContinueButtonLabel: '立即更新',
// 强制更新时的信息. 默认为"An update is available that must be installed."
mandatoryUpdateMessage: '必须更新后才能使用',
// 非强制更新时,按钮文字,默认为"ignore"
optionalIgnoreButtonLabel: '稍后',
// 非强制更新时,确认按钮文字. 默认为"Install"
optionalInstallButtonLabel: '后台更新',
// 非强制更新时,检查到更新的消息文本
optionalUpdateMessage: '有新版本了,是否更新?',
// Alert窗口的标题
title: '更新提示',
},
}).then(r => {
});
};
async componentWillMount() {
CodePush.disallowRestart(); //禁止重启
this.update();//开始检查更新
}
componentDidMount() {
CodePush.allowRestart();//加载完了,允许重启
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
backgroundColor: "#F5FCFF",
paddingTop: 50
},
image: {
margin: 30,
width: Dimensions.get("window").width - 100,
//height: 365 * (Dimensions.get("window").width - 100) / 651,
height:100
}
});
App = CodePush(codePushOptions)(App);
export default App;
注意,在第一次学习的时候,发现图片竟然加载不进来,就是怎么也不显示,最后发现是自己写法有问题:
<Image style={styles.image} resizeMode={"contain"} soure={require("./images/laptop_phone_howitworks.png")} />
<Image soure={{uri: "https://sample-videos.com/img/Sample-jpg-image-50kb.jpg"}} style={styles.image} resizeMode={"contain"}/>
发现没有,是source 写成了 soure,结果自然就不会显示,但是奇葩是尽然不报错,太坑爹,抱着这个问题查了两天。
参考: