通过React Native动态更新iOS应用
本文属原创,转载请注明出处,谢谢! NSURL *jsCodeLocation;
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"MyProject"
initialProperties:nil
launchOptions:launchOptions];
这种是直接读取本地文件 URL 的方式,而在 Debug 下我们也看到这样的读取方式: jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
如果我们将这个 URL 换成远程服务器上的 URL,就可以动态的读取最新的 JS Bundle 了。但是实际上这种方式是不可行的,因为远程加载 JS Bundle 是需要时间的,我们总不可能让用户在那干等着吧。于是想到另外的方式,通过进入 App 之后进行检测,如果有新版本的 JS Bundle 的话,则进行新 Bundle 的下载。而这个又可以通过两种方式进行处理: NSURL *jsCodeLocation;
jsCodeLocation = [self URLForCodeInDocumentsDirectory];
if (![self hasCodeInDocumentsDirectory]) {
//从 Document 上读取 JS Bundle
BOOL copyResult = [self copyBundleFileToURL:jsCodeLocation];
if (!copyResult) {
//拷贝失败,从 main Bundle 上读取
jsCodeLocation = [self URLForCodeInBundle];
}
}
RCTBridge *bridge = [self createBridgeWithBundleURL:jsCodeLocation];
rootView = [self createRootViewWithBridge:bridge];
上面代码只是进行了 Bundle 的读取操作,由于每个 JS 包需要进行版本的控制,所以,我将版本的检测放到了 JavaScript 里面,在 index.ios.js 文件开头,定义了一个常量 getLatestVersion((err,version)=>{ if (err || !version) { return; } let serverJSVersion = version.jsVersion; if (serverJSVersion > JSBundleVersion) { //通知 Native 有新的 JS 版本 NativeNotification.postNotification('HadNewJSBundleVersion'); } });
Native 接到通知后,负责去下载新的 JS bundle,下载成功后并保存到指定路径,用户下次打开 App 时直接加载即可。 const JSBundleVersion = 1.0;
let hadDownloadJSBundle = true;
//.....
componentDidMount() {
NetInfo.addEventListener('change',(reachability) => { if (reachability == 'wifi' && hadDownloadJSBundle == false) { hadDownloadJSBundle = true; NativeNotification.postNotification('HadNewJSBundleVersion'); } }); this._checkUpdate(); } _checkUpdate() { getLatestVersion((err,version)=>{ if (err || !version) { return; } let serverJSVersion = version.jsVersion; if (serverJSVersion > JSBundleVersion) { //通知 Native 有新的 JS 版本 isWifi((wifi) => { if (wifi) { hadDownloadJSBundle = true; NativeNotification.postNotification('HadNewJSBundleVersion'); } else { hadDownloadJSBundle = false; } }); } }); }
JS 代码基本就这些,接下来看看在 native 上需要做哪些操作。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
//...
[NativeNotificationManager addObserver:self selector:@selector(hadNewJSBundleVersion:) name:@"HadNewJSBundleVersion" object:nil];
//...
}
- (void)hadNewJSBundleVersion:(NSNotification *)notification {
//根据需求设置下载地址
NSString *version = APP_VERSION;
NSString *base = [@"http://domain/" stringByAppendingString:version];
NSString *uRLStr = [base stringByAppendingString:@"/main.jsbundle"];
NSString *md5URLStr = [base stringByAppendingString:@"/mainMd5.jsbundle"];
//存储路径为每次打开 App 要加载 JS 的路径
NSURL *dstURL = [self URLForCodeInDocumentsDirectory];
[self downloadCodeFrom:uRLStr md5URLString:md5URLStr toURL:dstURL completeHandler:^(BOOL result) {
NSLog(@"finish: %@",@(result));
}];
}
- (void)downloadCodeFrom:(NSString *)srcURLString
md5URLString:(NSString *)md5URLString
toURL:(NSURL *)dstURL
completeHandler:(CompletionBlock)complete {
//下载MD5数据
[SLNetworkManager sendWithRequestMethor:(RequestMethodGET) URLString:md5URLString parameters:nil error:nil completionHandler:^(NSData *md5Data,NSURLResponse *response,NSError *connectionError) {
if (connectionError && md5Data.length < 32) {
return;
}
//下载JS
[SLNetworkManager sendWithRequestMethor:(RequestMethodGET) URLString:srcURLString parameters:nil error:nil completionHandler:^(NSData *data,NSError *connectionError) {
if (connectionError || data.length < 10000) {
return;
}
//MD5 校验
NSString *md5String = [[NSString alloc] initWithData:md5Data encoding:NSUTF8StringEncoding];
if(checkMD5(data,md5String)) {
//校验成功,写入文件
NSError *error = nil;
[data writeToURL:dstURL options:(NSDataWritingAtomic) error:&error];
if (error) {
!complete ?: complete(NO);
//写入失败,删除
[SLFileManager deleteFileWithURL:dstURL error:nil];
} else {
!complete ?: complete(YES);
}
}
}];
}];
}
到这里,检测更新,下载新 bundle 的操作就算完成了。 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTRootView *rootView = [self getRootViewModuleName:@"DynamicUpdateDemo" launchOptions:launchOptions];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (RCTRootView *)getRootViewModuleName:(NSString *)moduleName
launchOptions:(NSDictionary *)launchOptions {
NSURL *jsCodeLocation = nil;
RCTRootView *rootView = nil;
#if DEBUG
#if TARGET_OS_SIMULATOR
//debug simulator
jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
//debug device
NSString *serverIP = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"SERVER_IP"];
NSString *jsCodeUrlString = [NSString stringWithFormat:@"http://%@:8081/index.ios.bundle?platform=ios&dev=true",serverIP];
NSString *jsBundleUrlString = [jsCodeUrlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
jsCodeLocation = [NSURL URLWithString:jsBundleUrlString];
#endif
rootView = [self createRootViewWithURL:jsCodeLocation moduleName:moduleName launchOptions:launchOptions];
#else
//production
jsCodeLocation = [self URLForCodeInDocumentsDirectory];
if (![self hasCodeInDocumentsDirectory]) {
[self resetJSBundlePath];
BOOL copyResult = [self copyBundleFileToURL:jsCodeLocation];
if (!copyResult) {
jsCodeLocation = [self URLForCodeInBundle];
}
}
RCTBridge *bridge = [self createBridgeWithBundleURL:jsCodeLocation];
rootView = [self createRootViewWithModuleName:moduleName bridge:bridge];
#endif
#if 0 && DEBUG
jsCodeLocation = [self URLForCodeInDocumentsDirectory];
if (![self hasCodeInDocumentsDirectory]) {
[self resetJSBundlePath];
BOOL copyResult = [self copyBundleFileToURL:jsCodeLocation];
if (!copyResult) {
jsCodeLocation = [self URLForCodeInBundle];
}
}
RCTBridge *bridge = [self createBridgeWithBundleURL:jsCodeLocation];
rootView = [self createRootViewWithModuleName:moduleName bridge:bridge];
#endif
return rootView;
}
这里,我们主要看 production 部分。上面其实已经贴出一次这段代码,在这之前我先说下我们存放和读取 JS 的路径。首先在 Documents 内创建一个目录叫 JSBundle,然后根据当前 App 的版本号再创建一个和版本号相同名字的目录(如:1.0, 1.1),最后路径大概这样:…/Documents/JSBundle/1.0/main.jsbundle 下面讲解下思路:首先判断我们的目标路径是否存在 JS bundle(用户首次安装或更新版本后该路径是不存在 JS 的),如果不存在,则将项目上的 JS bundle 拷贝到该路径下。可以看到在拷贝之前调用了 最后,说下目前我将 JS bundle 远程存放的服务器和版本检测所用的方法。
根据字段名称基本都能明白了,这里就不啰嗦了。 说了这么多,总结一下步骤: 这里,我写了个Demo,可供参考,如有任何问题,欢迎大家进行讨论。 本文属原创,转载请注明出处,谢谢! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |