美文网首页JavaScript
初识Cordova热更插件-cordova-hot-code-p

初识Cordova热更插件-cordova-hot-code-p

作者: Charlie_超仔 | 来源:发表于2018-09-14 11:50 被阅读256次

一.背景

近期接到项目即将在下个版本接入热更功能,提前进行预热学习。
目前项目的架构:vue.js+cordova+OC原生,项目侧重点为js开发。根据初步评估,热更功能是可以实现的,上架审核也不会受到阻拦(本人在3个月前尝试在ReactNative混合开发项目中接入热更模块,产品成功上架没有因为热更拒审)。
另:本文主要针对iOS平台下的cordova项目进行讲解。
【一个小tips:js、html属于标记语言,无需通过编译器进行编码工作即可在web下面进行加载,所以可以进行热更修改相关文件。但是诸如C、JAVA、C++最终都是需要通过编译器转译为机器码才能在设备上跑起来,所以不能通过更新源码文件的方式进行热更。】

二.热更功能介绍

参考之前的ReactNative项目的混合开发经验,热更功能的实现原理可以归结为以下:

1、需要搭建相关服务器,存放相关的项目资源(诸如JS文件等)。
2、需要配置客户端,用于下载服务器相关资源。
3、需要有版本匹配或内容匹配。
4、客户端采取策略如下:

1.查询本地是否有下载了相关版本资源,如果没有则下载
2.如果本地已经存在了相关资源,则尝试从服务器获取最新的版本号,然后与旧版本匹配
3.如果版本相同,则加载本地,如果不同,则从服务器下载相关资源
4.下载成功以后,更新到本地版本目录,更新本地最新版本标识。
5.根据加载策略[诸如:即时加载、下次打开后加载]更新页面。
6.旧版本回滚等

三.cordova-hot-code-push的使用

1.前提

需要先安装NPM
创建了cordova工程,并添加了安卓或iOS平台,详情查看教程
尝试在终端cd 到项目根目录/platforms/ios

2.安装热更插件(cordova-hot-code-push)

命令如下:

cordova plugin add cordova-hot-code-push-plugin

3.安装客户端

命令如下,如果不需要进行本地调试的话,可以忽略此步骤

npm install -g cordova-hot-code-push-cli

4.添加本地服务器(用于本地调试,建议上手热更功能后把它remove掉)

本地服务器主要用于模拟远程服务器的,方便前端热更接入调试。
命令如下

cordova plugin add cordova-hot-code-push-local-dev-addon

5.配置远程服务器路径

项目根目录/platforms/ios/ios项目工程目录/config.xml末尾加入以下代码:

<chcp>
     <config-file url="https://xxxxx/chcp.json"/>
</chcp>

tips:xxxxx是服务器地址,至于这里配置的用处,可以参考后面的cordova-hot-code-push-plugin原理介绍。

6.额外提醒

以上1-5步骤介是在前端进行,而后台服务器的配置稍后再说。

四.调试

1.打开本地模拟服务器

cd到项目根目录/platforms/ios下执行以下命令开启本地服务器

cordova-hcp server

跑起来后,你讲看到终端输出诸如以下的信息:

cordova-hcp local server available at: http://localhost:31284
cordova-hcp public server available at: https://f76dddd3.ngrok.io

解释过来就是:
1.本地服务器入口是:http://localhost:31284
2.对外暴露的域名地址是:https://f76dddd3.ngrok.io
此时你可以将https://f76dddd3.ngrok.io/index.html拷贝到浏览器打开即可看见你的项目内容了。
3.你可以发现,在www文件夹下生成了两个文件:chcp.json和chcp.manifest。其中chcp.json是本地服务器生成的一个版本配置,而chcp.manifest则是文件资源的哈希表,即服务器可以将该哈希表去判别文件是否有更改,如果有更改可以自动重载。
假如你更改了www下的文件,则服务器会自动重载资源,并输出了一下打印信息:File changed: xxxx Build 2018.09.13-11.48.29 created in xxxxxx Should trigger reload for build: 2018.09.13-11.48.29

2.run

