0
点赞
收藏
分享

微信扫一扫

RN - 封装Android原生WebView组件,实现JS获取原生消息回调及JS控制native组件

参考链接1
参考链接2

核心内容:
1. native向RN发送消息事件, RN通过回调函数获取消息
->即实现 onCanGoBack={(canGoBack:boolean)=>{}}
->onTitle={(title)=>{}}
2.RN向Native发送操作命令,Native接收后执行操作
-> 即实现this.refs.web.goBack()

A: 基础部分 - 封装原生WebView组件

1. WxPayReactWebViewManager实现

	public static final String RN_CLASS = "WxPayRCTWebView";
    @Override
    public String getName() {
        return RN_CLASS;
    }

实现视图方法 -返回ViewManager的实例对象, 这里返回WebView的实例对象

    @SuppressLint("ResourceAsColor")
    @Override
    protected WebView createViewInstance(ThemedReactContext reactContext) {
    ...具体代码省略

设置RN组件可以支持的参数名

    /**
     * js传递的参数
     *
     * @param view
     * @param loadInfo
     */
    @ReactProp(name = "loadInfo")
    public void setLoadInfo(WebView view, @Nullable ReadableMap loadInfo) {
    ...具体代码省略

2 WxPayWebViewReactPackage的实现

createNativeModules()是注册模块的,createViewManagers()是注册管理器的,如果想要注册多个模块就在List里面多个添加就行

    /**
     * 注册模块
     * @param reactContext
     * @return
     */
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    /**
     * 注册管理器
     * @param reactContext
     * @return
     */
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new WxPayReactWebViewManager()
        );
    }

3 MainApplication中注册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 WxPayWebViewReactPackage());
                return packages;
            }

4 RN中 - 创建WxPayWeb组件

WxPayRCTWebView:

var WxPayRCTWebView = requireNativeComponent('WxPayRCTWebView', WxPayWeb, {nativeOnly: {propABC:true}})
//propABC属性不想暴露给外面的话,就是通过nativeOnly来设置

WxPayWeb:

  render() {
    return <WxPayRCTWebView
      {...this.props}
      ref={RCT_WxPayWebVIEW_REF}
      //onCanGoBack={this._onCanGoBack}
      //onTitle={this._onTitle}
    />
  }

WxPayWeb.propTypes:
声明RN组件的支持参数

//RN组件支持的props列表
//新版本必须加...View.propTypes
WxPayWeb.propTypes = {
  loadInfo:PropTypes.any,
  html: PropTypes.string,
  onCanGoBack: PropTypes.func,
  onTitle: PropTypes.func,
  ...View.propTypes
}

目前支持的类型映射:

// java对应js 类型
Boolean -> Bool
Integer -> Number
Double -> Number
Float -> Number
String -> String
Callback -> function
ReadableMap -> Object
ReadableArray -> Array

5 具体使用

loadInfo就是自定义的参数名

 <WxPayWeb
        ref={(WxPayWeb: any) => this.web = WxPayWeb}
        style={{flex: 1}}
        loadInfo={{
          url: url,
          referer: url
        }}
      />

至此,已经完成基本原生组件的封装,以及RN层面组件的使用,也支持 传递参数供原生使用

B: Native与RN 事件传递及通信

一. Native向RN发送消息,RN通过回调函数获取消息

重写方法- 创建要支持的自定义事件名
自定义事件,只需要重写ReactVideoManager下的getExportedCustomBubblingEventTypeConstants()方法就好

    /**
     * 注册-自定义事件名
     * 自定义多个
     *
     * @return
     */
    @Override
    public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
        // onCanGoBack 自定义事件名, 其他固定写法 (包括 bubbled)
        MapBuilder.Builder<String, Object> builder = MapBuilder.<String, Object>builder();
        builder.put("onCanGoBack", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onCanGoBack")));
        // onTitle 自定义事件名, 其他固定写法 (包括 bubbled)
        builder.put("onTitle", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onTitle")));
        return builder.build();
    }

webView加载时,通过webView.canGoBack()方法判断是否canGoBack, 向RN层发送消息事件


	// 网页回调函数
     @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                if (wv.canGoBack()) {
                    WritableMap event = Arguments.createMap();
                    event.putBoolean("canGoBack", true);
                    // 发送给js
                    this.dispatch(reactContext, "onCanGoBack", event);
                } else {
                    WritableMap event = Arguments.createMap();
                    event.putBoolean("canGoBack", false);
                    // 发送给js
                    this.dispatch(reactContext, "onCanGoBack", event);
                }
                super.onPageStarted(view, url, favicon);
            }


         /**
             * 向js发送事件回调,传值
             * @param context
             * @param EventName
             * @param eventMap
             */
            public void dispatch(ThemedReactContext context, String EventName, WritableMap eventMap) {
                reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(wv.getId(), EventName, eventMap);
            }

