一.背景
近期接到项目即将在下个版本接入热更功能,提前进行预热学习。
目前项目的架构: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>-->
网友评论