mirror of
https://github.com/status-im/react-native-webview.git
synced 2025-02-22 08:48:39 +00:00
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:
parent
79afbd697d
commit
f3bdab5a22
@ -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));
|
||||
}
|
||||
|
@ -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 |
|
||||
| -------- | -------- |
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -1,8 +0,0 @@
|
||||
// !$*UTF8*$!
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:RNCWebView.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
@ -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={
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user