React Native集成CodePush热更新

4,594 阅读7分钟

作为一款以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桌面工具,然后点击发布新版本按钮,等待命令执行完毕后。 在这里插入图片描述 接下来,当我们重新启动应用程序时就会收到升级新版本的提示。 在这里插入图片描述 当我们点击马上更新按钮时,就会调用下载函数执行文件的下载,由于示例应用需要更新的内容较少,所以下载过程几乎是一闪而过。当文件下载完之后应用会执行重启,重启后看到的就是最新版本的内容了。