feat(Android/iOS postMessage): refactoring the old postMessage implementation (#303)

fixes #29
fixes #272
fixes #221
fixes #105
fixes #66

BREAKING CHANGE: Communication from webview to react-native has been completely rewritten. React-native-webview will not use or override window.postMessage anymore. Reasons behind these changes can be found throughout so many issues that it made sense to go that way.

Instead of using window.postMessage(data, *), please now use window.ReactNativeWebView.postMessage(data).

Side note: if you wish to keep compatibility with the old version when you upgrade, you can use the injectedJavascript prop to do that:

const injectedJavascript = `(function() {
  window.postMessage = function(data) {
    window.ReactNativeWebView.postMessage(data);
  };
})()`;

Huge thanks to @jordansexton and @KoenLav!
This commit is contained in:
Thibault Malbranche 2019-02-01 18:37:28 +01:00 committed by GitHub
parent 79afbd697d
commit f3bdab5a22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 63 additions and 116 deletions

View File

@ -109,7 +109,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
protected static final String HTML_ENCODING = "UTF-8";
protected static final String HTML_MIME_TYPE = "text/html";
protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebview";
protected static final String HTTP_METHOD_POST = "POST";
@ -138,8 +138,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
if (!mLastLoadFailed) {
RNCWebView reactWebView = (RNCWebView) webView;
reactWebView.callInjectedJavaScript();
reactWebView.linkBridge();
emitFinishEvent(webView, url);
}
}
@ -239,6 +240,10 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
mContext = c;
}
/**
* This method is called whenever JavaScript running within the web view calls:
* - window[JAVASCRIPT_INTERFACE].postMessage
*/
@JavascriptInterface
public void postMessage(String message) {
mContext.onMessage(message);
@ -312,11 +317,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
}
messagingEnabled = enabled;
if (enabled) {
addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
linkBridge();
addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
} else {
removeJavascriptInterface(BRIDGE_NAME);
removeJavascriptInterface(JAVASCRIPT_INTERFACE);
}
}
@ -342,30 +347,6 @@ public class RNCWebViewManager 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");
}
}
});
}
evaluateJavascriptWithFallback("(" +
"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));
}

View File

