經過React Native動態更新iOS應用

這篇文章一直拖了快1個多月了,一直都找藉口不去完成它。今天終於鐵了心了。開始正題。 
作 iOS 開發的都知道,和 Android 開發不一樣,在提交 App 以後老是要等上至少一個星期的審覈時間(加急審覈除外),而若是在這等待途中發現了什麼 bug,輕的話就等 Apple 審覈完,產品上線後再提交新版本進行等待,嚴重的話可能就只能撤下 App 從新提交,從新等待了。這個問題很困擾人。以後就有了 WaxPath, JSPath 等支持用 Lua, JavaScript 等語言進行 App 動態更新的第三方庫。另外,微軟實現的一個叫 CodePush 的庫則支持 Cordova 和 React Native 的動態部署更新。本文對這些第三方庫都不進行講解,而是經過本身的方式來實現 iOS 上 App 的動態更新。 
咱們知道,React Native 支持的語言是 JavaScript,在打包 App 前,須要對 JavaScript 進行打包。默認狀況下,是經過下面的代碼進行 RCTRootView的初始化的:前端

NSURL *jsCodeLocation;
jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                                      moduleName:@"MyProject"
                                               initialProperties:nil
                                                   launchOptions:launchOptions];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這種是直接讀取本地文件 URL 的方式,而在 Debug 下咱們也看到這樣的讀取方式:react

jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
  • 1
  • 1

若是咱們將這個 URL 換成遠程服務器上的 URL,就能夠動態的讀取最新的 JS Bundle 了。可是實際上這種方式是不可行的,由於遠程加載 JS Bundle 是須要時間的,咱們總不可能讓用戶在那乾等着吧。因而想到另外的方式,經過進入 App 以後進行檢測,若是有新版本的 JS Bundle 的話,則進行新 Bundle 的下載。而這個又能夠經過兩種方式進行處理: 
一、 直接告訴用戶,正在下載新的資源包,並經過 loading 界面讓用戶進行等待; 
二、 不讓用戶察覺,在後頭進行新版本的下載,用戶下次使用 App 的時候加載新的資源包。 
下面我要介紹的是第二種方法。也就是經過後臺更新。爲了讓用戶每次打開 App 能拿到當前最新的 JS Bundle,咱們讓其從 Document 處去讀取 JS Bundle,新版本的 JS Bundle 下載後也一樣存在這個目錄,相似下面代碼:ios

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];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

上面代碼只是進行了 Bundle 的讀取操做,因爲每一個 JS 包須要進行版本的控制,因此,我將版本的檢測放到了 JavaScript 裏面,在 index.ios.js 文件開頭,定義了一個常量const JSBundleVersion = 1.0; //JS 版本號,每次迭代新的 JS 版本則讓其加 0.01。而若是向 APP Store 提交新版本,好比提交了 1.1 版本,則相應的將 JSBundleVersion 設置爲 1.1,爲何這樣作我後面再詳細說明。 
當檢測到有新的 JS 版本時,則通知 Native 進行 JS 的下載和保存,固然也能夠直接在 JS 上進行下載保存。以下:git

getLatestVersion((err, version)=>{
  if (err || !version) {
    return;
  }
  let serverJSVersion = version.jsVersion;
  if (serverJSVersion > JSBundleVersion) {
    //通知 Native 有新的 JS 版本
    NativeNotification.postNotification('HadNewJSBundleVersion');
  }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Native 接到通知後,負責去下載新的 JS bundle,下載成功後並保存到指定路徑,用戶下次打開 App 時直接加載便可。 
這裏有幾個地方能夠優化一下: 
1. 當檢測到有新版本時,進一步判斷用戶當前網絡是不是 wifi 網絡,若是是則通知 native 下載,反之不下載。 
2. 在 1 的條件下,添加一個網絡改變的監測,由於不少狀況下用戶在非 wifi 網絡下打開了 App 可是以後 App 又沒被 kill 掉,這樣就下載不到最新的 bundle 了,因此經過監測網絡的改變,若是網絡變爲 wifi 而且有新版本,則下載。因而代碼大概以下:github

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;
          }
        });
      }
    });
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

JS 代碼基本就這些,接下來看看在 native 上須要作哪些操做。 
首先,要接收到下載 JS bundle 的通知,固然是要先註冊爲觀察者了。數據庫

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  //...
  [NativeNotificationManager addObserver:self selector:@selector(hadNewJSBundleVersion:) name:@"HadNewJSBundleVersion" object:nil];
  //...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

