React NativeでURL をパースする

# 経緯
React Naviveを使ったアプリで、Deep LinkやPush通知のペイロードに格納されたURLを解析したいのですが、有効な方法が見つからないのでiOS/Androidデバイスの（ネイティブ）ライブラリを使いそれらをパースするコードを実装しました。

## 方針
iOS, AndroidともにURLを解析するための似たAPIがあるので単純にそれらを呼び出します。
ネイティブコードを呼び出すので、この機能の戻り値はPromise経由で返すようにしています。
これらのAPIではクエリーパラメータについて、＆で結合された文字列（式）だけが戻ってくるので、JS側でキーと値を抽出しています。
例外処理やエラー処理は適当です（状況に合わせて書き換えて下さい）。

## 完全なコード
こちらにあります。
https://github.com/flipfrog/react-native-url-parse

## API
こんな感じで使用します。

```JavaScript
import URLParse from './URLParse';
:

  async componentDidMount(): void {
    const url = await URLParse.parse(this.state.urlSpec);

    this.setState({
      protocol: url.protocol, // httpsとかのプロトコルスキーマ
      host: url.host, // ホスト名
      port: url.port, // ポート番号
      path: url.path, // パス
      query: url.query, // クエリーパラメータ
      ref: url.ref, // インデックス
      queryMap: url.queryMap, // クエリーパラメータのマップオブジェクト
    });
  }
```

```JavaScript:URLParse.js
import {NativeModules} from 'react-native';

export default class URLParse {
    static async parse(urlSpec: string) {
        const url = await NativeModules.RNURLParseModule.parse(urlSpec);
        if (url && url.query) {
            const expressions = url.query.split('&');
            const queryMap = {};
            for (let expression of expressions) {
                const values = expression.split('=');
                queryMap[values[0]] = (values[1] ? values[1] : null);
            }
            url['queryMap'] =  queryMap;
        }
        return url;
    }
}
```

## iOS
iOSは、.hと.mファイル（Objective-Cを使いました）を作成するだけです。

```Objective-C:RNURLParseModule.h
#if __has_include(<React/RCTBridgeModule.h>)
#import <React/RCTBridgeModule.h>
#else
#import "RCTBridgeModule.h"
#endif

@interface RNURLParseModule : NSObject <RCTBridgeModule>
@end
```

```Objective-C:RNURLParseModule.m
#import "RNURLParseModule.h"

#if __has_include("RCTUtils.h")
#import "RCTUtils.h"
#else
#import <React/RCTUtils.h>
#endif

#import <Foundation/Foundation.h>

@implementation RNURLParseModule {
}

RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(parse:(NSString *)urlSpec
                  parseWithResolver:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject) {
  NSURL *url = [NSURL URLWithString:urlSpec];
  if (url == NULL) {
    NSDictionary *errorDic = @{
                               NSLocalizedDescriptionKey:@"Parse error",
                               NSLocalizedRecoverySuggestionErrorKey:@"Confirm parameter urlSpec."
                               };
    NSError *error = [[NSError alloc] initWithDomain:@"org.reactjs.native.example.URLParseSample.parse"
                                                code:-1 userInfo:errorDic];
    reject(@"Parse error", @"Parse error", error);
  } else {
    NSDictionary *info = @{
                           @"protocol": [url scheme],
                           @"host": [url host],
                           @"port": [url port],
                           @"path": [url path],
                           @"query": [url query],
                           @"ref": [url fragment]
                           };
    resolve(info);
  }
}
@end
```

## Android
Androidは、モジュールとパッケージ定義を作成して、MainApplication.javaでインスタンス化したパッケージを返すようにします。

```Java:RNURLParseModule,java
package com.urlparsesample.extension;

import java.net.URL;
import com.facebook.react.bridge.Promise;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import java.util.HashMap;
import java.util.Map;

// package private class
class RNURLParseModule extends ReactContextBaseJavaModule {
    private static final String E_URL_PARSE_ERROR = "URL Parse error";

    RNURLParseModule(ReactApplicationContext context) {
        super(context);
    }

    @Override
    public String getName() {
        return "RNURLParseModule";
    }

    @Override
    public Map<String, Object> getConstants() {
        final Map<String, Object> constants = new HashMap<>();
        constants.put("IsAndroid", true);
        return constants;
    }

    @ReactMethod
    public void parse(final String urlSpec, Promise promise) {
        WritableMap info = Arguments.createMap();
        try {
            URL url = new URL(urlSpec);
            info.putString("protocol", url.getProtocol());
            info.putString("host", url.getHost());
            info.putString("path", url.getPath());
            info.putInt("port", url.getPort());
            info.putString("query", url.getQuery());
            info.putString("ref", url.getRef());
            promise.resolve(info);
        } catch (Exception e) {
            promise.reject(E_URL_PARSE_ERROR, e);
        }
    }
}
```

```Java:RNURLParsePackage.java
package com.urlparsesample.extension;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

public class RNURLParsePackage implements ReactPackage {

    @Override
    public List<NativeModule> createNativeModules (ReactApplicationContext context) {
        List<NativeModule> modules = new ArrayList<>();
        modules.add(new RNURLParseModule(context));
        return modules;
    }

    // Deprecated RN 0.47
    // @Override
    public List<Class<? extends JavaScriptModule>> createJSModules() {
        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext context) {
        return Collections.emptyList();
    }
}
```

```Java:MainApplication.java(抜粋)

    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
              new RNURLParsePackage() // ここに追加します
      );
    }

```

## 実行結果
先に記載したGitHubのコードを実行すると下記のように結果を表示します。

![ScreenShot_S.png](https://qiita-image-store.s3.amazonaws.com/0/251588/8870f63b-50b7-a657-98fc-46ed7d015ead.png)