直接使用Xcode打开项目根目录/platforms/ios下的iOS工程,然后run一下

3.一些注意

【1】假如在Xcode中build过程中出现了错误:Generator has been renamed to Iterator,则需要直接点击Fix就好了。
【2】如果在Xcode的build过程中出现了错误:某些swift类文件无法找到定义。因为引入cordova-hot-code-push-local-dev-addon的时候,有一部分swift源文件,所以要将其通过Birging的方式桥接到oc里,建议参考文章:OC嵌入swift。检查是否已经在项目中import了中间生成的桥接头文件。

五.源码分析

1.解释config.xml

在上问的3.5中提到设置服务器的配置文件路径,而在HCPPlugin的第一步就是读取config.xml中的<chcp></chcp>之间的内容,然后解释出服务器配置文件的路径、是否自动下载、是否自动安装、原生的版本号。详细配置属性值请查看HCPXmlTags.m文件查看

NSString *const kHCPMainXmlTag = @"chcp";

// Keys for processing application config location on the server
NSString *const kHCPConfigFileXmlTag = @"config-file";
NSString *const kHCPConfigFileUrlXmlAttribute = @"url";

// Keys for processing auto download options
NSString *const kHCPAutoDownloadXmlTag = @"auto-download";
NSString *const kHCPAutoDownloadEnabledXmlAttribute = @"enabled";

// Keys for processing auto install options
NSString *const kHCPAutoInstallXmlTag = @"auto-install";
NSString *const kHCPAutoInstallEnabledXmlAttribute = @"enabled";

// Keys for processing native interface version
NSString *const kHCPNativeInterfaceXmlTag = @"native-interface";
NSString *const kHCPNativeInterfaceVersionXmlAttribute = @"version";

2.获取本地版本信息

本地版本信息包括:app构建版本号、www文件路径、前一个本地版本号、当前本地版本号、即将要安装的新版本号。想请请查看文件HCPPluginConfig.h

/**
 *  Model for plugin preferences, that can be changed during runtime.
 *  Using this you can disable/enable updates download and installation,
 *  and even change application config file url (by default it is set in config.xml).
 *
 *  Also, it stores current build version of the application,
 *  so we could determine if it has been updated through the App Store.
 */
@interface HCPPluginInternalPreferences : NSObject<HCPJsonConvertable>

/**
 *  Build version of the app which was detected on the last launch.
 *  Using it we can determine if application has been updated through the App Store.
 */
@property (nonatomic, strong) NSString *appBuildVersion;

/**
 *  Flag to check, if www folder from the bundle is installed on external storage.
 */
@property (nonatomic, getter=isWwwFolderInstalled) BOOL wwwFolderInstalled;

/**
 *  Previous version of the content. Can be used to rollback.
 */
@property (nonatomic, strong) NSString *previousReleaseVersionName;

/**
 *  Current version of the content, that is displayed to the user.
 */
@property (nonatomic, strong) NSString *currentReleaseVersionName;

/**
 *  Name of the new version, that was loaded and ready to be installed. 
 */
@property (nonatomic, strong) NSString *readyForInstallationReleaseVersionName;

3.更新webview的wwwFolderName

HCPPlugin.m文件中有一句这样的代码:

// rewrite starting page www folder path: should load from external storage
    if ([self.viewController isKindOfClass:[CDVViewController class]]) {
        ((CDVViewController *)self.viewController).wwwFolderName = _filesStructure.wwwFolder.absoluteString;
    } else {
        NSLog(@"HotCodePushError: Can't make starting page to be from external storage. Main controller should be of type CDVViewController.");
    }

该代码主要用于重置webview的加载文件的主目录路径。
但是HCPPlugin.m怎么选择www的路径的,可以参考以下思路:

1.检测是否有有效的本地www文件路径
2.如果有有效的本地www文件夹,则使用该路径,否则默认为main.budle的主路径中的www文件夹路径。

4.安装本地版本

如果第一次安装,HCPPlugin会自动生成当前版本号对应的文件夹路径,然后会进一步把main.budle的www文件夹的内容全部拷贝到该路径下。

