Notes and thoughts from Tony

RN 热更新及 code-push 的应用

Comments

前言

距离上次更新已经很久了。

中间做了一些 SwiftGG 小组的翻译工作,有兴趣可以看看这里。技术上主要是 RN 方向的研究,从各种红色警告到在公司项目中使用,再到方案的基本成型,基本已经算是上道了。主要是思想的一些转变,没有记录技术的东西。最近要上热更新方案,研究了一下,感觉配置的一些东西比较多,就记录下吧。

热更新方案主要有微软的 CodePushpushy 。考虑到灵活性及安全性,最后还是倾向于部署自己的热更新服务器。于是找到了 code-push-server

只记录步骤和关键的坑,不再赘述原理和原因,有些地方会给出链接。

环境搭建

目前比较稳定的 RN 版本是 0.44.3,对应的 react-native-code-push 版本为 2.x。

"react": "16.0.0-alpha.6",
"react-native": "0.44.3",
"react-native-code-push": "2.1.1-beta"

Podfile 配置文件添加:

pod 'CodePush', :path => '../node_modules/react-native-code-push'

npm install 之后,在 ios 目录下执行 pod install

iOS 项目编译如果有错,则需要将各种缓存清掉。还有就是 0.44.3 有个 bug,大概是 import <RCTAnimation/RCTValueAnimatedNode.h> 找不到。需要在 package.json 添加构建脚本:

"postinstall": "sed -i '' 's/#import <RCTAnimation\\/RCTValueAnimatedNode.h>/#import \"RCTValueAnimatedNode.h\"/' ./node_modules/react-native/Libraries/NativeAnimation/RCTNativeAnimatedNodesManager.h",

iOS 项目中的 plist 文件添加键为 CodePushServerURLCodePushDeploymentKey 的配置,后面会讲到值。

参考:

https://github.com/Microsoft/react-native-code-push

https://github.com/coderwin/CodePushCN

code-push-server 部署

步骤在这个链接说的比较细,需要注意的是在启动之前,要安装并启动 mysql 服务。

配置 config/config.js 文件除了文档中提到的修改之外,还需要的,主要有:db 的 host 和密码,storageDir 修改为自己的目录,downloadUrl,dataDir。直接搜索关键字改掉就可以,否则会报没有权限创建目录的错误。

其他参考: react native codepush之搭建自己的更新服务器

上面提到的 CodePushServerURL 为部署服务器的 url,本机测试可以使用 http://127.0.0.1:3000 ,局域网测试可以替换相应的 ip 地址。

CodePushDeploymentKey 有提到,就是 deploymentKey。不同的 deployment 对应不同的 key。

代码修改

iOS 项目中加载 bundle 的代码修改为:

jsCodeLocation = [CodePush bundleURLForResource:@"main" withExtension:@"jsbundle" subdirectory:@"bundle"];

CodePush 提供了好几个加载资源的方法,按需使用。

AppDelegate 中可以引入 #import <React/RCTLog.h> ,然后使用 RCTSetLogThreshold(RCTLogLevelInfo); 开启日志。

js 这边的关键代码:

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

componentDidMount() {
    CodePush.notifyAppReady();//为避免警告
    this.syncImmediate();
}

/** 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, //启动模式三种:ON_NEXT_RESUME、ON_NEXT_RESTART、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)
    );
}

_codePushDownloadDidProgress(progress) {
    console.log(progress);
}

_codePushStatusDidChange(syncStatus) {
    switch(syncStatus) {
        case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
        console.log("Checking for update.");
        break;
        case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
        console.log("Downloading package.");  
        break;
        case CodePush.SyncStatus.AWAITING_USER_ACTION:
        console.log("Awaiting user action."); 
        break;
        case CodePush.SyncStatus.INSTALLING_UPDATE:
        console.log("Installing update."); 
        break;
        case CodePush.SyncStatus.UP_TO_DATE:
        console.log("App up to date.");
        break;
        case CodePush.SyncStatus.UPDATE_IGNORED:
        console.log("Update cancelled by user.");
        break;
        case CodePush.SyncStatus.UPDATE_INSTALLED:
        console.log("Update installed and will be applied on restart.");
        break;
        case CodePush.SyncStatus.UNKNOWN_ERROR:
        console.log("An unknown error occurred.");
        break;
    }
}

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

APP = CodePush(codePushOptions)(App);

AppRegistry.registerComponent('APP', () => APP);

热更新操作

主要是使用 [code-push-cli](https://github.com/Microsoft/code-push),进行一些操作,实现打包和发布。 关键命令参考:CodePush 命令行

最主要的是使用 code-push release-react 命令,此命令实际是 react-native bundlecode-push release 的合体。

在这里注意,查看 code-push release-react 的命令帮助,有这样的参数说明。iOS 默认的 bundle 名字为 main.jsbundle,我之前配置打包出的名字为 index.ios.jsbundle,但是更新时会提示找不到 main.jsbundle,所以就改掉了。

–plistFile 参数用来指定 iOS 的 plist 配置文件,取其中的版本来作为基础版本进行更新。

$ code-push release-react
Usage: code-push release-react <appName> <platform> [options]

选项:
  --bundleName, -b           Name of the generated JS bundle file. If unspecified, the standard bundle name will be used, depending on the specified platform: "main.jsbundle" (iOS), "index.android.bundle" (Android) or "index.windows.bundle" (Windows)  [字符串] [默认值: null]
  ...
  --plistFile, -p            Path to the plist file which specifies the binary version you want to target this release at (iOS only).  [默认值: null]

另外,CodePush 方案中,是否有热更新都是基于 code-push release-react 中所指定的 -v 参数或 plist 中的版本,而热更新的版本(可以理解为 bundle 更新包的版本)则用 Label 标识。比如执行命令 code-push deployment history <appName> <deploymentName>

01

修改下导航栏的颜色,然后执行 code-push release-react ... 命令,重启后可以看到导航栏颜色被修改😆。

总结

至此,本地的 CodePush 部署已经成功,下周回来就需要项目集成和服务部署了,遇到坑会再记录下来。