Implement a postMessage function and an onMessage event for webviews …

Summary:
JS API very similar to web workers and node's child process.

Work has been done by somebody else for the Android implementation over at #7020, so we'd need to have these in sync before anything gets merged.

I've made a prop `messagingEnabled` to be more explicit about creating globals—it might be sufficient to just check for an onMessage handler though.

![screen shot 2016-09-06 at 10 28 23](https://cloud.githubusercontent.com/assets/7275322/18268669/b1a12348-741c-11e6-91a1-ad39d5a8bc03.png)
Closes https://github.com/facebook/react-native/pull/9762

Differential Revision: D4008260

fbshipit-source-id: 84b1afafbc0ab1edc3dfbf1a8fb870218e171a4c
This commit is contained in:
Jacob Parker
2016-10-16 06:29:14 -07:00
committed by Facebook Github Bot
parent 6ea26c01de
commit abb8ea3aea
11 changed files with 338 additions and 2 deletions

View File

@@ -77,6 +77,7 @@ import com.facebook.react.uimanager.events.TouchEventType;
.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 File

@@ -4,6 +4,7 @@ android_library(
name = 'webview',
srcs = glob(['**/*.java']),
deps = [
react_native_dep('libraries/fbcore/src/main/java/com/facebook/common/logging:logging'),
react_native_target('java/com/facebook/react/bridge:bridge'),
react_native_target('java/com/facebook/react/uimanager:uimanager'),
react_native_target('java/com/facebook/react/uimanager/annotations:annotations'),

View File

@@ -26,7 +26,11 @@ import android.webkit.GeolocationPermissions;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.webkit.JavascriptInterface;
import android.webkit.ValueCallback;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.LifecycleEventListener;
import com.facebook.react.bridge.ReactContext;
@@ -46,6 +50,10 @@ import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.views.webview.events.TopLoadingErrorEvent;
import com.facebook.react.views.webview.events.TopLoadingFinishEvent;
import com.facebook.react.views.webview.events.TopLoadingStartEvent;
import com.facebook.react.views.webview.events.TopMessageEvent;
import org.json.JSONObject;
import org.json.JSONException;
/**
* Manages instances of {@link WebView}
@@ -74,6 +82,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
private static final String HTML_ENCODING = "UTF-8";
private static final String HTML_MIME_TYPE = "text/html; charset=utf-8";
private static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
private static final String HTTP_METHOD_POST = "POST";
@@ -81,6 +90,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
public static final int COMMAND_GO_FORWARD = 2;
public static final int COMMAND_RELOAD = 3;
public static final int COMMAND_STOP_LOADING = 4;
public static final int COMMAND_POST_MESSAGE = 5;
// Use `webView.loadUrl("about:blank")` to reliably reset the view
// state and release page resources (including any running JavaScript).
@@ -100,6 +110,7 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
if (!mLastLoadFailed) {
ReactWebView reactWebView = (ReactWebView) webView;
reactWebView.callInjectedJavaScript();
reactWebView.linkBridge();
emitFinishEvent(webView, url);
}
}
@@ -190,6 +201,20 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
*/
private static class ReactWebView extends WebView implements LifecycleEventListener {
private @Nullable String injectedJS;
private boolean messagingEnabled = false;
private class ReactWebViewBridge {
ReactWebView mContext;
ReactWebViewBridge(ReactWebView c) {
mContext = c;
}
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
}
}
/**
* WebView must be created with an context of the current activity
@@ -221,6 +246,20 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
injectedJS = js;
}
public void setMessagingEnabled(boolean enabled) {
if (messagingEnabled == enabled) {
return;
}
messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(new ReactWebViewBridge(this), BRIDGE_NAME);
linkBridge();
} else {
removeJavascriptInterface(BRIDGE_NAME);
}
}
public void callInjectedJavaScript() {
if (getSettings().getJavaScriptEnabled() &&
injectedJS != null &&
@@ -229,6 +268,34 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
}
}
public void linkBridge() {
if (messagingEnabled) {
if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// See isNative in lodash
String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
if (value.equals("true")) {
FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
}
});
}
loadUrl("javascript:(" +
"window.originalPostMessage = window.postMessage," +
"window.postMessage = function(data) {" +
BRIDGE_NAME + ".postMessage(String(data));" +
"}" +
")");
}
}
public void onMessage(String message) {
dispatchEvent(this, new TopMessageEvent(this.getId(), message));
}
private void cleanupCallbacksAndDestroy() {
setWebViewClient(null);
destroy();
@@ -310,6 +377,11 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
((ReactWebView) view).setInjectedJavaScript(injectedJavaScript);
}
@ReactProp(name = "messagingEnabled")
public void setMessagingEnabled(WebView view, boolean enabled) {
((ReactWebView) view).setMessagingEnabled(enabled);
}
@ReactProp(name = "source")
public void setSource(WebView view, @Nullable ReadableMap source) {
if (source != null) {
@@ -385,7 +457,8 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
"goBack", COMMAND_GO_BACK,
"goForward", COMMAND_GO_FORWARD,
"reload", COMMAND_RELOAD,
"stopLoading", COMMAND_STOP_LOADING);
"stopLoading", COMMAND_STOP_LOADING,
"postMessage", COMMAND_POST_MESSAGE);
}
@Override
@@ -403,6 +476,15 @@ public class ReactWebViewManager extends SimpleViewManager<WebView> {
case COMMAND_STOP_LOADING:
root.stopLoading();
break;
case COMMAND_POST_MESSAGE:
try {
JSONObject eventInitDict = new JSONObject();
eventInitDict.put("data", args.getString(0));
root.loadUrl("javascript:(document.dispatchEvent(new MessageEvent('message', " + eventInitDict.toString() + ")))");
} catch (JSONException e) {
throw new RuntimeException(e);
}
break;
}
}

View File

@@ -0,0 +1,52 @@
/**
* 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.
*/
package com.facebook.react.views.webview.events;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
/**
* Event emitted when there is an error in loading.
*/
public class TopMessageEvent extends Event<TopMessageEvent> {
public static final String EVENT_NAME = "topMessage";
private final String mData;
public TopMessageEvent(int viewId, String data) {
super(viewId);
mData = data;
}
@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) {
WritableMap data = Arguments.createMap();
data.putString("data", mData);
rctEventEmitter.receiveEvent(getViewTag(), EVENT_NAME, data);
}
}