@ -196,9 +196,9 @@ Function that is invoked when the `WebView` is loading.
### `onMessage`
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`.
Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`. Setting this property will inject this global into your webview.
`window.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.
`window.ReactNativeWebview.postMessage` accepts one argument, `data`, which will be available on the event object, `event.nativeEvent.data`. `data` must be a string.
| Type | Required |
| -------- | -------- |

View File

@ -11,7 +11,7 @@
NSString *const RNCJSNavigationScheme = @"react-js-navigation";
static NSString *const kPostMessageHost = @"postMessage";
static NSString *const MessageHandlerName = @"ReactNativeWebview";
@interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
@ -86,7 +86,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
@"data": message,
};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[_webView stringByEvaluatingJavaScriptFromString:source];
@ -236,7 +236,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
}
}
if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
if (isJSNavigation && [request.URL.host isEqualToString:MessageHandlerName]) {
NSString *data = request.URL.query;
data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
@ -246,7 +246,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
@"data": data,
}];
NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
NSString *source = [NSString stringWithFormat:@"window.%@.messageReceived();", MessageHandlerName];
[_webView stringByEvaluatingJavaScriptFromString:source];
@ -289,40 +289,28 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
- (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:
@"(function() {"
"window.originalPostMessage = window.postMessage;"
" var messageQueue = [];"
" var messagePending = false;"
"var messageQueue = [];"
"var messagePending = false;"
" function processQueue () {"
" if (!messageQueue.length || messagePending) return;"
" messagePending = true;"
" document.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
" }"
"function processQueue() {"
"if (!messageQueue.length || messagePending) return;"
"messagePending = true;"
"window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
"}"
"window.postMessage = function(data) {"
"messageQueue.push(String(data));"
"processQueue();"
"};"
"document.addEventListener('message:received', function(e) {"
"messagePending = false;"
"processQueue();"
"});"
"})();", RNCJSNavigationScheme, kPostMessageHost
" window.%@ = {"
" postMessage: function (data) {"
" messageQueue.push(String(data));"
" processQueue();"
" },"
" messageReceived: function () {"
" messagePending = false;"
" processQueue();"
" }"
" };"
"})();", RNCJSNavigationScheme, MessageHandlerName, MessageHandlerName
];
[webView stringByEvaluatingJavaScriptFromString:source];
}

View File

@ -13,7 +13,7 @@
#import "objc/runtime.h"
static NSString *const MessageHanderName = @"ReactNative";
static NSString *const MessageHandlerName = @"ReactNativeWebview";
// runtime trick to remove WKWebView keyboard default toolbar
// see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
@ -101,7 +101,22 @@ static NSString *const MessageHanderName = @"ReactNative";
wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
}
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
if (_messagingEnabled) {
[wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];
NSString *source = [NSString stringWithFormat:
@"window.%@ = {"
" postMessage: function (data) {"
" window.webkit.messageHandlers.%@.postMessage(String(data));"
" }"
"};", MessageHandlerName, MessageHandlerName
];
WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
[wkWebViewConfig.userContentController addUserScript:script];
}
wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
#if WEBKIT_IOS_10_APIS_AVAILABLE
wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
@ -148,7 +163,7 @@ static NSString *const MessageHanderName = @"ReactNative";
- (void)removeFromSuperview
{
if (_webView) {
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
[_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
[_webView removeObserver:self forKeyPath:@"estimatedProgress"];
[_webView removeFromSuperview];
_webView = nil;
@ -184,7 +199,7 @@ static NSString *const MessageHanderName = @"ReactNative";
/**
* This method is called whenever JavaScript running within the web view calls:
* - window.webkit.messageHandlers.[MessageHanderName].postMessage
* - window.webkit.messageHandlers[MessageHandlerName].postMessage
*/
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message
@ -253,7 +268,6 @@ static NSString *const MessageHanderName = @"ReactNative";
-(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
{
if (_webView == nil) {
_savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
return;
@ -264,6 +278,7 @@ static NSString *const MessageHanderName = @"ReactNative";
}
UIView* subview;
for (UIView* view in _webView.scrollView.subviews) {
if([[view.class description] hasPrefix:@"WK"])
subview = view;
@ -303,10 +318,10 @@ static NSString *const MessageHanderName = @"ReactNative";
{
NSDictionary *eventInitDict = @{@"data": message};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[self evaluateJS: source thenCall: nil];
[self injectJavaScript: source];
}
- (void)layoutSubviews
@ -520,7 +535,6 @@ static NSString *const MessageHanderName = @"ReactNative";
}];
}
/**
* Called when the navigation is complete.
* @see https://fburl.com/rtys6jlb
@ -528,35 +542,11 @@ static NSString *const MessageHanderName = @"ReactNative";
- (void) webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation
{
if (_messagingEnabled) {
#if RCT_DEV
// Implementation inspired by Lodash.isNative.
NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
[self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
if (! [result isEqualToString:@"true"]) {
RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
}
}];
#endif
NSString *source = [NSString stringWithFormat:
@"(function() {"
"window.originalPostMessage = window.postMessage;"
"window.postMessage = function(data) {"
"window.webkit.messageHandlers.%@.postMessage(String(data));"
"};"
"})();",
MessageHanderName
];
[self evaluateJS: source thenCall: nil];
}
if (_injectedJavaScript) {
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
NSMutableDictionary *event = [self baseEvent];
event[@"jsEvaluationValue"] = jsEvaluationValue;
if (self.onLoadingFinish) {
self.onLoadingFinish(event);
}

View File

@ -1,8 +0,0 @@
// !$*UTF8*$!
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:RNCWebView.xcodeproj">
</FileRef>
</Workspace>

View File

@ -157,9 +157,9 @@ class WebView extends React.Component<WebViewSharedProps, State> {
}
thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
domStorageEnabled={this.props.domStorageEnabled}
messagingEnabled={typeof this.props.onMessage === 'function'}
cacheEnabled={this.props.cacheEnabled}
onMessage={this.onMessage}
messagingEnabled={typeof this.props.onMessage === 'function'}
overScrollMode={this.props.overScrollMode}
contentInset={this.props.contentInset}
automaticallyAdjustContentInsets={

View File

@ -232,8 +232,6 @@ class WebView extends React.Component<WebViewSharedProps, State> {
source = { uri: this.props.url };
}
const messagingEnabled = typeof this.props.onMessage === 'function';
let NativeWebView = nativeConfig.component;
if (this.props.useWebKit) {
@ -268,8 +266,8 @@ class WebView extends React.Component<WebViewSharedProps, State> {
onLoadingFinish={this._onLoadingFinish}
onLoadingError={this._onLoadingError}
onLoadingProgress={this._onLoadingProgress}
messagingEnabled={messagingEnabled}
onMessage={this._onMessage}
messagingEnabled={typeof this.props.onMessage === 'function'}
onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
scalesPageToFit={scalesPageToFit}
allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}

View File

@ -424,13 +424,11 @@ export type WebViewSharedProps = $ReadOnly<{|
onNavigationStateChange?: (event: WebViewNavigation) => mixed,
/**
* 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`.
* Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`.
* Setting this property will inject this global into your webview.
*
* `window.postMessage` accepts one argument, `data`, which will be
* available on the event object, `event.nativeEvent.data`. `data`
* must be a string.
* `window.ReactNativeWebview.postMessage` accepts one argument, `data`, which will be
* available on the event object, `event.nativeEvent.data`. `data` must be a string.
*/
onMessage?: (event: WebViewMessageEvent) => mixed,