作为一款以JavaScript语音为基础跨平台开发框架,React Native本身已经具备了动态更新的能力,不过官方却没有提供一套标准的动态更新方案。因为一个标准的动态更新方案,除了需要客户端具备动态更新的能力外,还需要服务器端支持资源包的管理和下发。 虽然官方没有提供标准的热更新方案,但是React Native社区却提供了搭建热更新的私服方案,比如React Native中文网的pushy和微软的CodePush。相比于pushy,我们更推荐使用CodePush来搭建热更新私服。
CodePush是微软提供的一项可直接用于React Native和Cordova应用热更新的云服务。作为一个管理资源的中央仓库,CodePush具备实时的推送更新能力,当开发人员在CodePush后台系统中发布某些更新时,集成了CodePush的客户端在启动后就会执行热更新查询。这样一来,不需要重新执行打包、审核、发布即可轻松的解决线上版本的缺陷。
除此之外,CodePush还具有如下特性:
- 支持对用户部署代码的直接更新;
- 能够管理Alpha、Beta和生产等多套环境;
- 支持React Native和Cordova等跨平台框架;
- 支持JavaScript代码文件与图片资源的更新;
为了快速集成CodePush热更新,本文使用的是CodePush中文社区提供的cpcn-client桌面工具。
一、注册新用户
首先,进入CodePush中文网的控制台,如果此时你不是处于登入状态,则会见到一个“登入对话框”,点击该“对话框”右上角的注册,将会打开“注册对话框”,填写相关的信息注册即可。
二、安装cpcn-client桌面工具
cpcn-client是一个为CodePush设计的桌面工具。傻瓜化的操作,让开发者和运维人员只需轻点几下鼠标就能完成相关的操作,不仅简化了操作,还能大大提高工作效率。目前,支持Windows和Mac两个操作系统。
安装完成之后,启动即可,如果遇到无法验证开发者的错误,可以使用下面的方式继续运行。
三、创建应用
进入控制台,点击“创建应用”,为你的应用设置一个名字:
如果你的应用即有android版,也有ios版,则应分别创建两个应用。为了便于区分,建议在你的应用的名字的后面标明,例如:如果你的应用的名字是myapp,则android版的名字建议设为myapp-android,ios版的名字建议设为myapp-ios。在设置好对应的“应用类型”与“平台”后,点击“确定”以创建应用。
四、手动集成
4.1 Android集成
首先,在项目的根目录下执行以下命令安装 cpcn-react-native,如下所示。
npm install cpcn-react-native --save
然后,打开 /android/app/build.gradle 文件,添加以下代码。
apply from: "../../node_modules/cpcn-react-native/android/codepush.gradle"
接着,修改 /android/app/src/main/res/values/strings.xml 文件,在根节点<resources>内加入以下节点。
<string moduleConfig="true" name="reactNativeCodePush_androidDeploymentKey">YOUR_DEPLOYMENT_KEY</string>
需要说明是,示例中的YOUR_DEPLOYMENT_KEY替换为你的应用的deployent key。可在控制台中点击你的应用的名字,在打开的面板中找到你的应用的deployment key。
接着,修改 /android/app/src/main/java/com/APP_NAME/MainApplication.java 文件, 在getUseDeveloperSupport()上面重写一下getJSBundleFile(),如下所示。
@Override
protected String getJSBundleFile(){
return CodePush.getJSBundleFile();
}
4.2 iOS集成
首先,找到/ios/Podfile,然后添加如下内容。
pod 'CodePush', :path => '../node_modules/cpcn-react-native'
接着,在项目的根目录下的ios文件夹下执行pod install命令安装插件。然后,打开/ios/APP_NAME/AppDelegate.m文件,然后修改sourceURLForBridge(),如下所示。
#import <CodePush/CodePush.h>
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
#else
return [CodePush bundleURL];
#endif
}
然后,打开/ios/APP_NAME/Info.plist文件,在<dict>节点下加入以下节点。
<key>CodePushDeploymentKey</key>
<string>YOUR_DEPLOYMENT_KEY</string>
需要说明的是,将以上示例中的YOUR_DEPLOYMENT_KEY替换为你的应用的deployent key。可在控制台中点击你的应用的名字,在打开的面板中找到你的应用的deployment key。
五、示例
5.1 创建React Native项目
首先,使用如下的命令创建React Native项目。
react-native init myapp
//或者
npx react-native init myapp
5.2 在CodePush(中国)创建对应的应用
为了使用CodePush(中国)提供的服务,需在CodePush(中国)上创建对应的应用。进入CodePush(中国)的控制台,并登入。如果还没有帐户,则注册一个新帐户。
接着,点击页面上的创建应用,在弹出的面板中填写相应的信息,以创建应用。应用的名称可任意填写,只要不与其它应用重复就行了,并且需要区分是Android还是iOS。因为这个示例即有Android版,也有iOS版,所以需要在CodePush(中国)的控制台中为Android版和iOS版各创建一个应用。
5.3 安装cpcn-react-native插件
如果你的电脑上还没有安装cpcn-client,则点击这里下载并安装它。打开电脑上安装的cpcn-client,登入后,将能看到刚刚在控制台中创建的两个应用:myapp-android和myapp-ios。
点击myapp-android应用的名字,将会打开一个面板。在打开的面板中,设置项目文件夹,即E:\test\myapp。点击【install cpcn-react-native & link】按钮,等待执行完毕。
由于iOS版需在Mac电脑上运行和测试,因此需在Mac电脑上为myapp-ios重复以上的操作。
等待执行完毕后,cpcn-react-native就安装并配置成功了。
5.4 修改代码
使用VS Code打开/App.js中的代码,修改为下面这个样子:
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/
import React from 'react';
import type {Node} from 'react';
import {
Button,
Modal,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from 'react-native';
import codePush from 'cpcn-react-native/CodePush';
import {
Colors,
DebugInstructions,
Header,
LearnMoreLinks,
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
const Section = ({children, title}): Node => {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
</Text>
<Text
style={[
styles.sectionDescription,
{
color: isDarkMode ? Colors.light : Colors.dark,
},
]}>
{children}
</Text>
</View>
);
};
const App: () => Node = () => {
const isDarkMode = useColorScheme() === 'dark';
const [upgradeState, setUpgradeState] = React.useState(0);
const [upgradeReceived, setUpgradeReceived] = React.useState(0);
const [upgradeAllBytes, setUpgradeAllBytes] = React.useState(0);
const backgroundStyle = {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
};
React.useEffect(() => {
checkUpdate();
}, []);
function checkUpdate() {
codePush.check({
//检查是否有新版本后调用此方法
checkCallback: (remotePackage, agreeContinueFun) => {
console.log('checkCallback', remotePackage.toString());
if (remotePackage) {
//如果remotePackage 有值,表示有新版本可更新。
setUpgradeState(1);
}
},
//下载新版本时调用此方法
downloadProgressCallback: dp => {
console.log('downloadProgressCallback>>>');
// 更新显示的下载进度中的数值
setUpgradeReceived(dp.receivedBytes); //已下载的字节数
setUpgradeAllBytes(dp.totalBytes); //总共需下载的字节数
},
//安装新版本后调用此方法
installedCallback: restartFun => {
console.log('installedCallback>>>');
//新版本安装成功,关闭对话框
setUpgradeState(0);
restartFun(true);
},
});
}
function upgradeContinue() {
codePush.agreeContinue(true);
//将upgradeState的值设为2,以显示下载进度
setUpgradeState(2);
}
function renderUpdateModal() {
return (
<Modal visible={upgradeState > 0} transparent={true}>
<View style={styles.modal}>
<View style={styles.content}>
{upgradeState == 1 && (
<View>
<Text style={styles.tip}>发现新版本</Text>
<Text style={styles.des}>检测到新版本,点击按钮执行更新!</Text>
<Button title="马上更新" onPress={upgradeContinue} />
</View>
)}
{upgradeState == 2 && (
<View>
<Text style={{textAlign: 'center'}}>
{upgradeReceived} / {upgradeAllBytes}
</Text>
</View>
)}
</View>
</View>
</Modal>
);
}
return (
<SafeAreaView style={backgroundStyle}>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={backgroundStyle}>
<Header />
<View
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white,
}}>
<Section title="Step One">
Edit <Text style={styles.highlight}>App.js</Text> to change this
screen and then come back to see your edits.
</Section>
<Section title="See Your Changes">
<ReloadInstructions />
</Section>
<Section title="Debug">
<DebugInstructions />
</Section>
<Section title="Learn More">
Read the docs to discover what to do next:
</Section>
<LearnMoreLinks />
</View>
{renderUpdateModal()}
</ScrollView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 24,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
},
highlight: {
fontWeight: '700',
},
modal: {
padding: 25,
backgroundColor: 'rgba(10,10,10,0.6)',
height: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
},
content: {
backgroundColor: '#fff',
width: '100%',
padding: 18,
},
tip: {
paddingBottom: 20,
textAlign: 'center',
fontSize: 22,
},
des: {
paddingBottom: 20,
},
});
export default App;
在这个示例中,用Modal来做提示对话框。用this.state.upgradeState来控制对话框是否显示,当this.state.upgradeState的值大于0时则显示,其中,当this.state.upgradeState的值等于1时显示“提示更新”的消息,当this.state.upgradeState的值等于2时显示“下载进度”。
为了使App启动时自动检查新版本,可将相关代码写在/App.js的useEffect生命周期函数中。
5.5 验证热更新
通过以上的工作,热更新的功能已经添加到React Native App中了。接下来需要验证一下热更新功能是否能正常运作。首先,打开cpcn-client桌面工具,然后点击发布新版本按钮,等待命令执行完毕后。
接下来,当我们重新启动应用程序时就会收到升级新版本的提示。
当我们点击马上更新按钮时,就会调用下载函数执行文件的下载,由于示例应用需要更新的内容较少,所以下载过程几乎是一闪而过。当文件下载完之后应用会执行重启,重启后看到的就是最新版本的内容了。