1. 程式人生 > >react-native WebView 返回處理 (非回調方法可解決)

react-native WebView 返回處理 (非回調方法可解決)

外部 log finish 既然 node div hang rec 代碼段

1.前言

項目中有些頁面內容是變更比較頻繁的,這些頁面我們會考慮用網頁來解決。

在RN項目中提供一個公用的Web頁,如果是網頁內容,就跳轉到這個界面展示。

此時會有一個問題是,網頁會有一級頁面,二級頁面,這就會設計到導航欄返回鍵的處理(以及在Android上返回鍵的處理)。

這個問題,在RN官網就可找到解決方式。就是用onNavigationStateChange這個回調方法記錄當前的導航狀態,從而判斷是返回上一級頁面還是退出這個網頁,回到App的其他界面。

但是,當網頁的實現是React時,就會有問題了,你會發現,當頁面跳轉的時候,onNavigationStateChange這個回調方法沒有回調!!!怎麽肥四!!

一開始嘗試了把網頁地址換成百度的,可以接收回調,一切都運行的很好,可是換成我們的鏈接就不行,所以就把鍋甩給了後臺,以為是React哪邊寫的不對。

因為上一個項目時間緊,沒有時間好好去看一下源碼,就想了一個不是很完善的解決方案,就是網頁用js來回調App來告知現在的導航狀態,這樣的解決方式顯示是不友好的。

現在稍微有點時間看了源碼才知道真正原因。

2.原因

下面就分析一下這個問題的原因和我的解決方式。

1.首先,先找到源碼的位置。

node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\views\webview

node_modules\react-native\Libraries\Components\WebView

目錄結構是這樣的:技術分享圖片 技術分享圖片

2.實現的代碼段 (JAVA端)

RN的實際運行代碼都是原生代碼,所以,像WebView組件的一些事件回調,其實都是原生代碼中的回調觸發的。如下

(ReactWebViewManager.java) rn版本0.47.1

技術分享圖片
  protected static class ReactWebViewClient extends WebViewClient { //WebViewClient就是我們在寫Android原生代碼時,監聽網頁加載情況使用的工具。
protected static final String REACT_CLASS = "RCTWebView"; //定義的原生組件名,在後面JS中會對應到。 //... @Override public void onPageStarted(WebView webView, String url, Bitmap favicon) { //有很多回調方法,此處只舉一例 super.onPageStarted(webView, url, favicon); mLastLoadFailed = false; dispatchEvent( webView, new TopLoadingStartEvent( //自己定義的時間,dispatch後,事件會傳給js webView.getId(), createWebViewEvent(webView, url))); } //... }
View Code

(ReactWebViewManager.java) rn版本0.43.3 ,RN不同版本會有代碼調整,所以RN升級的時候,需要仔細的回歸測試。

技術分享圖片
protected static class ReactWebViewClient extends WebViewClient { //WebViewClient就是我們在寫Android原生代碼時,監聽網頁加載情況使用的工具。
      protected static final String REACT_CLASS = "RCTWebView"; //定義的原生組件名,在後面JS中會對應到。

    //...

    @Override
    public void onPageStarted(WebView webView, String url, Bitmap favicon) {  //有很多回調方法,此處只舉一例
      super.onPageStarted(webView, url, favicon);
      mLastLoadFailed = false;

      dispatchEvent(
          webView,
          new TopLoadingStartEvent(      //自己定義的時間,dispatch後,事件會傳給js
              webView.getId(),
              createWebViewEvent(webView, url)));
    }

    @Override
    public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) {  //坑在這,這裏就是導航有變化的時候會回調在這個版本是有這個處理的,但是不知道在哪個版本刪掉了 -.-
      super.doUpdateVisitedHistory(webView, url, isReload);

      dispatchEvent(
          webView,
          new TopLoadingStartEvent(
              webView.getId(),
              createWebViewEvent(webView, url)));
    }

    //...
 }
View Code

