CodePush热更新之Cordova项目实践

用web做app的都知道codepush,它可以跳过频繁的app发布审查过程,可以方便快捷的更新程序,如cordova, react-native等,本文介绍微软的cordova-plugin-code-push插件(也是用的最多的)。

CodePush

微软开发的,可以实时更新 React Native 和 Cordova 应用,给 React Native 和 Cordova 开发者直接部署移动应用更新给用户设备的云服务。

作为一个云仓库,作为开发者可以直接推送更新到 JS, HTML, CSS and images,应用可以从客户端 SDKs 里面查询更新。

可以让我们在修复一些小问题和添加新特性的时候,不需要经过二进制打包,可以直接推送代码进行实时更新。

CodePush 推送代码更新:

  • 直接对用户部署代码更新 管理 Alpha,Beta 和生产环境应用
  • 支持 Cordova 和 React Native
  • CodePush提供两个客户端 SDKs(Cordova 和React Native)
  • 相关插件cordova-plugin-code-push
  • 另一个热更新的插件,但是已经在2018.09.30宣布不再维护了,可以了解下(cordova-hot-code-push)
  • 苹果App允许使用热更新Apple's developer agreement, 为了不影响用户体验,规定必须使用静默更新。 Google Play不能使用静默更新,必须弹框告知用户App有更新。中国的android市场必须采用静默更新(如果弹框提示,App会被“请上传最新版本的二进制应用包”原因驳回)。

准备环境

全局安装typescript、typings、tslint、code-push-cli、cordova

npm install -g typescript
npm install -g typings
npm install -g tslint
npm install -g cordova
npm install -g code-push-cli

注:code-push-cli是连接热更新服务端的工具,我们把要更新的代码上传至服务端,客户端安装cordova-plugin-code-push插件来从服务端下载代码.

注册登录CodePush服务器

CodePush终端安装完成后就可以使用code-push命令了。
在终端输入code-push registercode-push login(有账号),会跳转授权网页。在这个网页可以选择Github。或者微软作为授权提供者,不过我觉得90%的开发者都会选择Github。

codepush.png

授权完成后,CodePush会显示你的Access Key,复制输入到终端即可完成注册并登陆。
accesskey.png

cmd.png

若选择CodePush Server 私有化部署,则只需登录自己的code push Server的url,code-push login url,然后输入运维给你的Access Key即可。

在CodePush服务器中创建App

在终端输入code-push app add <appName>即可完成创建,注册完成之后会返回一套deployment key,包括Staging和Production,分别代表开发和生产环境

Usage: code-push app add <appName> <os> <platform>
$:code-push app add MyApp android cordova

add.png

添加完成后,可以登录https://appcenter.ms/apps查看创建的MyApp.

创建Cordova项目并整合CodePush

创建项目

cordova create MyApp

添加平台

cd MyApp
cordova platform add android
or
cordova platform add android@^6.0.0
cordova platform add ios
目前使用的是6.x

添加插件

cordova plugin add cordova-plugin-code-push@latest
cordova plugin add cordova-plugin-whitelist

在config.xml中配置deployment keys:
当在code-push服务器添加app应用目录时,会生成生产和开发的key,需要配置在这里,app通过此key更新

<platform name="android">
<preference name="CodePushDeploymentKey" value="YOUR-ANDROID-DEPLOYMENT-KEY" />
</platform>
<platform name="ios">
<preference name="CodePushDeploymentKey" value="YOUR-IOS-DEPLOYMENT-KEY" />
</platform>

若不知道上面的key,可以用命令查看:

code-push deployment ls APP_NAME -k

config.xml中配置访问权限:

<access origin="https://codepush.azurewebsites.net" />
<access origin="https://codepush.blob.core.windows.net" />
<access origin="https://codepushupdates.azureedge.net" />

<access origin="*" />

index.html配置白名单安全协议:

<meta http-equiv="Content-Security-Policy" content="default-src https://codepush.azurewebsites.net 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *" />

更新config.xml后重新 build/prepare 项目,相关CLI命令如下:

  • Help:显示可用CLI命令的信息。
  • Create:创建Cordova项目并关联项目文件夹和文件。
  • Plateform:管理Cordova项目使用的移动平台。
  • Plugin:管理Cordova插件的安装和卸载。
  • Prepare:从Cordova项目的www文件夹复制web应用内容到项目移动平台项目文件夹中。
  • Compile:把web应用打包成Cordova应用。
  • Build:先执行Prepare命令然后打包web应用。
  • Emulate:在一个或多个移动设备平台的设备模拟器中运行Cordova应用。
  • Run:在一个或多个移动设备中运行Cordova应用。
  • Serve:启动一个服务器加载web内容以便于用浏览器访问。

在app启动的时候调用code-push同步方法

