Permalink
Browse files

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 #9762

Differential Revision: D4008260

fbshipit-source-id: 84b1afafbc0ab1edc3dfbf1a8fb870218e171a4c
  • Loading branch information...
1 parent 6ea26c0 commit abb8ea3aea686e2cd881a61fbc66d137857ef422 @jacobp100 jacobp100 committed with Facebook Github Bot Oct 16, 2016
@@ -217,6 +217,53 @@ class ScaledWebView extends React.Component {
}
}
+class MessagingTest extends React.Component {
+ webview = null
+
+ state = {
+ messagesReceivedFromWebView: 0,
+ message: '',
+ }
+
+ onMessage = e => this.setState({
+ messagesReceivedFromWebView: this.state.messagesReceivedFromWebView + 1,
+ message: e.nativeEvent.data,
+ })
+
+ postMessage = () => {
+ if (this.webview) {
+ this.webview.postMessage('"Hello" from React Native!');
+ }
+ }
+
+ render(): ReactElement<any> {
+ const {messagesReceivedFromWebView, message} = this.state;
+
+ return (
+ <View style={[styles.container, { height: 200 }]}>
+ <View style={styles.container}>
+ <Text>Messages received from web view: {messagesReceivedFromWebView}</Text>
+ <Text>{message || '(No message)'}</Text>
+ <View style={styles.buttons}>
+ <Button text="Send Message to Web View" enabled onPress={this.postMessage} />
+ </View>
+ </View>
+ <View style={styles.container}>
+ <WebView
+ ref={webview => { this.webview = webview; }}
+ style={{
+ backgroundColor: BGWASH,
+ height: 100,
+ }}
+ source={require('./messagingtest.html')}
+ onMessage={this.onMessage}
+ />
+ </View>
+ </View>
+ );
+ }
+}
+
var styles = StyleSheet.create({
container: {
flex: 1,
@@ -391,5 +438,9 @@ exports.examples = [
/>
);
}
+ },
+ {
+ title: 'Mesaging Test',
+ render(): ReactElement<any> { return <MessagingTest />; }
}
];
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Messaging Test</title>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8">
+ <meta name="viewport" content="width=320, user-scalable=no">
+ </head>
+ <body>
+ <p>Messages recieved from React Native: 0</p>
+ <p>(No messages)</p>
+ <button type="button">
+ Send message to React Native
+ </button>
+ </body>
+ <script>
+ var messagesReceivedFromReactNative = 0;
+ document.addEventListener('message', function(e) {
+ messagesReceivedFromReactNative += 1;
+ document.getElementsByTagName('p')[0].innerHTML =
+ 'Messages recieved from React Native: ' + messagesReceivedFromReactNative;
+ document.getElementsByTagName('p')[1].innerHTML = e.data;
+ });
+
+ document.getElementsByTagName('button')[0].addEventListener('click', function() {
+ window.postMessage('"Hello" from the web view');
+ });
+ </script>
+</html>
@@ -58,6 +58,7 @@ class WebView extends React.Component {
automaticallyAdjustContentInsets: PropTypes.bool,
contentInset: EdgeInsetsPropType,
onNavigationStateChange: PropTypes.func,
+ onMessage: PropTypes.func,
onContentSizeChange: PropTypes.func,
startInLoadingState: PropTypes.bool, // force WebView to show loadingView on first load
style: View.propTypes.style,
@@ -218,6 +219,8 @@ class WebView extends React.Component {
userAgent={this.props.userAgent}
javaScriptEnabled={this.props.javaScriptEnabled}
domStorageEnabled={this.props.domStorageEnabled}
+ messagingEnabled={typeof this.props.onMessage === 'function'}
+ onMessage={this.onMessage}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={this.props.automaticallyAdjustContentInsets}
onContentSizeChange={this.props.onContentSizeChange}
@@ -268,6 +271,14 @@ class WebView extends React.Component {
);
};
+ postMessage = (data) => {
+ UIManager.dispatchViewManagerCommand(
+ this.getWebViewHandle(),
+ UIManager.RCTWebView.Commands.postMessage,
+ [String(data)]
+ );
+ };
+
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
@@ -310,9 +321,18 @@ class WebView extends React.Component {
});
this.updateNavigationState(event);
};
+
+ onMessage = (event: Event) => {
+ var {onMessage} = this.props;
+ onMessage && onMessage(event);
+ }
}
-var RCTWebView = requireNativeComponent('RCTWebView', WebView);
+var RCTWebView = requireNativeComponent('RCTWebView', WebView, {
+ nativeOnly: {
+ messagingEnabled: PropTypes.bool,
+ },
+});
var styles = StyleSheet.create({
container: {
@@ -236,6 +236,16 @@ class WebView extends React.Component {
*/
onNavigationStateChange: PropTypes.func,
/**
+ * A function that is invoked when the webview calls `window.postMessage`.
+ * Setting this property will inject a `postMessage` global into your
+ * webview, but will still call pre-existing values of `postMessage`.
+ *
+ * `window.postMessage` accepts one argument, `data`, which will be
+ * available on the event object, `event.nativeEvent.data`. `data`
+ * must be a string.
+ */
+ onMessage: PropTypes.func,
+ /**
* Boolean value that forces the `WebView` to show the loading view
* on the first load.
*/
@@ -382,6 +392,8 @@ class WebView extends React.Component {
source.uri = this.props.url;
}
+ const messagingEnabled = typeof this.props.onMessage === 'function';
+
var webView =
<RCTWebView
ref={RCT_WEBVIEW_REF}
@@ -397,6 +409,8 @@ class WebView extends React.Component {
onLoadingStart={this._onLoadingStart}
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
+ messagingEnabled={messagingEnabled}
+ onMessage={this._onMessage}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={this.props.scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
@@ -458,6 +472,24 @@ class WebView extends React.Component {
};
/**
+ * Posts a message to the web view, which will emit a `message` event.
+ * Accepts one argument, `data`, which must be a string.
+ *
+ * In your webview, you'll need to something like the following.
+ *
+ * ```js
+ * document.addEventListener('message', e => { document.title = e.data; });
+ * ```
+ */
+ postMessage = (data) => {
+ UIManager.dispatchViewManagerCommand(
+ this.getWebViewHandle(),
+ UIManager.RCTWebView.Commands.postMessage,
+ [String(data)]
+ );
+ };
+
+ /**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
@@ -502,13 +534,20 @@ class WebView extends React.Component {
});
this._updateNavigationState(event);
};
+
+ _onMessage = (event: Event) => {
+ var {onMessage} = this.props;
+ onMessage && onMessage(event);
+ }
}
var RCTWebView = requireNativeComponent('RCTWebView', WebView, {
nativeOnly: {
onLoadingStart: true,
onLoadingError: true,
onLoadingFinish: true,
+ onMessage: true,
+ messagingEnabled: PropTypes.bool,
},
});
@@ -34,12 +34,14 @@ shouldStartLoadForRequest:(NSMutableDictionary<NSString *, id> *)request
@property (nonatomic, copy) NSDictionary *source;
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, assign) BOOL automaticallyAdjustContentInsets;
+@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString *injectedJavaScript;
@property (nonatomic, assign) BOOL scalesPageToFit;
- (void)goForward;
- (void)goBack;
- (void)reload;
- (void)stopLoading;
+- (void)postMessage:(NSString *)message;
@end
@@ -20,13 +20,15 @@
#import "UIView+React.h"
NSString *const RCTJSNavigationScheme = @"react-js-navigation";
+NSString *const RCTJSPostMessageHost = @"postMessage";
@interface RCTWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onShouldStartLoadWithRequest;
+@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@end
@@ -82,6 +84,18 @@ - (void)stopLoading
[_webView stopLoading];
}
+- (void)postMessage:(NSString *)message
+{
+ NSDictionary *eventInitDict = @{
+ @"data": message,
+ };
+ NSString *source = [NSString
+ stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
+ RCTJSONStringify(eventInitDict, NULL)
+ ];
+ [_webView stringByEvaluatingJavaScriptFromString:source];
+}
+
- (void)setSource:(NSDictionary *)source
{
if (![_source isEqualToDictionary:source]) {
@@ -221,6 +235,18 @@ - (BOOL)webView:(__unused UIWebView *)webView shouldStartLoadWithRequest:(NSURLR
}
}
+ if (isJSNavigation && [request.URL.host isEqualToString:RCTJSPostMessageHost]) {
+ NSString *data = request.URL.query;
+ data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
+ data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+
+ NSMutableDictionary<NSString *, id> *event = [self baseEvent];
+ [event addEntriesFromDictionary: @{
+ @"data": data,
+ }];
+ _onMessage(event);
+ }
+
// JS Navigation handler
return !isJSNavigation;
}
@@ -248,6 +274,26 @@ - (void)webView:(__unused UIWebView *)webView didFailLoadWithError:(NSError *)er
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
+ if (_messagingEnabled) {
+ #if RCT_DEV
+ // See isNative in lodash
+ NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
+ BOOL postMessageIsNative = [
+ [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
+ isEqualToString:@"true"
+ ];
+ if (!postMessageIsNative) {
+ RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
+ }
+ #endif
+ NSString *source = [NSString stringWithFormat:
+ @"window.originalPostMessage = window.postMessage;"
+ "window.postMessage = function(data) {"
+ "window.location = '%@://%@?' + encodeURIComponent(String(data));"
+ "};", RCTJSNavigationScheme, RCTJSPostMessageHost
+ ];
+ [webView stringByEvaluatingJavaScriptFromString:source];
+ }
if (_injectedJavaScript != nil) {
NSString *jsEvaluationValue = [webView stringByEvaluatingJavaScriptFromString:_injectedJavaScript];
@@ -38,12 +38,14 @@ - (UIView *)view
RCT_REMAP_VIEW_PROPERTY(scrollEnabled, _webView.scrollView.scrollEnabled, BOOL)
RCT_REMAP_VIEW_PROPERTY(decelerationRate, _webView.scrollView.decelerationRate, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(scalesPageToFit, BOOL)
+RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
RCT_EXPORT_VIEW_PROPERTY(contentInset, UIEdgeInsets)
RCT_EXPORT_VIEW_PROPERTY(automaticallyAdjustContentInsets, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock)
+RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onShouldStartLoadWithRequest, RCTDirectEventBlock)
RCT_REMAP_VIEW_PROPERTY(allowsInlineMediaPlayback, _webView.allowsInlineMediaPlayback, BOOL)
RCT_REMAP_VIEW_PROPERTY(mediaPlaybackRequiresUserAction, _webView.mediaPlaybackRequiresUserAction, BOOL)
@@ -97,6 +99,18 @@ - (UIView *)view
}];
}
+RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
+{
+ [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWebView *> *viewRegistry) {
+ RCTWebView *view = viewRegistry[reactTag];
+ if (![view isKindOfClass:[RCTWebView class]]) {
+ RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
+ } else {
+ [view postMessage:message];
+ }
+ }];
+}
+
#pragma mark - Exported synchronous methods
- (BOOL)webView:(__unused RCTWebView *)webView
@@ -77,6 +77,7 @@
.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();
}
@@ -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'),
Oops, something went wrong.

0 comments on commit abb8ea3

Please sign in to comment.