(TopLoadingStartEvent.java) 回調JS的Event

技術分享圖片
public class TopLoadingStartEvent extends Event<TopLoadingStartEvent> {

  public static final String EVENT_NAME = "topLoadingStart";   //對應方法是onLoadingStart, 因為對RN的結構不熟悉,在此處花了很長時間研究是怎麽對應的,最後找到了定義對應的文件
  private WritableMap mEventData;

  public TopLoadingStartEvent(int viewId, WritableMap eventData) {
    super(viewId);
    mEventData = eventData;
  }

  @Override
  public String getEventName() {
    return EVENT_NAME;
  }

  @Override
  public boolean canCoalesce() {
    return false;
  }

  @Override
  public short getCoalescingKey() {
    // All events for a given view can be coalesced.
    return 0;
  }

  @Override
  public void dispatch(RCTEventEmitter rctEventEmitter) {
    rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
  }
}
View Code

(node_modules\react-native\ReactAndroid\src\main\java\com\facebook\react\uimanager\UIManagerModuleConstants.java)

這個文件裏,定義了對應關系

技術分享圖片
/**
 * Constants exposed to JS from {@link UIManagerModule}.
 */
/* package */ class UIManagerModuleConstants {

  /* package */ static Map getDirectEventTypeConstants() {
    return MapBuilder.builder()
        .put("topContentSizeChange", MapBuilder.of("registrationName", "onContentSizeChange"))
        .put("topLayout", MapBuilder.of("registrationName", "onLayout"))
        .put("topLoadingError", MapBuilder.of("registrationName", "onLoadingError"))
        .put("topLoadingFinish", MapBuilder.of("registrationName", "onLoadingFinish"))
        .put("topLoadingStart", MapBuilder.of("registrationName", "onLoadingStart"))
        .put("topSelectionChange", MapBuilder.of("registrationName", "onSelectionChange"))
        .put("topMessage", MapBuilder.of("registrationName", "onMessage"))
        .build();
  }

}
View Code

3.實現的代碼段 (JS端)

(node_modules\react-native\Libraries\Components\WebView\WebView.android.js)

在下面的代碼中可以看到只有 onLoadingStart onLoadingFinish才會調用 updateNavigationState,問題就出現在這了,由於我們的網頁實現是React,只有一個頁面啊!所以只會調用一次onLoadingStart onLoadingFinish。再點擊詳情頁並不會跳轉到新頁面,而是刷新原來的頁面。所以也就沒有updateNavigationState回調了。

技術分享圖片
class WebView extends React.Component {
  static propTypes = {    //給外部定義的可設置的屬性
    ...ViewPropTypes,
    renderError: PropTypes.func,
    renderLoading: PropTypes.func,
    onLoad: PropTypes.func,
    //...
   }

  render() {  //繪制頁面內容
    //...
    var webView =
      <RCTWebView
        ref={RCT_WEBVIEW_REF}
        key="webViewKey"
        style={webViewStyles}
        source={resolveAssetSource(source)}
        onLoadingStart={this.onLoadingStart}
        onLoadingFinish={this.onLoadingFinish}
        onLoadingError={this.onLoadingError}/>;

    return (
      <View style={styles.container}>
        {webView}
        {otherView}
      </View>
    );
  }

  onLoadingStart = (event) => {
    var onLoadStart = this.props.onLoadStart;
    onLoadStart && onLoadStart(event);
    this.updateNavigationState(event);
  };

  onLoadingFinish = (event) => {
    var {onLoad, onLoadEnd} = this.props;
    onLoad && onLoad(event);
    onLoadEnd && onLoadEnd(event);
    this.setState({
      viewState: WebViewState.IDLE,
    });
    this.updateNavigationState(event);
  };

  updateNavigationState = (event) => {
    if (this.props.onNavigationStateChange) {
      this.props.onNavigationStateChange(event.nativeEvent);
    }
  };
}