hadNewJSBundleVersion 方法裏面根據需求下載 JS bundle, 爲了能保證下載的包完整,咱們能夠同時準備一份 JS bundle 的 md5 碼,用於校驗。以下:react-native

- (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));
  }];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

downloadCodeFrom: md5URLString: toURL:completeHandler 方法就賦值下載,檢驗和保存操做。 
(注意這句代碼: 
NSString *base = [@"http://domain/" stringByAppendingString:version];,這跟咱們遠程服務器存儲文件的路徑有關,我會在後面進行說明)。服務器

- (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, NSURLResponse *response, 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);
        }
      }
    }];
  }];
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

到這裏,檢測更新,下載新 bundle 的操做就算完成了。 
下面,來完成文件讀取並初始化 RCTRootView 的操做。在 AppDelegate 內咱們經過調用自定義方法來得到 RCTRootView ,以下:網絡

- (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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

getRootViewModuleName:launchOptions方法負責處理一些咱們須要的邏輯(如:根據是否在Debug模式下,是否在模擬器上等不一樣狀態初始化不一樣的rootView),最終返回一個 RCTRootView 對象。app

- (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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

這裏,咱們主要看 production 部分。上面其實已經貼出一次這段代碼,在這以前我先說下咱們存放和讀取 JS 的路徑。首先在 Documents 內建立一個目錄叫 JSBundle,而後根據當前 App 的版本號再建立一個和版本號相同名字的目錄(如:1.0, 1.1),最後路徑大概這樣:…/Documents/JSBundle/1.0/main.jsbundle

下面講解下思路:首先判斷咱們的目標路徑是否存在 JS bundle(用戶首次安裝或更新版本後該路徑是不存在 JS 的),若是不存在,則將項目上的 JS bundle 拷貝到該路徑下。能夠看到在拷貝以前調用了 resetJSBundlePath 方法,該方法的做用是將這個路徑的其餘文件清除,這樣作的緣由是:從舊版本更新到新版本(這裏指的是App發佈的新版本)後,以前舊的 JS bundle 還存在着。爲了保險起見,得判斷一下文件是否拷貝成功了,若是沒成功,則將讀取路徑設置成項目上的 JS bundle 路徑。最後,建立 bridge,建立 rootView 並返回。 
這樣,動態更新的操做就完成了。還有一件事,上面說到的代碼 
NSString *base = [@"http://domain/" stringByAppendingString:version]; 
爲何要這樣作呢?緣由很簡單:爲了兼容不一樣版本。舉個例子:你發佈了1.0版本後,下載路徑是 http://domain/1.0/main.jsbundle,過了一段時間你又發佈了1.1 版本, 這時下載路徑是 http://domain/1.1/main.jsbundle,1.1版本中,你可能在 native 上添加了其餘文件,或者是更新了 react-native 的版本,這時,若是讓仍是 1.0 版本的用戶下載了 1.1 的 JS bundle,問題就來了,你懂得。這只是我我的的解決方案,固然,這些其實徹底能夠放到服務器端去處理的,服務器端提供一個接口,咱們能夠經過傳遞當前 App 的版本號,服務器判斷是否有新的 JS bundle 後返回下載路徑,而後前端再進行下載存儲。至於用什麼方法你們以爲哪一種方便就用哪一種吧。

最後,說下目前我將 JS bundle 遠程存放的服務器和版本檢測所用的方法。 
1. 文件我存放在了阿里雲上,它會根據你存放的位置給你生成一個目標URL; 
2. 版本檢測個人方法是:在遠程數據庫上建立一個表格,字段分別有

forceUpdate newestVersion nativeVersion JSVersion platform message
false 1.0 1.0 1.0 iOS 有新版本提示

根據字段名稱基本都能明白了,這裏就不囉嗦了。

說了這麼多,總結一下步驟: 
- JS 端檢測是否有新的 JS bundle,有則通知 native 下載 
- native 下載完 JS 後進行 md5 的校驗,並存儲 
- 每次打開 App 檢測要讀取的路徑是否有 JS 
- 有則直接讀取,沒有則進行拷貝

這裏,我寫了個Demo,可供參考,若有任何問題,歡迎你們進行討論。

 

原文:http://blog.csdn.net/linshaolie/article/details/50961955

相關文章
相關標籤/搜索