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则直接允许重名,也是后加的覆盖先加的

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