var RCTWebView = requireNativeComponent(‘RCTWebView‘, WebView, {    //對應上面JAVA中的 ‘RCTWebView’
 nativeOnly: { messagingEnabled: PropTypes.bool, }, });


 module.exports = WebView;  
View Code

2.解決方法

既然原因找到了,就容易解決了

解決方式:自定義WebView,添加 doUpdateVisitedHistory 處理,在每次導航變化的時候,通知JS。

1. 拷貝下圖中的文件到我們自己項目中的Android代碼目錄下

技術分享圖片

拷貝完後的Android目錄:

技術分享圖片

  • ReactWebViewManager.java中需要修改幾個地方

技術分享圖片
public class ReactWebViewManager extends SimpleViewManager<WebView> {
  protected static final String REACT_CLASS = "RCTWebView1";  //此處修改一下名字


  protected static class ReactWebViewClient extends WebViewClient {
        @Override
        public void doUpdateVisitedHistory(WebView webView, String url, boolean isReload) {
            super.doUpdateVisitedHistory(webView, url, isReload);

            dispatchEvent(       //在導航變化的時候,dispatchEvent
                    webView,
                    new TopCanGoBackEvent(
                            webView.getId(),
                            createCanGoBackWebViewEvent(webView, url)));
        }
  }
}
View Code

  • TopCanGoBackEvent是我自己添加的一個Event,專門用來通知導航變化

TopCanGoBackEvent.java

技術分享圖片
public class TopCanGoBackEvent extends Event<TopCanGoBackEvent> {

  public static final String EVENT_NAME = "topChange";  
  private WritableMap mEventData;

  public TopCanGoBackEvent(int viewId, WritableMap eventData) {
    super(viewId);
    mEventData = eventData;
  }

  @Override
  public String getEventName() {
    return EVENT_NAME;
  }

  @Override
  public boolean canCoalesce() {
    return false;
  }

  @Override
  public short getCoalescingKey() {
    // All events for a given view can be coalesced.
    return 0;
  }

  @Override
  public void dispatch(RCTEventEmitter rctEventEmitter) {
    rctEventEmitter.receiveEvent(getViewTag(), getEventName(), mEventData);
  }
}
View Code

  • 新建 ReactWebViewPage.java
技術分享圖片
public class ReactWebViewPackage implements ReactPackage {


    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {

        return Collections.emptyList();
    }

    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new ReactWebViewManager()
        );
    }
}
View Code

  • 然後在MainApplication中添加這個模塊

技術分享圖片
public class MainApplication extends Application implements ReactApplication {
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
          new MainReactPackage(),
          new ReactWebViewPackage()    //WebView
      );
    }
}
View Code

以上就是Android需要修改的地方,ios我沒有嘗試過,應該大差不差同一個道理。

2. 拷貝下圖中的文件到我們自己項目中的JS代碼目錄下,並修改一下名字

技術分享圖片

JS代碼目錄:

技術分享圖片

    • CustomWebView.android.js 有幾個地方需要修改。

技術分享圖片
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule CustomWebView    //此處需要修改名稱
 */

var RCT_WEBVIEW_REF = ‘webview1‘;  //此處需要修改名稱

  render() {
    var webView =
      <NativeWebView
        onLoadingStart={this.onLoadingStart}
        onLoadingFinish={this.onLoadingFinish}
        onLoadingError={this.onLoadingError}
        onChange={this.onChange} //添加方法
      />;

    return (
      <View style={styles.container}>
        {webView}
        {otherView}
      </View>
    );
  }

  onChange = (event) => {    //添加方法
    this.updateNavigationState(event);
  };
}

var RCTWebView = requireNativeComponent(‘RCTWebView1‘, CustomWebView, CustomWebView.extraNativeComponentConfig);  //修改名稱

module.exports = CustomWebView;  //修改名稱
View Code

至此就完成自定義WebView模塊。也可以解決網頁是React實現,不能導航的問題。

不善排版,看不懂的可留言

react-native WebView 返回處理 (非回調方法可解決)