ReactNative WebView onNavigationStateChange等方法不響應

一、問題

RN項目中因爲多個頁面使用h5來實現,因此需要使用到WebView,我這裏使用的是官方社區維護的WebView

在使用中發現在h5中跳轉到二級頁面時,onNavigationStateChangeonLoadonLoadEndonLoadStart等方法都不響應,因此無法獲取到頁面路由狀態,Android端的物理返回鍵無法處理是應該webview回退還是頁面關閉。

二、原因

經過各種檢查,發現是因爲h5頁面使用React單頁面實現,而單頁面實現不會觸發原生WebView中WebViewClient的一系列響應,代碼如下:

  @Override
    public void onPageFinished(WebView webView, String url) {
      super.onPageFinished(webView, url);
      if (!mLastLoadFailed) {
        RNCWebView reactWebView = (RNCWebView) webView;

        reactWebView.callInjectedJavaScript();

        emitFinishEvent(webView, url);
      }
    }

    @Override
    public void onPageStarted(WebView webView, String url, Bitmap favicon) {
      super.onPageStarted(webView, url, favicon);
      mLastLoadFailed = false;
      dispatchEvent(
        webView,
        new TopLoadingStartEvent(
          webView.getId(),
          createWebViewEvent(webView, url)));
    }

onPageStarted爲例,當頁面跳轉時,Android原生WebView會調用onPageStarteddispatchEvent方法則是向js代碼中發送事件,createWebViewEvent將WebView中獲取到的信息封裝起來,其中就包括我們操作路由邏輯的canGoBack,如下:

protected WritableMap createWebViewEvent(WebView webView, String url) {
      WritableMap event = Arguments.createMap();
      event.putDouble("target", webView.getId());
      // Don't use webView.getUrl() here, the URL isn't updated to the new value yet in callbacks
      // like onPageFinished
      event.putString("url", url);
      event.putBoolean("loading", !mLastLoadFailed && webView.getProgress() != 100);
      event.putString("title", webView.getTitle());
      event.putBoolean("canGoBack", webView.canGoBack());
      event.putBoolean("canGoForward", webView.canGoForward());
      return event;
    }

具體發送的事件被封裝成TopLoadingStartEvent,代碼如下:

class TopLoadingStartEvent(viewId: Int, private val mEventData: WritableMap) :
  Event<TopLoadingStartEvent>(viewId) {
  companion object {
    const val EVENT_NAME = "topLoadingStart"
  }

  override fun getEventName(): String = EVENT_NAME

  override fun canCoalesce(): Boolean = false

  override fun getCoalescingKey(): Short = 0

  override fun dispatch(rctEventEmitter: RCTEventEmitter) =
    rctEventEmitter.receiveEvent(viewTag, eventName, mEventData)

}

通過getEventName方法設置事件名稱爲 topLoadingStart ,那麼接下來就需要原生與js通過名字映射來將事件對應起來。