5.匹配更新

HCPPlugin.m中在jsInitPlugin函数的调用中会尝试进行更新操作。不过在操作下载前,先通过config.xml中的config-file路径获取远程服务器的chcp.json文件,从中获取服务器最新版本,然后从本地www路径中的chcp.json中获取本地版本号,两者进行匹配是否相同,如果不相同,则匹配chcp.manifest中的hash值,发现不同的hash值后,将对应地从服务器下载替换本地文件。源码如下:
【获取服务器chcp.json和chcp.manifest文件】

- (BOOL)_fetchUpdate:(NSString *)callbackId withOptions:(HCPFetchUpdateOptions *)options {
    if (!_isPluginReadyForWork) {
        return NO;
    }
    
    if (!options && self.defaultFetchUpdateOptions) {
        options = self.defaultFetchUpdateOptions;
    }
    
    HCPUpdateRequest *request = [[HCPUpdateRequest alloc] init];
    request.configURL = options.configFileURL ? options.configFileURL : _pluginXmlConfig.configUrl;
    request.requestHeaders = options.requestHeaders;
    request.currentWebVersion = _pluginInternalPrefs.currentReleaseVersionName;
    request.currentNativeVersion = _pluginXmlConfig.nativeInterfaceVersion;
    
    NSError *error = nil;
    [[HCPUpdateLoader sharedInstance] executeDownloadRequest:request error:&error];
    
    if (error) {
        if (callbackId) {
            CDVPluginResult *errorResult = [CDVPluginResult pluginResultWithActionName:kHCPUpdateDownloadErrorEvent
                                                                     applicationConfig:nil
                                                                                 error:error];
            [self.commandDelegate sendPluginResult:errorResult callbackId:callbackId];
        }
        
        return NO;
    }
    
    if (callbackId) {
        _downloadCallback = callbackId;
    }
    
    return YES;
}

【下载的核心代码】

- (void)runWithComplitionBlock:(void (^)(void))updateLoaderComplitionBlock {
    _complitionBlock = updateLoaderComplitionBlock;
    
    // initialize before the run
    NSError *error = nil;
    if (![self loadLocalConfigs:&error]) {
        [self notifyWithError:error applicationConfig:nil];
        return;
    }
    
    HCPDataDownloader *configDownloader = [[HCPDataDownloader alloc] init];
    
    // download new application config
    [configDownloader downloadDataFromUrl:_configURL requestHeaders:_requestHeaders completionBlock:^(NSData *data, NSError *error) {
        HCPApplicationConfig *newAppConfig = [self getApplicationConfigFromData:data error:&error];
        if (newAppConfig == nil) {
            [self notifyWithError:[NSError errorWithCode:kHCPFailedToDownloadApplicationConfigErrorCode descriptionFromError:error]
                applicationConfig:nil];
            return;
        }
        
        // check if new version is available
        if ([newAppConfig.contentConfig.releaseVersion isEqualToString:_oldAppConfig.contentConfig.releaseVersion]) {
            [self notifyNothingToUpdate:newAppConfig];
            return;
        }
        
        // check if current native version supports new content
        if (newAppConfig.contentConfig.minimumNativeVersion > _nativeInterfaceVersion) {
            [self notifyWithError:[NSError errorWithCode:kHCPApplicationBuildVersionTooLowErrorCode
                                             description:@"Application build version is too low for this update"]
                applicationConfig:newAppConfig];
            return;
        }
        
        // download new content manifest
        NSURL *manifestFileURL = [newAppConfig.contentConfig.contentURL URLByAppendingPathComponent:_pluginFiles.manifestFileName];
        [configDownloader downloadDataFromUrl:manifestFileURL requestHeaders:_requestHeaders completionBlock:^(NSData *data, NSError *error) {
            HCPContentManifest *newManifest = [self getManifestConfigFromData:data error:&error];
            if (newManifest == nil) {
                [self notifyWithError:[NSError errorWithCode:kHCPFailedToDownloadContentManifestErrorCode
                                        descriptionFromError:error]
                    applicationConfig:newAppConfig];
                return;
            }
            
            // compare manifests to find out if anything has changed since the last update
            HCPManifestDiff *manifestDiff = [_oldManifest calculateDifference:newManifest];
            if (manifestDiff.isEmpty) {
                [_manifestStorage store:newManifest inFolder:_pluginFiles.wwwFolder];
                [_appConfigStorage store:newAppConfig inFolder:_pluginFiles.wwwFolder];
                [self notifyNothingToUpdate:newAppConfig];
                return;
            }
            
            // switch file structure to new release
            _pluginFiles = [[HCPFilesStructure alloc] initWithReleaseVersion:newAppConfig.contentConfig.releaseVersion];
            
            // create new download folder
            [self createNewReleaseDownloadFolder:_pluginFiles.downloadFolder];
            
            // if there is anything to load - do that
            NSArray *updatedFiles = manifestDiff.updateFileList;
            if (updatedFiles.count > 0) {
                [self downloadUpdatedFiles:updatedFiles appConfig:newAppConfig manifest:newManifest];
                return;
            }
            
            // otherwise - update holds only files for deletion;
            // just save new configs and notify subscribers about success
            [_manifestStorage store:newManifest inFolder:_pluginFiles.downloadFolder];
            [_appConfigStorage store:newAppConfig inFolder:_pluginFiles.downloadFolder];
            
            [self notifyUpdateDownloadSuccess:newAppConfig];
        }];
    }];
}

