react-native-code-push基础篇

CodePush是什么?

CodePush是一个微软开发的云服务器。通过它,开发者可以直接在用户的设备上部署手机应用更新。CodePush相当于一个中心仓库,开发者可以推送当前的更新(包括JS/HTML/CSS/IMAGE等)到CoduPush,然后应用将会查询是否有更新,本篇主要介绍使用微软云服务器

当然,我们也可以自己搭建code-push-server服务器,详情可以查看本地热更新服务器搭建。

实现环境

1.react-native:0.62.2

2.react-native-code-push:^6.2.0

首先介绍下Code Push相关命令

/**前言——code-pus相关指令**/
  /**注册登录相关指令**/
    code-push login    /*进行身份验证以开始管理您的应用*/
    code-push logout    /*注销当前会话*/
    code-push access-key     /*查看和管理与您的帐户关联的访问密钥*/
    code-push access-key ls    /*列出登陆的token*/
    code-push access-key rm <accessKye> 删除某个 access-key
  /**管理App相关指令**/
    code-push app    /*查看和管理您的CodePush应用*/
    code-push app add 在账号里面添加一个新的app
    code-push app remove 或者 rm 在账号里移除一个app
    code-push app rename 重命名一个存在app
    code-push app list 或则 ls 列出账号下面的所有app
    code-push app transfer 把app的所有权转移到另外一个账号
  /**查看deployment key**/
    code-push deployment    /*查看和管理您的应用程序部署*/
    code-push deployment ls Appname -k    /*查看deployment key*/
  /**其他**/
    code-push collaborator    /*查看和管理应用协作者*/
    code-push debug    /*查看正在运行的应用程序的CodePush调试日志*/
    code-push link    /*将其他身份验证提供程序(例如GitHub)链接到现有的Mobile Center帐户*/
    code-push patch    /*更新现有版本的元数据*/
    code-push promote    /*将最新版本从一种应用程序部署升级到另一种*/
    code-push register    /*注册一个新的Mobile Center帐户*/
    code-push release    /*发布更新到应用程序部署*/
    code-push release-cordova    /*将Cordova更新发布到应用程序部署*/
    code-push release-react    /*将React Native更新发布到应用程序部署*/
    code-push rollback    /*回滚最新版本的应用程序部署*/

接入流程

1.安装 CodePush CLI
2.注册 CodePush账号
3.在CodePush服务器注册App
4.React Native(JavaScript)端集成CodePush
5.原生应用中配置CodePush
6.发布更新的版本

react-native-code-push Demo示例

首先新建一个React Native项目CodePushDemo

1.CodePush CLI

使用CodePush之前,需要先安装CodePush命令行工具,并注册CodePush账号和应用,安装命令如下:
注意:这个CodePush指令只需要全局安装一次即可,如果第一次安装成功了,那后面就不在需要安装。

npm install -g code-push-cli
安装code-push-cli.png

安装完成后可以通过code-push -v命令进行验证


查看code-push -v.png

2.注册 CodePush账号

注册CodePush账号也很简单,同样是只需简单的执行下面的命令,同样这个注册操作也是全局只需要注册一次即可

code-push register

注意:当执行完上面的命令后,会自动打开一个授权网页,让你选择使用哪种方式进行授权登录,这里我们统一就选择使用GitHub即可

登录页面.png

当注册登录成功后,CodePush会给我们一个key:

获取CodePus Key.png

我们直接复制这个key,然后在终端中将这个key填写进去即可,填写key登录成功显示效果如下


登录成功.png

3.在CodePush服务器注册App

为了使CodePush服务器有我们的App,我们需要CodePush注册App,输入下面命令即可完成注册:
注意:如果我们的应用分为iOS和Android两个平台,这时我们需要分别注册两套key:

 code-push app add 应用平台命名 平台名称 使用的框架/语言
注册Android平台应用
code-push app add CodePushDemo-Android android react-native
注册安卓平台.png
注册iOS平台应用ios
code-push app add CodePushDemo-iOS ios react-native
注册苹果平台.png

应用添加成功后就会返回对应的 ProductionStaging 两个key。
Production 代表生产版的热更新部署,
Staging 代表开发版的热更新部署,
在ios中将staging的部署key复制在info.plist的CodePushDeploymentKey值中,
在android中复制在Application的getPackages的CodePush中

