react-native热更新全方位讲解

作者: Yochi | 来源:发表于2018-01-12 16:49 被阅读3500次

    最近在研究热更新技术,看了网上各个大佬的博客,整体流程上总是卡壳。跳了几天坑,刚刚终于把简单的热更新流程跑通,现在也正在一边学习更新,一边整理资料,在此篇博客上记录操作流程,希望我的实践能帮助各位同行少走弯路,快速掌握热更新技术。

    热更新流程图

    热更新步骤

    • 1、热更新服务器部署

    • 2、客户端配置

    • 3、客户端热更新代码

    • 4、打包最新代码

    • 5、推送你的新包到客户端

    • 6、疑问
      Q: “苹果应用商店和android应用商店允不允许使用热更新?”
      A: “都允许。”

      苹果允许使用热更新Apple's developer agreement 条款3.3.2, 但是规定不能弹框提示用户更新,影响用户体验。苹果禁的是 rollout.io, JSPatch 这类具备修改原生代码能力的框架。 Google Play也允许热更新,但必须弹框告知用户更新。在中国的android市场发布时,都必须关闭更新弹框,否则会在审核应用时以“请上传最新版本的二进制应用包”驳回应用。

      Q: “react-native 开发环境更新模式是否可以直接用在生产环境下?”
      A: “不能。”
      Q: “code-push使用复杂么?”
      A: “不复杂。很多网上的文章说复杂,是因为作者没有仔细理解官方文档,而且认为踩坑了。”
      Q: “为什么推荐code-push?”
      A: ”非常好。除了满足基本更新功能外,还有统计,hash计算容错和补丁更新功能。微软的项目,大公司技术有保障,而且开源。1.直接对用户部署代码更新2.管理 Alpha,Beta 和生产环境应用3.支持 React Native 和 Cordova4.支持JavaScript 文件与图片资源的更新5.CodePush开源了react-native版本,react-native-code-push托管在GitHub上。

    codepushserver本地部署,动动脑,动动手,更新速度蹭蹭的

    此处是在mac平台上搭建的codepush服务器。

    • 安装MySQL


      安装完成后,会弹出初始密码,粗细的我,随手点了OK,此处挖坑半天。
      权限密码问题可以参考这篇博客,其它的都是小问题
      将mysql的命令添加到系统中
      
      (1).进入/usr/local/mysql/bin,查看此目录下是否有mysql
      (2).执行vim ~/.bash_profile
          在该文件中添加mysql/bin的目录
          PATH=$PATH:/usr/local/mysql/bin
      添加完成后,按esc,然后输入:wq保存。
      (3).最后在命令行输入source ~/.bash_profile
      
      (4). 通过mysql -uroot -p登录mysql, 输入之前保存的密码
      
      (5).重置mysql初始密码
      登录mysql后输入如下命令重置密码:
      SET PASSWORD FOR 'root'@'localhost' = PASSWORD('newpassword');
      
      
    • 部署code-push-server

      若数据库初始化失败,修改给初始密码后便可成功
      $ cd 存放code-push-server文件夹
      $ git clone https://github.com/lisong/code-push-server.git
      $ cd code-push-server
      $ npm install
      $ vim ./bin/db
      
       #别高兴,路漫漫其修远兮
       #初始化mysql  数据库 
       $ ./bin/db init --dbhost localhost --dbuser root --dbpassword xx00(刚刚刚安  装的数据库密码) 
       #若错误使用vim编辑器或者subtext修改code-push-server/bin/db里面的默认数据库密码
      
      

      修改配置文件:code-push-server/config/config.js
      如果文件放在七牛上,就修改七牛的配置,本次搭建在电脑本地

      修改数据库密码就行,端口都不用修改
      db: {
      username: process.env.RDS_USERNAME || "root",
      password: process.env.RDS_PASSWORD || 'gyq123456',
      database: process.env.DATA_BASE || "codepush",
      host: process.env.RDS_HOST || "127.0.0.1",
      port: process.env.RDS_PORT || 3306,
      dialect: "mysql",
      logging: false
      },
      local: {
      // 一些产生的二进制文件会存放在这里,需要你创建文件夹
      storageDir:"/Users/chmtech003/Sites/storage",
      // ip,改为你电脑的ip,其他不用换
      downloadUrl: "http://192.168.163.108:3000/download",
      // 照着写
      public: '/download'
      },
      jwt: {
      // Recommended: 63 random alpha-numeric characters
      // 网址打开: https://www.grc.com/passwords.htm
      // 找到63 random alpha-numeric characters 复制放在放在里面就行
      tokenSecret:'pq7fHGHvsv3JPZwMZqj79I7eEPvErMUPlM5hFZUyGdPDV9MSQfLZTCLn25sehhR'
      },
      
    • 获取登录token

      #启动服务 浏览器中打开 http://127.0.0.1:3000
       $ ./bin/www 
      
      #使用 code-push-cli 管理 CodePushServer
      $ npm install code-push-cli@latest -g
      $ code-push login http://127.0.0.1:3000 #login in browser account:admin password:123456
      #登录成功后,将浏览器窗口获取到的token粘贴到需要输入token的终端窗口,登录成功
      # code-push logout 登出
      
      # 修改登录密码方式,不推荐修改,难记
      $ curl -X PATCH -H "Authorization: Bearer mytoken" -H "Accept: application/json" -H "Content-Type:application/json" -d '{"oldPassword":"123456","newPassword":"654321"}' http://127.0.0.1:3000/users/password
      
    • 获取客户端key

      $ code-push app add 工程名-android #android版
      $ code-push app add 工程名-ios #ios版
      Successfully added the "HotUpdateDemo-android" app, along with the   following default deployments:
      ┌────────────┬──────────────────────────────────  ─────┐
      │ Name       │ Deployment Key                        │
      ├────────────┼──────────────────────────────────  ─────┤
      │ Production │ Praagnzym0XuijhvH1LRNSdK4kPO4ksvOXqog │
      ├────────────┼──────────────────────────────────  ─────┤
      │ Staging    │ jcgp79y1MT8YSZzPmfs60LbY8vYP4ksvOXqog │
      └────────────┴──────────────────────────────────  ─────┘
      分别获得iOS和安卓的Deployment Key,推送的时候通过key将app和服务器端关联,推送是分开的,所以iOS,Android分别在项目名后加入后缀区分
      
      # 命令汇总
      $code-push app list 可以查看创建好的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 list 或则 ls 列出账号下面的所有app
      $code-push app transfer 把app的所有权转移到另外一个账号
      

    客户端配置code-push提供的demo(建议自己创建,避免环境问题)

    • 客户端环境搭建
      创建一个RN的练手项目,然后配置上面获取到的Deployment Key 配置
      $cd ./ 存放项目目录
      $react-native init HotUpdateDemo
      $ cd ./HotUpdateDemo
      $ npm install --save react-native-code-push  #安装react-native-code-push
      $ react-native link react-native-code-push   #连接到项目中,提示输入配置可以先行忽略
    
    • iOS添加Deployment Key
      在info.plist中添加key
      <key>CodePushDeploymentKey</key>
      <string>Deployment Key</string>
      <key>CodePushServerURL</key>
      <string>填入你的电脑IP:eg. http://192.168.0.7:3000</string>
      
    • Android添加Deployment Key
       在MainApplication.java文件中
      @Override
      protected List<ReactPackage> getPackages() {
        return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new CodePush(
             "填入Deployment Key ",
             MainApplication.this,
             BuildConfig.DEBUG,
             "填入你的电脑IP:eg. http://192.168.0.7:3000" 
          )
      );
      }
      

    CodePush的API有个大致的了解点击查看来源

    import CodePush from "react-native-code-push";

    --CodePush.sync--
    CodePush.sync(options: Object, syncStatusChangeCallback: function(syncStatus: Number), downloadProgressCallback: function(progress: DownloadProgress)): Promise<Number>;
    通过调用该方法CodePush会帮我们自动完成检查更新,下载,安装等一系列操作。除非我们需要自定义UI表现,不然直接用这个方法就可以了。

    • sync方法,提供了如下属性以允许你定制sync方法的默认行为
      • deploymentKey (String): 部署key,指定你要查询更新的部署秘钥,默认情况下该值来自于Info.plist(Ios)和MianActivity.java(Android)文件,你可以通过设置该属性来动态查询不同部署key下的更新。
      • installMode (codePush.InstallMode): 安装模式,用在向CodePush推送更新时没有设置强制更新(mandatory为true)的情况下,默认codePush.InstallMode.ON_NEXT_RESTART即下一次启动的时候安装。
      • mandatoryInstallMode (codePush.InstallMode):强制更新,默认codePush.InstallMode.IMMEDIATE。
      • minimumBackgroundDuration (Number):该属性用于指定app处于后台多少秒才进行重启已完成更新。默认为0。该属性只在installMode为InstallMode.ON_NEXT_RESUME情况下有效。
      • updateDialog (UpdateDialogOptions) :可选的,更新的对话框,默认是null,包含以下属性
        • appendReleaseDescription (Boolean) - 是否显示更新description,默认false
        • descriptionPrefix (String) - 更新说明的前缀。 默认是” Description: “
          mandatoryContinueButtonLabel (String) - 强制更新的按钮文字. 默认 to “Continue”.
        • mandatoryUpdateMessage (String) - 强制更新时,更新通知. Defaults to “An update is available that must be installed.”.
        • optionalIgnoreButtonLabel (String) - 非强制更新时,取消按钮文字. Defaults to “Ignore”.
        • optionalInstallButtonLabel (String) - 非强制更新时,确认文字. Defaults to “Install”.
        • optionalUpdateMessage (String) - 非强制更新时,更新通知. Defaults to “An update is available. Would you like to install it?”.
        • title (String) - 要显示的更新通知的标题. Defaults to “Update available”.

    --CodePush.allowRestart--
    CodePush.allowRestart(): void;
    允许重新启动应用以完成更新。
    如果一个CodePush更新将要发生并且需要重启应用(e.g.设置InstallMode.IMMEDIATE模式),但由于调用了disallowRestart方法而导致APP无法通过重启来完成更新,可以调用此方法来解除disallowRestart限制。
    但在如下四种情况下,CodePush将不会立即重启应用:

    • 自上一次disallowRestart被调用,没有新的更新。
    • 有更新,但installMode为InstallMode.ON_NEXT_RESTART的情况下。
    • 有更新,但installMode为InstallMode.ON_NEXT_RESUME,并且程序一直处于前台,并没有从后台切换到前台的情况下。
    • 自从上次disallowRestart被调用,没有再调用restartApp。

    --CodePush.checkForUpdate--
    codePush.checkForUpdate(deploymentKey: String = null): Promise<RemotePackage>;
    向CodePush服务器查询是否有更新。
    该方法返回Promise,有如下两种值:

    • null 没有更新
      通常有如下情况导致RemotePackage为null:
      1. 当前APP版本下没有部署新的更新版本。也就是说没有想CodePush服务器推送基于当前版本的有关更新。
      2. CodePush上的更新和用户当前所安装的APP版本不匹配。也就是说CodePush服务器上有更新,但该更新对应的APP版本和用户安装的当前版本不对应。
      3. 当前APP已将安装了最新的更新。
      4. 部署在CodePush上可用于当前APP版本的更新被标记成了不可用。
      5. 部署在CodePush上可用于当前APP版本的更新是"active rollout"状态,并且当前的设备不在有资格更新的百分比的设备之内。
    • A RemotePackage instance
      有更新可供下载。

    --CodePush.disallowRestart--
    CodePush.disallowRestart(): void;
    不允许立即重启用于以完成更新。

    --CodePush.getUpdateMetadata --
    CodePush.getUpdateMetadata(updateState: UpdateState = UpdateState.RUNNING): Promise<LocalPackage>;
    获取当前已安装更新的元数据(描述、安装时间、大小等)。

    --CodePush.notifyAppReady --
    codePush.notifyAppReady(): Promise<void>;
    通知CodePush,一个更新安装好了。当你检查并安装更新,(比如没有使用sync方法去handle的时候),这个方法必须被调用。否则CodePush会认为update失败,并rollback当前版本,在app重启时。
    当使用sync方法时,不需要调用本方法,因为sync会自动调用。

    --CodePush.restartApp --
    CodePush.restartApp(onlyIfUpdateIsPending: Boolean = false): void;
    立即重启app。
    当以下情况时,这个方式是很有用的:
    app 当 调用 sync 或 LocalPackage.install 方法时,指定的 install mode是 ON_NEXT_RESTART 或 ON_NEXT_RESUME时 。 这两种情况都是当app重启或resume时,更新内容才能被看到。
    在特定情况下,如用户从其它页面返回到APP的首页时,这个时候调用此方法完成过更新对用户来说不是特别的明显。因为强制重启,能马上显示更新内容。

    熟悉了上面的api,现在再来看CodePush提供的更新demo是不是很简单了
    点击查看demo
    这个是测试代码,全选,替换app.js文件中的代码即可,有大兄弟使用下面代码更新报错,若遇到同类问题,点击上面的Demo替换。

    /**
     * Sample React Native App
     * https://github.com/facebook/react-native
     * @flow
     */
    
    import React, { Component } from 'react';
    import {
      AppRegistry,
      Dimensions,
      Image,
      StyleSheet,
      Text,
      TouchableOpacity,
      Platform,
      View,
    } from 'react-native';
    
    import CodePush from "react-native-code-push";
    
    //var Dimensions = require('Dimensions');
    
    const instructions = Platform.select({
      ios: 'Press Cmd+R to reload,\n' +
        'Cmd+D or shake for dev menu',
      android: 'Double tap R on your keyboard to reload,\n' +
        'Shake or press menu button for dev menu',
    });
    
    export default class App extends Component<{}> {
    
      constructor() {
        super();
        this.state = { restartAllowed: true ,
                       syncMessage: "我是小更新" ,
                       progress: false};
      }
    
      // 监听更新状态
      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." });
            break;
          case CodePush.SyncStatus.AWAITING_USER_ACTION:
            this.setState({ syncMessage: "Awaiting user action." });
            break;
          case CodePush.SyncStatus.INSTALLING_UPDATE:
            this.setState({ syncMessage: "Installing update." });
            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 });
      }
    
      // 允许重启后更新
      toggleAllowRestart() {
        this.state.restartAllowed
          ? CodePush.disallowRestart()
          : CodePush.allowRestart();
    
        this.setState({ restartAllowed: !this.state.restartAllowed });
      }
    
      // 获取更新数据
      getUpdateMetadata() {
        CodePush.getUpdateMetadata(CodePush.UpdateState.RUNNING)
          .then((metadata: LocalPackage) => {
            this.setState({ syncMessage: metadata ? JSON.stringify(metadata) : "Running binary version", progress: false });
          }, (error: any) => {
            this.setState({ syncMessage: "Error: " + error, progress: false });
          });
      }
    
      /** Update is downloaded silently, and applied on restart (recommended) 自动更新,一键操作 */
      sync() {
        CodePush.sync(
          {},
          this.codePushStatusDidChange.bind(this),
          this.codePushDownloadDidProgress.bind(this)
        );
      }
    
      /** Update pops a confirmation dialog, and then immediately reboots the app 一键更新,加入的配置项 */
      syncImmediate() {
        CodePush.sync(
          { installMode: CodePush.InstallMode.IMMEDIATE, updateDialog: true },
          this.codePushStatusDidChange.bind(this),
          this.codePushDownloadDidProgress.bind(this)
        );
      }
    
      render() {
    
       let progressView;
    
        if (this.state.progress) {
          progressView = (
            <Text style={styles.messages}>{this.state.progress.receivedBytes} of {this.state.progress.totalBytes} bytes received</Text>
          );
        }
    
        return (
          <View style={styles.container}>
    
            <Text style={styles.welcome}>
             可以修改此处文字,查看是否更新成功!
            </Text>
    
            <TouchableOpacity onPress={this.sync.bind(this)}>
              <Text style={styles.syncButton}>Press for background sync</Text>
            </TouchableOpacity>
    
            <TouchableOpacity onPress={this.syncImmediate.bind(this)}>
              <Text style={styles.syncButton}>Press for dialog-driven sync</Text>
            </TouchableOpacity>
    
            {progressView}
            
            <TouchableOpacity onPress={this.toggleAllowRestart.bind(this)}>
              <Text style={styles.restartToggleButton}>Restart { this.state.restartAllowed ? "allowed" : "forbidden"}</Text>
            </TouchableOpacity>
    
            <TouchableOpacity onPress={this.getUpdateMetadata.bind(this)}>
              <Text style={styles.syncButton}>Press for Update Metadata</Text>
            </TouchableOpacity>
    
            <Text style={styles.messages}>{this.state.syncMessage || ""}</Text>
    
          </View>
        );
      }
    }
    
    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,
      },
      messages: {
        marginTop: 30,
        textAlign: "center",
      },
      restartToggleButton: {
        color: "blue",
        fontSize: 17
      },
      syncButton: {
        color: "green",
        fontSize: 17
      },
      welcome: {
        fontSize: 20,
        textAlign: "center",
        margin: 20
      },
    });
    
    

    热更新操作

    • 最简单的方式
    cd ./工程目录
    测试环境执行:
    $ code-push release-react 项目名-android android
    生产环境执行:
    $ code-push release-react 项目名-android android -d Production
    一直没成功过,不知道为什么,这种一句命令的更新超级简单,适不适用,待考虑
    
    • 通过上传包来进行更新

    --离线包--

     $ cd ./工程目录
     $ mkdir bundles
     $ react-native bundle --platform 平台 --entry-file 启动文件 --bundle-output 打包js输出文件 --assets-dest 资源输出目录 --dev 是否调试。
     eg.
     $react-native bundle --platform ios --entry-file index.js --bundle-output ./ios/bundles/main.jsbundle --dev false
    

    --发布更新--

    $ code-push release <应用名称> <Bundles所在目录> <对应的应用版本> --deploymentName: 更新环境 --description: 更新描述 --mandatory: 是否强制更新 
    code-push release HotUpdateDemo-ios ./ios/bundles/main.jsbundle 1.0.0 --deploymentName Production --description "我是新包,很新的那种" --mandatory true  
    

    调试技巧

    ***iOS***
     把基础版离线包 main.jsbundle放入工程,在appdelegate中,只保留 jsCodeLocation = [CodePush bundleURL];安装初始版本
    //    #ifdef DEBUG
    //        jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
    //    #else
            jsCodeLocation = [CodePush bundleURL];
    //    #endif
    
    ***Android***
    在Android可以将开发环境的调试地址改为一个不可用的地址,这样APP就无法连接到NodeJS服务器了,自然也就不能从NodeJS服务器下载bundle进行更新了,它也只能乖乖的等待从CodePush服务器下载更新包进行更新了。
    

    热更新技术持续补充中。。。

    好博客推荐:
    文章中,没有提到的地方,或遇到的问题,可以在下面博客中找到答案,我有坑的地方我会写上去,相同的内容,我就不再造轮子了。。。
    bsdiff和bspatch热更新方案
    https://www.jianshu.com/p/9e3b4a133bcc (iOS和Android环境配置比较全)
    http://blog.csdn.net/szy406469533/article/details/75663722
    JSPatch实现原理详解 对苹果无用了哦
    React-Native痛点解析之开发环境搭建及扩展
    React-Native 热更新以及增量更新
    react native 实战系列教程之热更新原理分析与实现
    diff和patch使用指南
    ReactNative之bundle文件瘦身(google-diff-match-patch)
    react native增量热更新生成合并补丁文件
    ReactNative增量升级方案
    http://www.itwendao.com/article/detail/240040.html
    ReactNative之bundle文件瘦身(google-diff-match-patch)

    相关文章

      网友评论

      • 一线码农:按你写的步骤配置成功了,可以实现热更新了,有点问题就是使用
        测试环境执行:
        $ code-push release-react 项目名-android android
        生产环境执行:
        $ code-push release-react 项目名-android android -d Production
        这两句上传更新包的话就可以在客户端更新,如果使用后面的上传离线包的方式就不行,目前还没找到原因
      • DaZenD:你好,看到你参考的这篇文章 https://github.com/cnsnake11/blog/blob/master/ReactNative开发指导/ReactNative增量升级方案.md 说
        其增量升级仅仅是针对图片资源的
        其升级服务器端程序并不开源
        其升级服务器在美国,国内访问很慢且不稳定
        请问:您基于code-push-server搭建本地服务能解决1和3问题吗
      • 既然可以颠覆何必循规蹈矩:我服务器是放在阿里云 我在RN目录下执行 上传包的时候报错
        执行 code-push release-react LookingForRoom-ios ios
        报错 [Error] connect ECONNREFUSED 127.0.0.1:3000
        请问有人遇到这个问题吗?
      • 小码儿:有些东西没有看懂
        Yochi:嗯,没看懂你说,我补充进去
      • haohua:大兄弟,配置本地服务器的时候遇到“http://127.0.0.1:3000/”不能访问怎么搞呢?还有就是terminal执行“./bin/www ”竟然抛出了异常,好尴尬:“internal/crypto/hash.js:53
        throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'data',
        ^

        TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be one of type string, TypedArray, or DataView”。
      • 邢峰_461d:热更新的时候能不能显示进度条??
      • 每天都有新的太阳:给力!点赞,热更新趟坑必备

      本文标题:react-native热更新全方位讲解

      本文链接:https://www.haomeiwen.com/subject/ygcznxtx.html