var app = {
    // Application Constructor
    initialize: function() {
        this.bindEvents();
    },
    // Bind Event Listeners
    //
    // Bind any events that are required on startup. Common events are:
    // 'load', 'deviceready', 'offline', and 'online'.
    bindEvents: function() {
        document.addEventListener('deviceready', this.onDeviceReady, false);
    },
    // deviceready Event Handler
    //
    // The scope of 'this' is the event. In order to call the 'receivedEvent'
    // function, we must explicitly call 'app.receivedEvent(...);'
    onDeviceReady: function() {
        /* Invoke sync with the custom options, which enables user interaction.
           For customizing the sync behavior, see SyncOptions in the CodePush documentation. */
        window.codePush.sync(
            function (syncStatus) {
                switch (syncStatus) {
                    // Result (final) statuses
                    case SyncStatus.UPDATE_INSTALLED:
                        app.displayMessage("The update was installed successfully. For InstallMode.ON_NEXT_RESTART, the changes will be visible after application restart. ");
                        break;
                    case SyncStatus.UP_TO_DATE:
                        app.displayMessage("The application is up to date.");
                        break;
                    case SyncStatus.UPDATE_IGNORED:
                        app.displayMessage("The user decided not to install the optional update.");
                        break;
                    case SyncStatus.ERROR:
                        app.displayMessage("An error occured while checking for updates");
                        break;
                    
                    // Intermediate (non final) statuses
                    case SyncStatus.CHECKING_FOR_UPDATE:
                        console.log("Checking for update.");
                        break;
                    case SyncStatus.AWAITING_USER_ACTION:
                        console.log("Alerting user.");
                        break;
                    case SyncStatus.DOWNLOADING_PACKAGE:
                        console.log("Downloading package.");
                        break;
                    case SyncStatus.INSTALLING_UPDATE:
                        console.log("Installing update");
                        break;
                }
            },
            {
                installMode: InstallMode.ON_NEXT_RESTART, updateDialog: true
            },
            function (downloadProgress) {
                console.log("Downloading " + downloadProgress.receivedBytes + " of " + downloadProgress.totalBytes + " bytes.");
            });
        app.receivedEvent('deviceready');
    },
    // Update DOM on a Received Event
    receivedEvent: function(id) {
        var parentElement = document.getElementById(id);
        var listeningElement = parentElement.querySelector('.listening');
        var receivedElement = parentElement.querySelector('.received');

        listeningElement.setAttribute('style', 'display:none;');
        receivedElement.setAttribute('style', 'display:block;');

        console.log('Received Event: ' + id);
    },
      // Displays an alert dialog containing a message.
    displayMessage: function (message) {
        navigator.notification.alert(
            message,
            null,
            'CodePush',
            'OK');
    }
};

app.initialize();

更多用法,请参考codePush.sync

发布更新

先添加应用到code-push,如code-push app add MyApp android cordova,注意配置生成的key.
使用命令code-push deployment list <appName>查看发布状态
如下图可以看到目前android应用"Production"和"Staging"两种部署状态

使用命令code-push release-cordova <appName> <platform> [options]发布更新,如下图所示.

默认发布"Staging"部署状态,也就是开发版.code-push release-cordova MyApp android --des "修改按钮颜色"
发布上传时间依网速快慢大概需要1分钟左右
再次查看发布状态,会看到"Staging"已经有一个版本"v1",但是还没有安装记录

codepush.png

发布完成后再看就有相关信息了,如下:


deployment.png

然后启动app就可以连接到服务器并检查更新,若有则自动更新。
注意:这里有2个版本,主版本app:1.0.0(在config.xml中找到),子版本v1(每次修改www代码提交都会变),若修改主版本则会同时改变。

调试技巧

使用android studio导入工程,然后使用真机联调,可以看到完整的日志输出,方便定位问题。
注意:修改cordova代码,需要同步到platform,执行cordova prepare [platform]cordova prepare,否则打包的app代码不是最新的。

code-push常用命令

//给app在热更新服务器上创建应用
code-push app add <appName> <os> <platform> 

//删除应用
code-push app rm <appName>

//查看热更新服务器上有哪些应用
code-push app list

//发布应用
code-push release-cordova <appName> <platform> [options]
 Options参数:
  --deploymentName, -d ..指定部署的类型.默认"Staging",可以选择"Production"或其他  自定义类型
  --description, --des ..添加描述
  --mandatory, -m .......指定此版本是否为强制更新版本
  例1:发布更新
  code-push release-cordova ionic2_tabs_android android --des ""
  例2:部署"Production"状态的更新,即生产环境的热更新部署使用这句命令
  code-push release-cordova ionic2_tabs_android android  -d "Production" --des ""
  注意:一般生产环境的app是压缩过的,所以在发布正式环境热更新之前,先执行"ionic build --prod"压缩代码
  例3:部署ios应用的更新
  code-push release-cordova ionic2_tabs_ios ios --des ""
  例4:添加-m参数强制更新,code-push插件从服务端下载完代码,会立即自动重启app
  code-push release-cordova ionic2_tabs_android android  -m --des ""

//查看部署状态
code-push deployment list <appName>
  例1:
  code-push deployment list ionic2_tabs_android
  例2:查看部署状态及key值,忘记key就这样找
  code-push deployment list ionic2_tabs_android -k

//清空部署记录
code-push deployment clear <appName> <deploymentName>
如:清空Staging状态的部署记录
code-push deployment clear ionic2_tabs_android Staging