我们可以输入如下命令来查看我们刚刚添加的App
code-push app list
查看App列表.png

4.React Native(JavaScript)端集成CodePush

安装组件
npm install react-native-code-push --save
安装组件.png

添加原生依赖,这里添加依赖我们使用自动添加依赖的方式

react-native link react-native-code-push
添加原生依赖.png

JS逻辑代码实现

主页面(App.js)
/**
 * 热更新主页面
 * Created by 技术渣渣 on 2020/4/26
 **/
import React, {Component} from 'react'
import {View, Text, StyleSheet, TouchableOpacity} from 'react-native'
import CodePush from 'react-native-code-push';
import ProgressBarModal from './ProgressBarModal';
class App extends Component{
    constructor(props) {
        super(props);
        this.state={
            progressModalVisible:false
        }
    }
    componentDidMount() {
        this.syncImmediate(); //开始检查更新
    }
    syncImmediate() {
        CodePush.sync(
            {
                installMode: CodePush.InstallMode.IMMEDIATE,
                updateDialog: {
                    appendReleaseDescription: true, //是否显示更新description,默认为false
                    descriptionPrefix: '更新内容:', //更新说明的前缀。 默认是” Description:
                    mandatoryContinueButtonLabel: '立即更新', //强制更新的按钮文字,默认为continue
                    mandatoryUpdateMessage: '', //- 强制更新时,更新通知. Defaults to “An update is available that must be installed.”.
                    optionalIgnoreButtonLabel: '稍后', //非强制更新时,取消按钮文字,默认是ignore
                    optionalInstallButtonLabel: '后台更新', //非强制更新时,确认文字. Defaults to “Install”
                    optionalUpdateMessage: '有新版本了,是否更新?', //非强制更新时,更新通知. Defaults to “An update is available. Would you like to install it?”.
                    title: '更新提示', //要显示的更新通知的标题. Defaults to “Update available”.
                },
            },
            this.codePushStatusDidChange.bind(this),
            this.codePushDownloadDidProgress.bind(this),
        );
    }

    codePushStatusDidChange(syncStatus) {
        switch (syncStatus) {
            case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
                this.setState({syncMessage: 'Checking for update.'});
                break;
            case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
                this.setState({syncMessage: 'Downloading package.',progressModalVisible:true});
                break;
            case CodePush.SyncStatus.AWAITING_USER_ACTION:
                this.setState({syncMessage: 'Awaiting user action.'});
                break;
            case CodePush.SyncStatus.INSTALLING_UPDATE:
                this.setState({syncMessage: 'Installing update.',progressModalVisible:true});
                break;
            case CodePush.SyncStatus.UP_TO_DATE:
                this.setState({syncMessage: 'App up to date.', progress: false});
                break;
            case CodePush.SyncStatus.UPDATE_IGNORED:
                this.setState({syncMessage: 'Update cancelled by user.', progress: false,});
                break;
            case CodePush.SyncStatus.UPDATE_INSTALLED:
                this.setState({syncMessage: 'Update installed and will be applied on restart.', progress: false,});
                break;
            case CodePush.SyncStatus.UNKNOWN_ERROR:
                this.setState({syncMessage: 'An unknown error occurred.', progress: false,});
                break;
        }
    }

    codePushDownloadDidProgress(progress) {
        this.setState({progress});
    }


    render(){
        let progressView;
        if (this.state.progress) {
            let total = (this.state.progress.totalBytes/(1024*1024)).toFixed(2);
            let received =(this.state.progress.receivedBytes/(1024*1024)).toFixed(2);
            let progress = parseInt((received/total)*100);
                  progressView = (
                      <ProgressBarModal
                          progress={progress}
                          totalPackageSize={total}
                          receivedPackageSize={received}
                          progressModalVisible={this.state.progressModalVisible}
                      />
                  );
        }
        return(
            <View style={styles.container}>
                <Text style={styles.welcome}>欢迎使用热更新!</Text>
                <Text>版本1</Text>
                <TouchableOpacity onPress={this.syncImmediate.bind(this)}>
                    <Text style={styles.syncButton}>Press for dialog-driven sync</Text>
                </TouchableOpacity>
                { progressView }
            </View>
        )
    }
}