RN端回调函数实现及数据接收:

 render() {
    return <BXRCTWebView
      {...this.props}
      ref={RCT_BXWEBVIEW_REF}
      onCanGoBack={this._onCanGoBack}
      onTitle={this._onTitle}
    />
  }

  // onCanGoBack 回调
  _onCanGoBack = (event) => {
    //回调均接受的是event,取值为 event.nativeEvent下的各个字段
    this.props.onCanGoBack && this.props.onCanGoBack(event.nativeEvent.canGoBack)
  }

  // onTitle 回调
  _onTitle = (event) => {
    //回调均接受的是event,取值为 event.nativeEvent下的各个字段
    this.props.onTitle && this.props.onTitle(event.nativeEvent.title);
  }


//新版本必须加...View.propTypes
BxWeb.propTypes = {
  url: PropTypes.string,
  html: PropTypes.string,
  onCanGoBack: PropTypes.func,
  onTitle: PropTypes.func,
  ...View.propTypes
}

WxPayWeb组件的完整使用

<WxPayWeb
        ref={(WxPayWeb: any) => this.web = WxPayWeb}
        style={{flex: 1}}
        loadInfo={{
          url: url,
          referer: url
        }}
        onCanGoBack={(canGoBack: boolean) => {
        	// canGoBack 
        }}
        onTitle={(title: string) => {
        	// title 
        }}
      />

二. RN向Native发送操作命令,Native接收后执行操作

在native侧, 需要实现两个函数, 用于注册命名列表, 以及接收命令列表
实现 getCommandsMap:原生组件提供函数调用列表
receiveCommand: 接收js命令的函数

注意 是这个 public void receiveCommand(WebView root, int commandId, ReadableArray args){}
而不是 public void receiveCommand(WebView root, String commandId, ReadableArray args)
后者接收不到

    /**
     * 注册 js调用native的方法集合
     *
     * @return
     */
    @Nullable
    @Override
    public Map<String, Integer> getCommandsMap() {
        return MapBuilder.of(
                "goBack", 1,
                "toast", 2
        );
    }

    /**
     * 响应 js调用函数的实现
     *
     * @param root
     * @param commandId
     * @param args
     */
    @Override
    public void receiveCommand(WebView root, int commandId, ReadableArray args) {
        Log.d("receiveCommand======1", commandId + "");
        switch (commandId) {
            case 1:
                root.goBack();
                break;
            case 2:
                Toast.makeText(wv.getContext(), "hello", Toast.LENGTH_LONG);
                break;
            default:
                break;
        }
    }

RN组件 WxPayWebgoBack 命令的实现

  // js组件执行事件 向native层发送命令
  goBack() {
    //向native层发送命令
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.refs[RCT_WxPayWebVIEW_REF]),
      UIManager.WxPayRCTWebView.Commands.goBack,//Commands.goBack与native层定义的"goBack"一致
      null//命令携带的参数数据
    );
  }

  toast_test() {
    //向native层发送命令
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.refs[RCT_WxPayWebVIEW_REF]),
      UIManager.WxPayRCTWebView.Commands.toast,//Commands.toast与native层定义的"toast"一致
      ["a", "b"]//命令携带的参数数据 [数据a,数据b...]
    );
  }

RN组件调用执行

 this.refs.web.goBack();

所有代码

Java侧代码:

WxPayWebViewReactPackage

package com.regan.ebankhome;

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

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class WxPayWebViewReactPackage implements ReactPackage {


    /**
     * 注册模块
     * @param reactContext
     * @return
     */
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    /**
     * 注册管理器
     * @param reactContext
     * @return
     */
    @Override
    public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
        return Arrays.<ViewManager>asList(
                new WxPayReactWebViewManager()
        );
    }

}

WxPayReactWebViewManager

package com.regan.ebankhome;

import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.net.http.SslError;
import android.util.Log;
import android.webkit.ClientCertRequest;
import android.webkit.SslErrorHandler;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.annotations.ReactPropGroup;
import com.facebook.react.uimanager.annotations.ReactPropertyHolder;
import com.facebook.react.uimanager.events.RCTEventEmitter;

import java.util.HashMap;
import java.util.Map;

import static com.umeng.socialize.utils.ContextUtil.getContext;

/**
 * ViewManager
 */
public class WxPayReactWebViewManager extends SimpleViewManager<WebView> {

    public static final String RN_CLASS = "WxPayRCTWebView";
    public String url;
    public String referer;
    private WebView wv;

    /**
     * 返回的 组件名称 - 原生组件名称
     * @return
     */
    @Override
    public String getName() {
        return RN_CLASS;
    }

