react nativeReact NativeRN iOS开发相关

react-native热更新全方位讲解

2018-01-12  本文已影响3500人  Yochi

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

热更新流程图

热更新步骤

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

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

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

  $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   #连接到项目中,提示输入配置可以先行忽略

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表现,不然直接用这个方法就可以了。

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

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

--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)

上一篇下一篇

猜你喜欢

热点阅读