const styles = StyleSheet.create({
    container:{
        flex: 1,
        alignItems: 'center',
        backgroundColor: '#F5FCFF',
        paddingTop: 50,
    },
    welcome:{
        fontSize: 20,
        textAlign: 'center',
        margin: 20,
    },
    syncButton: {
        color: 'green',
        fontSize: 17,
    },
})

let codePushOptions = {checkFrequency: CodePush.CheckFrequency.MANUAL};

App = CodePush(codePushOptions)(App);

export default App;

进度条页面1(ProgressBarModal.js)
import React, { PureComponent } from 'react';
import {
    View,
    Modal,
    Text,
    ImageBackground,
    StyleSheet
} from 'react-native';
import Bar from './Bar';
import scale from './scale';

const propTypes = {
    ...Modal.propTypes,
};

const defaultProps = {
    animationType: 'none',
    transparent: true,
    progressModalVisible: false,
    onRequestClose: () => {},
};

/* 更新进度条Modal */
class ProgressBarModal extends PureComponent {

    constructor(props) {
        super(props);
        this.state = {
            title: '正在下载更新文件' // 更新提示标题
        };
    }

    render() {
        const {
            animationType,
            transparent,
            onRequestClose,
            progress,
            progressModalVisible,
            totalPackageSize,
            receivedPackageSize,
        } = this.props;
        return (
            <Modal
                animationType={animationType}
                transparent={transparent}
                visible={progressModalVisible}
                onRequestClose={onRequestClose}
            >
                <View style={styles.progressBarView}>
                    <View style={styles.imageBg}>
                        <Text style={styles.title}>
                            {this.state.title}
                        </Text>
                    </View>
                    <View style={styles.subView}>
                        <Bar
                            style={{ width: scale(540), borderRadius: scale(30) }}
                            progress={progress}
                            backgroundStyle={styles.barBackgroundStyle}
                        />
                        <Text style={styles.textPackageSize}>
                            {`${receivedPackageSize}M/${totalPackageSize}M`}
                        </Text>
                        <Text style={{color:'red',marginTop:scale(100)}}>*温馨提示:下载完更新文件,应用会重启</Text>
                    </View>
                    <View style={styles.bottomContainer} />
                </View>
            </Modal>
        );
    }
}

ProgressBarModal.propTypes = propTypes;
ProgressBarModal.defaultProps = defaultProps;

export default ProgressBarModal;

const styles = StyleSheet.create({
    imageBg: {
        width: scale(600),
        height: scale(100),
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor:'#1083E6',
        borderTopLeftRadius:scale(26),
        borderTopRightRadius:scale(26),
    },
    progressBarView: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
        backgroundColor: 'rgba(0,0,0,0.2)'
    },
    // 默认进度条背景底色
    barBackgroundStyle: {
        backgroundColor: '#e0e0e0'
    },
    subView: {
        width: scale(600),
        height: scale(296),
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center'
    },
    bottomContainer: {
        width: scale(600),
        height: scale(39),
        borderBottomLeftRadius: scale(26),
        borderBottomRightRadius: scale(26),
        backgroundColor: '#FFF'
    },
    textPackageSize: {
        fontSize: scale(40),
        color: '#686868',
        marginTop: scale(36)
    },
    title: {
        color: '#FFF',
        fontSize: scale(30)
    }
})
进度条页面1(Bar.js)
import React, { PureComponent } from 'react';
import {
    View,
    Animated,
} from 'react-native';
import PropTypes from 'prop-types';
import scale from './scale';


const propTypes = {
    progress: PropTypes.number.isRequired,
    backgroundStyle: PropTypes.number.isRequired,
    style: PropTypes.object.isRequired,
};

/* 进度条组件 */
class Bar extends PureComponent {

    constructor(props) {
        super(props);
        this.progress = new Animated.Value(0);
        this.update = this.update.bind(this);
    }

    componentWillReceiveProps(nextProps) {
        if (this.props.progress >= 0 && this.props.progress !== nextProps.progress) {
            this.update(nextProps.progress);
        }
    }

    update(progress) {
        Animated.spring(this.progress, {
            toValue: progress
        }).start();
    }