    /**
     * 返回ViewManager的实例对象, 这里返回WebView的实例对象
     * @param reactContext
     * @return
     */
    @SuppressLint("ResourceAsColor")
    @Override
    protected WebView createViewInstance(ThemedReactContext reactContext) {
        wv = new WebView(reactContext);
        webSetting();
        wv.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                try {
                    if (url.startsWith("weixin://") || url.startsWith("alipays://")) {
                        Intent intent = new Intent();
                        intent.setAction(Intent.ACTION_VIEW);
                        intent.setData(Uri.parse(url));
                        reactContext.startActivity(intent);
                        return true;
                    }
                } catch (Exception e) {
                    return false;
                }

                if (url.contains("https://wx.tenpay.com")) {
                    Map<String, String> extraHeaders = new HashMap<>();
                    extraHeaders.put("Referer", referer.length() > 0 ? referer : url);
                    view.loadUrl(url, extraHeaders);
                    return true;
                }
                view.loadUrl(url);
                return true;
            }

            /**
             * 向js发送事件回调,传值
             * @param context
             * @param EventName
             * @param eventMap
             */
            public void dispatch(ThemedReactContext context, String EventName, WritableMap eventMap) {
                reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(wv.getId(), EventName, eventMap);
            }


            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                if (wv.canGoBack()) {
                    Log.d("canGoBack===>", "yes-goback");
                    WritableMap event = Arguments.createMap();
                    event.putBoolean("canGoBack", true);
                    // 发送给js
                    this.dispatch(reactContext, "onCanGoBack", event);
                } else {
                    WritableMap event = Arguments.createMap();
                    event.putBoolean("canGoBack", false);
                    // 发送给js
                    this.dispatch(reactContext, "onCanGoBack", event);
                }
                super.onPageStarted(view, url, favicon);
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                Log.d("onPageFinished===>", url);
                WritableMap event = Arguments.createMap();
                event.putString("title", view.getTitle());
                // 发送给js
                this.dispatch(reactContext, "onTitle", event);
                super.onPageFinished(view, url);
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                return super.shouldOverrideUrlLoading(view, request);
            }

            @Override
            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