//添加部署状态,默认只有"Staging"和"Production"两中状态
code-push deployment add <appName> [deploymentName]

//删除自定义的部署状态
code-push deployment rm <appName> <deploymentName>

其它

  • 官方文档教程中,要给index.html添加如下<meta>,只是增加安全性
<meta http-equiv="Content-Security-Policy" content="default-src https://codepush.azurewebsites.net 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *" />

安全规则:若从图片库取图片,需要加content: 从远程取则加 http://ip:port

<meta http-equiv="Content-Security-Policy" content="default-src http://192.168.102.253:3000 'self' data: gap: content: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *" />

使用zeptor或ajax

移动端开发一般使用zeptor代替jq,注意白明单规则,即只能请求白明单中的源,如下

<script type="text/javascript" src="js/zepto.js"></script>
...
receivedEvent: function(id) {
        var parentElement = document.getElementById(id);
        var listeningElement = parentElement.querySelector('.listening');
        var receivedElement = parentElement.querySelector('.received');
        var btn = parentElement.querySelector('#btn');

        listeningElement.setAttribute('style', 'display:none;');
        receivedElement.setAttribute('style', 'display:block;');
        var self = this;
        btn.addEventListener('click', function(){
            /*let xhr = new XMLHttpRequest();
            xhr.open('GET', 'http://192.168.102.253:3000/updateCheck?deploymentKey=ufwYib6UBu0tl2NlS2jRxMYsL6DT4ksvOXqog&appVersion=1.0.0&packageHash=066dc68c79dacb16da8dd97a10fa45d681927cd480a7b576b7eb5daa8dc87187&isCompanion=false&label=v15&clientUniqueId=2b42c07102b21ac3', true);
            xhr.onload=function(){
                app.print(xhr.response)
            }
            xhr.send();*/
            $.get('http://192.168.102.253:3000/updateCheck?deploymentKey=ufwYib6UBu0tl2NlS2jRxMYsL6DT4ksvOXqog&appVersion=1.0.0&packageHash=066dc68c79dacb16da8dd97a10fa45d681927cd480a7b576b7eb5daa8dc87187&isCompanion=false&label=v15&clientUniqueId=2b42c07102b21ac3', function(response){
                app.print(response)
            })
        })

        console.log('Received Event: ' + id);
    }

与Vue项目整合

在main.js中配置环境变量控制,如只在生产环境才需要使用cordova,如下

if(process.env.NODE_ENV === "development") {
    new Vue({
        el: '#app',
        store,
        router,
        components: { App },
        template: '<App/>'
      })
  } else {
  // 生产环境需要确保cordova加载完成
  console.log('deviceready start')
  document.addEventListener('deviceready', function() {
    console.log('deviceready end')
    new Vue({
          el: '#app',
          store,
          router,
          components: { App },
          template: '<App/>'
        })
      // new Promise(function(resolve) {
      //   ExpressPlugin.getAppReleaseVersion(function(releaseVersion) {
      //       try {
      //         var len = releaseVersion.indexOf("V");
      //         var numArr = releaseVersion.substr(len + 1).split(".");
      //         if((Number(numArr[0]) >= 4) || (Number(numArr[0]) == 3 && Number(numArr[1]) >= 5)) {
      //           window.showHeader = false;
      //         } else {
      //           window.showHeader = true;
      //         }
      //       } catch(e) {
      //         window.showHeader = true;
      //       }
      //       resolve();
      //     },
      //     function() {
      //       resolve();
      //     })
      // }).then(() => {
      //   new Vue({
      //     el: '#app',
      //     store,
      //     router,
      //     components: { App },
      //     template: '<App/>'
      //   })
      // })
  }, false);
  }

这样开发完成时,可使用npm run build打包vue,再将build之后的内容放到cordova工程www目录运行即可。
注意,打包CLI需配置publicPath为'./',默认为'/'则必须通过server方式访问。

移动端开发调试工具

  • vconsole 该工具方便在移动端查看web的数据请求,即移动端的控制台。
  • android studio 该工具方便整合cordova,因cordova app运行日志都在该工具输出。

下面看下vconsole简单配置,以下是在vue工程中main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import VConsole from 'vconsole'
// 最好加上process.env.NODE_ENV控制,只在移动端才显示
new VConsole()  //会在屏幕右下角生成一个按钮,点击弹出控制台

if(process.env.NODE_ENV === "development") {
  new Vue({
    router,
    render: h => h(App)
  }).$mount('#app')
} else {
  // 生产环境需要确保cordova加载完成
  console.log('deviceready start')
  document.addEventListener('deviceready', function() {
    console.log('deviceready end')
    new Vue({
      router,
      render: h => h(App)
    }).$mount('#app')
  }, false);
}

运行结果如下:


vconsole.png

vconsole2.png
  • 关于苹果 App Store禁止热更新的说明
    目前还没有全面禁止热更新,收到警告邮件的开发者绝大部分是使用了 JS-Patch 或 Rollout 的热更新类库

参考文档:

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