在ReactNative源碼中已經給我們預定義了一些事件映射,代碼如下:

 /* package */ static Map getBubblingEventTypeConstants() {
    return MapBuilder.builder()
        .put(
            "topChange",
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onChange", "captured", "onChangeCapture")))
        .put(
            "topSelect",
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onSelect", "captured", "onSelectCapture")))
        .put(
            TouchEventType.getJSEventName(TouchEventType.START),
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onTouchStart", "captured", "onTouchStartCapture")))
        .put(
            TouchEventType.getJSEventName(TouchEventType.MOVE),
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onTouchMove", "captured", "onTouchMoveCapture")))
        .put(
            TouchEventType.getJSEventName(TouchEventType.END),
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onTouchEnd", "captured", "onTouchEndCapture")))
        .put(
            TouchEventType.getJSEventName(TouchEventType.CANCEL),
            MapBuilder.of(
                "phasedRegistrationNames",
                MapBuilder.of("bubbled", "onTouchCancel", "captured", "onTouchCancelCapture")))
        .build();
  }
 /* package */ static Map getDirectEventTypeConstants() {
    final String rn = "registrationName";
    return MapBuilder.builder()
        .put("topContentSizeChange", MapBuilder.of(rn, "onContentSizeChange"))
        .put("topLayout", MapBuilder.of(rn, "onLayout"))
        .put("topLoadingError", MapBuilder.of(rn, "onLoadingError"))
        .put("topLoadingFinish", MapBuilder.of(rn, "onLoadingFinish"))
        .put("topLoadingStart", MapBuilder.of(rn, "onLoadingStart"))
        .put("topSelectionChange", MapBuilder.of(rn, "onSelectionChange"))
        .put("topMessage", MapBuilder.of(rn, "onMessage"))
        .put("topClick", MapBuilder.of(rn, "onClick"))
        // Scroll events are added as per task T22348735.
        // Subject for further improvement.
        .put("topScrollBeginDrag", MapBuilder.of(rn, "onScrollBeginDrag"))
        .put("topScrollEndDrag", MapBuilder.of(rn, "onScrollEndDrag"))
        .put("topScroll", MapBuilder.of(rn, "onScroll"))
        .put("topMomentumScrollBegin", MapBuilder.of(rn, "onMomentumScrollBegin"))
        .put("topMomentumScrollEnd", MapBuilder.of(rn, "onMomentumScrollEnd"))
        .build();
  }

這兩個方法都設置了一些,如果我們想自己定義一些事件,可以在自定義的ViewManager中重寫getExportedCustomDirectEventTypeConstants方法實現,WebView框架代碼實現如下:

@Override
  public Map getExportedCustomDirectEventTypeConstants() {
    Map export = super.getExportedCustomDirectEventTypeConstants();
    if (export == null) {
      export = MapBuilder.newHashMap();
    }
    export.put(TopLoadingProgressEvent.EVENT_NAME, MapBuilder.of("registrationName", "onLoadingProgress"));
    export.put(TopShouldStartLoadWithRequestEvent.EVENT_NAME, MapBuilder.of("registrationName", "onShouldStartLoadWithRequest"));
    export.put(ScrollEventType.getJSEventName(ScrollEventType.SCROLL), MapBuilder.of("registrationName", "onScroll"));
    export.put(TopHttpErrorEvent.EVENT_NAME, MapBuilder.of("registrationName", "onHttpError"));
    return export;
  }

再看框架源碼設置事件名爲 topLoadingStart,對應js中的名字爲 onLoadingStart。接下來我們看WebView在js當中的源碼,路徑爲在這裏插入圖片描述

我們找到 onLoadingStart方法:

        var webView = (<NativeWebView key="webViewKey" {...otherProps} messagingEnabled={typeof onMessage === 'function'} onLoadingError={this.onLoadingError} onLoadingFinish={this.onLoadingFinish} onLoadingProgress={this.onLoadingProgress} onLoadingStart={this.onLoadingStart} onHttpError={this.onHttpError} onMessage={this.onMessage} onShouldStartLoadWithRequest={onShouldStartLoadWithRequest} ref={this.webViewRef} 

具體實現如下:

 _this.onLoadingStart = function (event) {
            var onLoadStart = _this.props.onLoadStart;
            var url = event.nativeEvent.url;
            _this.startUrl = url;
            if (onLoadStart) {
                onLoadStart(event);
            }
            _this.updateNavigationState(event);
        };
_this.onLoadingFinish = function (event) {
            var _a = _this.props, onLoad = _a.onLoad, onLoadEnd = _a.onLoadEnd;
            var url = event.nativeEvent.url;
            if (onLoad) {
                onLoad(event);
            }
            if (onLoadEnd) {
                onLoadEnd(event);
            }
            if (url === _this.startUrl) {
                _this.setState({
                    viewState: 'IDLE'
                });
            }
            _this.updateNavigationState(event);
        };


_this.updateNavigationState = function (event) {
            if (_this.props.onNavigationStateChange) {
                _this.props.onNavigationStateChange(event.nativeEvent);
            }
        };

由此可以看出,onNavigationStateChange方法是在onLoadingStartonLoadingFinish中調用的,而因爲React單頁面的緣故,原生WebView中的onPageStartedonPageFinished不會響應,所以不會發送事件調用js中的方法,onNavigationStateChange方法也就不會響應了,終於找到了問題原因。

三、解決

尋找發現 WebViewClient中的doUpdateVisitedHistory(WebView view, String url, boolean isReload);方法會在單頁面h5切換時調用,因此決定在這個方法中發送事件通知js方法。

本着最小改動的想法,決定使用已有的onLoadingStart事件,這樣js代碼中就不需要額外處理事件了,而且onLoadingStart也會調用onNavigationStateChange

拷貝WebView中的兩個類

在這裏插入圖片描述

RNCWebViewManager中添加代碼如下代碼如下:

  protected static class RNCWebViewClient extends WebViewClient {
    @Override
    public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) {
      super.doUpdateVisitedHistory(webView, url, isReload);
      dispatchEvent(
        webView,
        new TopLoadingStartEvent(
          webView.getId(),
          createWebViewEvent(webView, url)));
    }
  }

在ReactPackage中添加ViewManager:

public class WebViewReactPackage implements ReactPackage {

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Collections.singletonList(new RNCWebViewManager());
    }

    @Override
    public List<NativeModule> createNativeModules(
        ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }
}

然後再在Application中註冊ReactPackage

   @Override
        protected List<ReactPackage> getPackages() {
            @SuppressWarnings("UnnecessaryLocalVariable")
            List<ReactPackage> packages = new PackageList(this).getPackages();
            // Packages that cannot be autolinked yet can be added manually here, for example:
            packages.add(new WebViewReactPackage());
            return packages;
        }

到此問題就已經解決了,每次頁面跳轉都會調用onNavigationStateChange了。

此處涉及到一個問題,因爲我們拷貝出來的ViewManager沒有修改他的name,因此他會覆蓋掉相同名字先註冊的Package(在new PackageList(this).getPackages()中註冊),所以我們就不需要修改自定義ViewManager的name,然後修改和拷貝js源碼。用這種方式就可以簡單的修改框架源碼。

ReactNative之原生模塊重名的問題
iOS端NativeModule爲實現RCTBridgeModule接口,NativeComponent爲繼承RCTViewManager類。名字是禁止重複,一旦有模塊名重複,運行會直接報錯。
Android端NativeModule爲繼承ReactContextBaseJavaModule類,NativeComponent爲繼承SimpleViewManager或者ViewGroupManager類。Android端通過重寫getName方法來指定模塊或者組件名,NativeModule通過canOverrideExistingModule設置是否允許重名,允許重名會覆蓋掉先註冊的Module。NativeComponent則直接允許重名,也是後加的覆蓋先加的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章