    render() {
        return (
            <View style={[this.props.backgroundStyle, this.props.style]}>
                <Animated.View style={{
                    backgroundColor: '#1083E6',
                    height: scale(28),
                    borderRadius: scale(30),
                    width: this.progress.interpolate({
                        inputRange: [0, 100],
                        outputRange: [0, 1 * this.props.style.width],
                    }) }}
                />
            </View>
        );
    }
}

Bar.propTypes = propTypes;

export default Bar;

5.原生应用中配置CodePush

配置Android平台

1.编辑 android/app/build.gradle,新增依赖:

  dependencies {
       implementation project(':react-native-code-push')
  }

2.编辑 android/app/build.gradle,修改buildTypes,输入对应的Key:

  buildTypes {
        debug {
            signingConfig signingConfigs.debug
            resValue "string", "CodePushDeploymentKey", ""
        }
        release {
            // Caution! In production, you need to generate your own keystore file.
            // see https://facebook.github.io/react-native/docs/signed-apk-android.
            signingConfig signingConfigs.release
            minifyEnabled enableProguardInReleaseBuilds
            proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
            //这里的key为Productionde的key
            resValue "string", "CodePushDeploymentKey", "9DSt0TxqNuDlsX1SneATSr8qrrkJ4ksvOXqog"
        }
        releaseStaging {
            //这里的key为Staging的key
            resValue "string", "CodePushDeploymentKey", "HmTsl4j3G7qlMtxmIG93beLpF6ns4ksvOXqog"
            matchingFallbacks = ['release']
        }
    }

3.编辑 android/app/build.gradle,修改defaultConfig,将versionName 修改为三位(1.0.0):

    defaultConfig {
        applicationId "com.codepushdemo"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0.0"
    }

6.发布更新的版本

使用前应考虑到应用检查更新的时机,是否要求强制更新,是否要求即时更新等问题。

常见的更新时机有两种

1.打开App就自动检查更新

可以在页面的componentDidMount生命周期方法里通过codePush.sync来触发检查更新,如果有更新包可供下载则会在重启后生效。
注意:这种下载和安装都是静默的,即用户不可见,本文额外增加了更新下载进度条的 modal

codePush.sync({
         installMode: CodePush.InstallMode.IMMEDIATE,
         updateDialog: {
              appendReleaseDescription: true, //是否显示更新description,默认为false
              descriptionPrefix: '更新内容:', //更新说明的前缀。 默认是” Description:
              mandatoryContinueButtonLabel: '立即更新', //强制更新的按钮文字,默认为continue
              mandatoryUpdateMessage: '', //- 强制更新时,更新通知. Defaults to “An update is available that must be installed.”.
              optionalIgnoreButtonLabel: '稍后', //非强制更新时,取消按钮文字,默认是ignore
              optionalInstallButtonLabel: '后台更新', //非强制更新时,确认文字. Defaults to “Install”
              optionalUpdateMessage: '有新版本了,是否更新?', //非强制更新时,更新通知. Defaults to “An update is available. Would you like to install it?”.
              title: '更新提示', //要显示的更新通知的标题. Defaults to “Update available”.
                },
  }),
2.用户点击检查更新并安装

在用户点击按钮触发事件时,在调用上面的codePush.sync方法,在有更新时弹出提示框让用户选择是否更新,如果用户点击立即更新按钮,则会进行安装包的下载,下载完成后会立即重启并生效。

常见的更新时机有两种

如果是强制更新需要在发布的时候指定,发布命令中配置--m true

更新是否要求即时

在更新配置中通过指定installMode来决定安装完成的重启时机,亦即更新生效时机
1.codePush.InstallMode.IMMEDIATE :安装完成立即重启更新
2.codePush.InstallMode.ON_NEXT_RESTART :安装完成后会在下次重启后进行更新
3.codePush.InstallMode.ON_NEXT_RESUME :安装完成后会在应用进入后台后重启更新

如何发布CodePush更新包

本文介绍楼主使用过的发布方式,通过以下指令发布更新包:

code-push release-react <Appname> <Platform> --t <本更新包面向的旧版本号> --dev false --d Production --des <本次更新说明> --m true

注意:CodePush默认是更新Staging 环境的,如果发布生产环境的更新包,需要指定--d参数:--d Production,如果发布的是强制更新包,需要加上 --m true强制更新

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335