//                super.onReceivedSslError(view, handler, error);
                // 不要使用super,否则有些手机访问不了,因为包含了一条 handler.cancel()
                // super.onReceivedSslError(view, handler, error);
                // 接受所有网站的证书,忽略SSL错误,执行访问网页
                handler.proceed();
            }
        });
        return wv;
    }
    private void webSetting() {
        WebSettings webSettings = wv.getSettings();
        webSettings.setAllowFileAccessFromFileURLs(true);
        webSettings.setJavaScriptEnabled(true);
        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
        webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
        webSettings.setDomStorageEnabled(true);
        webSettings.setDatabaseEnabled(true);
        webSettings.setAppCacheEnabled(true);
        webSettings.setAllowFileAccess(true);
        webSettings.setSavePassword(true);
        webSettings.setSupportZoom(true);
        webSettings.setBuiltInZoomControls(false);
        webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
        webSettings.setUseWideViewPort(false);
        webSettings.setLoadWithOverviewMode(true);
    }


    /**
     * js传递的参数
     *
     * @param view
     * @param loadInfo
     */
    @ReactProp(name = "loadInfo")
    public void setLoadInfo(WebView view, @Nullable ReadableMap loadInfo) {
        String url = loadInfo.getString("url");
        String referer = loadInfo.getString("referer");
        Log.d("*(*(*(*(--url", url);
        Log.d("*(*(*(*(--referer", referer);
        this.url = url;
        this.referer = referer;
        view.loadUrl(url, (Map<String, String>) new HashMap<>().put("Referer", this.referer));
    }

    /**
     * js传递的参数
     *
     * @param view
     * @param html
     */
    @ReactProp(name = "html")
    public void setHtml(WebView view, @Nullable String html) {
        Log.d("*(*(*(*(--html", html);
        view.loadData(html, "text/html; charset=utf-8", "UTF-8");
    }

    /**
     * 注册-自定义事件名
     * 自定义多个
     *
     * @return
     */
    @Override
    public Map<String, Object> getExportedCustomBubblingEventTypeConstants() {
        // onCanGoBack 自定义事件名, 其他固定写法 (包括 bubbled)
        MapBuilder.Builder<String, Object> builder = MapBuilder.<String, Object>builder();
        builder.put("onCanGoBack", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onCanGoBack")));
        // onTitle 自定义事件名, 其他固定写法 (包括 bubbled)
        builder.put("onTitle", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onTitle")));
        return builder.build();
    }


    /**
     * 注册 js调用native的方法集合
     *
     * @return
     */
    @Nullable
    @Override
    public Map<String, Integer> getCommandsMap() {
        return MapBuilder.of(
                "goBack", 1,
                "toast", 2
        );
    }

    /**
     * 响应 js调用函数的实现
     *
     * @param root
     * @param commandId
     * @param args
     */
    @Override
    public void receiveCommand(WebView root, int commandId, ReadableArray args) {
        Log.d("receiveCommand======1", commandId + "");
        switch (commandId) {
            case 1:
                root.goBack();
                break;
            case 2:
                Toast.makeText(wv.getContext(), "hello", Toast.LENGTH_LONG);
                break;
            default:
                break;
        }
    }
}

MainApplication 中省略…

js侧代码:

WxPayWeb

/**
 *
 * @auther:  tiannan
 * @date:  2021-07-08 15:44
 * @profile: WxPayWebView
 * @project: ebankhome_c_b
 * @description:
 *
 */

import React, {Component} from 'react';
import {requireNativeComponent, View, UIManager, findNodeHandle} from 'react-native';
import PropTypes from 'prop-types';

//REF
var RCT_WxPayWebVIEW_REF = 'RCT_WxPayWebVIEW_REF';


// requireNativeComponent - 通过该函数将原生组件名称 与 RN组件类名 进行关联,返回原生组件的实例
// WxPayRCTWebView - 原生组件可以用追加 RCT表示, 明确这是原生组件的创建
// WxPayWeb - RN组件类
// nativeOnly:{todo:false}, 表示todo属性私有,外部不能使用
var WxPayRCTWebView = requireNativeComponent('WxPayRCTWebView', WxPayWeb, {nativeOnly: {}})

// WxPayWeb 真正的RN组件类, 实际是返回的一个 <WxPayRCTWebView {...this.props} .../>原生组件
// 可以提供 如goBack()等方法提供给外部调用
// 可以接受 props, 实现RN传递参数 或者 回调函数给原生组件
// 通过 WxPayWeb.propTypes ={a:PropTypes.String ,b:PropTypes.func} 来明确支持的props及类型
class WxPayWeb extends Component {
  constructor(props) {
    super(props);
  }

  // js组件执行事件 向native层发送命令
  goBack() {
    //向native层发送命令
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.refs[RCT_WxPayWebVIEW_REF]),
      UIManager.WxPayRCTWebView.Commands.goBack,//Commands.goBack与native层定义的"goBack"一致
      null//命令携带的参数数据
    );
  }

  toast_test() {
    //向native层发送命令
    UIManager.dispatchViewManagerCommand(
      findNodeHandle(this.refs[RCT_WxPayWebVIEW_REF]),
      UIManager.WxPayRCTWebView.Commands.toast,//Commands.toast与native层定义的"toast"一致
      ["a", "b"]//命令携带的参数数据 [数据a,数据b...]
    );
  }


  render() {
    return <WxPayRCTWebView
      {...this.props}
      ref={RCT_WxPayWebVIEW_REF}
      onCanGoBack={this._onCanGoBack}
      onTitle={this._onTitle}
    />
  }

  // onCanGoBack 回调
  _onCanGoBack = (event) => {
    //回调均接受的是event,取值为 event.nativeEvent下的各个字段
    this.props.onCanGoBack && this.props.onCanGoBack(event.nativeEvent.canGoBack)
  }

  // onTitle 回调
  _onTitle = (event) => {
    //回调均接受的是event,取值为 event.nativeEvent下的各个字段
    this.props.onTitle && this.props.onTitle(event.nativeEvent.title);
  }

}

//RN组件支持的props列表
//新版本必须加...View.propTypes
WxPayWeb.propTypes = {
  loadInfo: PropTypes.any,
  html: PropTypes.string,
  onCanGoBack: PropTypes.func,
  onTitle: PropTypes.func,
  ...View.propTypes
}

export default WxPayWeb


//----------------------------------------- 旧代码 ---------------------------
// var iface = {
//   name: 'WebView',
//   propTypes: {
//     url: "",
//     html: "",
//     // readTitle: null,
//     // canGoBack: null,
//     ...View.propTypes
//   },
// }
//
// const WxPayWeb = requireNativeComponent('WxPayRCTWebView', iface, {
//   nativeOnly: {
//     "testID": true,
//     'renderToHardwareTextureAndroid': true,
//     'accessibilityComponentType': true,
//     'accessibilityLabel': true,
//     'importantForAccessibility': true,
//     'accessibilityLiveRegion': true,
//     'onLayout': true,
//   }
// });
//
// export default WxPayWeb;

组件调用

<WxPayWeb
        ref={(WxPayWeb: any) => this.web = WxPayWeb}
        style={{flex: 1}}
        loadInfo={{
          url: url,
          referer: url
        }}
        onCanGoBack={(canGoBack: boolean) => {
          this.props.navigation.setParams({
            canGoBack,
          });
        }}
        onTitle={(title: string) => {
          this.props.navigation.setParams({
            changeTitle: title.startsWith('http') ? '' : title,
          });
        }}
      />
举报

相关推荐

0 条评论