留意一下这句,用于匹配本地和服务器版本

 // check if new version is available
        if ([newAppConfig.contentConfig.releaseVersion isEqualToString:_oldAppConfig.contentConfig.releaseVersion]) {
            [self notifyNothingToUpdate:newAppConfig];
            return;
        }

这句是下载新的资源文件

 // if there is anything to load - do that
            NSArray *updatedFiles = manifestDiff.updateFileList;
            if (updatedFiles.count > 0) {
                [self downloadUpdatedFiles:updatedFiles appConfig:newAppConfig manifest:newManifest];
                return;
            }

注意:如果想让用户在app store升级版本后才能从服务器加载新的资源,则需要在服务器的chcp.json文件中加入min_native_interface字段来限制app的线上最低版本要求

六.线上服务器配置

配置线上热更服务器时,需要在服务器新增一个叫chcp.json的文件,文件的配置key参考以下

#pragma mark Json keys declaration

static NSString *const RELEASE_VERSION_JSON_KEY = @"release";
static NSString *const MINIMUM_NATIVE_VERSION_JSON_KEY = @"min_native_interface";
static NSString *const UPDATE_TIME_JSON_KEY = @"update";
static NSString *const CONTENT_URL_JSON_KEY = @"content_url";

而对于update的文件加载到webview的时机,则根据以下值

static NSString *const UPDATE_TIME_NOW = @"now";//下载完之后立马刷新
static NSString *const UPDATE_TIME_ON_START = @"start";//启动app的时候再刷新
static NSString *const UPDATE_TIME_ON_RESUME = @"resume";//app在恢复的时候刷新,诸如在后台回到前台

当然,你需要把你要更新的文件都拷贝一份并跟chcp.json放在同一目录下,个人建议可以直接通过线下使用本地服务器运行后得到的含有chcp.json和chcp.manifest的www文件夹拷贝一份放到服务器就OK了,不过别忘了配置一下项目中config.xml的config-file。

写在最后

1.使用本地服务器插件之后,在js更新之后,你无须每次都要再跑一下xcode,可以选择把app重启即可刷新。
2.由于在执行第3.4之后,config.xml会把服务器插件写入其中,为了避免占用网络资源建议把它注销掉,然后在打包上线app之前,把本地服务器插件去掉即可。

<!--    <feature name="HotCodePushLocalDevMode">-->
<!--        <param name="ios-package" value="HCPLDPlugin" />-->
<!--        <param name="onload" value="true" />-->
<!--    </feature>-->

如有问题,敬请留言.

相关文章

网友评论

    本文标题:初识Cordova热更插件-cordova-hot-code-p

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