Browse Source

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!
Thibault Malbranche 5 years ago
parent
commit
f3bdab5a22
No account linked to committer's email address

+ 10
- 29
android/src/main/java/com/reactnativecommunity/webview/RNCWebViewManager.java View File

109
 
109
 
110
   protected static final String HTML_ENCODING = "UTF-8";
110
   protected static final String HTML_ENCODING = "UTF-8";
111
   protected static final String HTML_MIME_TYPE = "text/html";
111
   protected static final String HTML_MIME_TYPE = "text/html";
112
-  protected static final String BRIDGE_NAME = "__REACT_WEB_VIEW_BRIDGE";
112
+  protected static final String JAVASCRIPT_INTERFACE = "ReactNativeWebview";
113
 
113
 
114
   protected static final String HTTP_METHOD_POST = "POST";
114
   protected static final String HTTP_METHOD_POST = "POST";
115
 
115
 
138
 
138
 
139
       if (!mLastLoadFailed) {
139
       if (!mLastLoadFailed) {
140
         RNCWebView reactWebView = (RNCWebView) webView;
140
         RNCWebView reactWebView = (RNCWebView) webView;
141
+
141
         reactWebView.callInjectedJavaScript();
142
         reactWebView.callInjectedJavaScript();
142
-        reactWebView.linkBridge();
143
+
143
         emitFinishEvent(webView, url);
144
         emitFinishEvent(webView, url);
144
       }
145
       }
145
     }
146
     }
239
         mContext = c;
240
         mContext = c;
240
       }
241
       }
241
 
242
 
243
+      /**
244
+       * This method is called whenever JavaScript running within the web view calls:
245
+       *   - window[JAVASCRIPT_INTERFACE].postMessage
246
+       */
242
       @JavascriptInterface
247
       @JavascriptInterface
243
       public void postMessage(String message) {
248
       public void postMessage(String message) {
244
         mContext.onMessage(message);
249
         mContext.onMessage(message);
312
       }
317
       }
313
 
318
 
314
       messagingEnabled = enabled;
319
       messagingEnabled = enabled;
320
+
315
       if (enabled) {
321
       if (enabled) {
316
-        addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
317
-        linkBridge();
322
+        addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
318
       } else {
323
       } else {
319
-        removeJavascriptInterface(BRIDGE_NAME);
324
+        removeJavascriptInterface(JAVASCRIPT_INTERFACE);
320
       }
325
       }
321
     }
326
     }
322
 
327
 
342
       }
347
       }
343
     }
348
     }
344
 
349
 
345
-    public void linkBridge() {
346
-      if (messagingEnabled) {
347
-        if (ReactBuildConfig.DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
348
-          // See isNative in lodash
349
-          String testPostMessageNative = "String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
350
-          evaluateJavascript(testPostMessageNative, new ValueCallback<String>() {
351
-            @Override
352
-            public void onReceiveValue(String value) {
353
-              if (value.equals("true")) {
354
-                FLog.w(ReactConstants.TAG, "Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
355
-              }
356
-            }
357
-          });
358
-        }
359
-
360
-        evaluateJavascriptWithFallback("(" +
361
-          "window.originalPostMessage = window.postMessage," +
362
-          "window.postMessage = function(data) {" +
363
-            BRIDGE_NAME + ".postMessage(String(data));" +
364
-          "}" +
365
-        ")");
366
-      }
367
-    }
368
-
369
     public void onMessage(String message) {
350
     public void onMessage(String message) {
370
       dispatchEvent(this, new TopMessageEvent(this.getId(), message));
351
       dispatchEvent(this, new TopMessageEvent(this.getId(), message));
371
     }
352
     }

+ 2
- 2
docs/Reference.md View File

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

+ 24
- 36
ios/RNCUIWebView.m View File

11
 
11
 
12
 NSString *const RNCJSNavigationScheme = @"react-js-navigation";
12
 NSString *const RNCJSNavigationScheme = @"react-js-navigation";
13
 
13
 
14
-static NSString *const kPostMessageHost = @"postMessage";
14
+static NSString *const MessageHandlerName = @"ReactNativeWebview";
15
 
15
 
16
 @interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
16
 @interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
17
 
17
 
86
     @"data": message,
86
     @"data": message,
87
   };
87
   };
88
   NSString *source = [NSString
88
   NSString *source = [NSString
89
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
89
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
90
     RCTJSONStringify(eventInitDict, NULL)
90
     RCTJSONStringify(eventInitDict, NULL)
91
   ];
91
   ];
92
   [_webView stringByEvaluatingJavaScriptFromString:source];
92
   [_webView stringByEvaluatingJavaScriptFromString:source];
236
     }
236
     }
237
   }
237
   }
238
 
238
 
239
-  if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
239
+  if (isJSNavigation && [request.URL.host isEqualToString:MessageHandlerName]) {
240
     NSString *data = request.URL.query;
240
     NSString *data = request.URL.query;
241
     data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
241
     data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
242
     data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
242
     data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
246
       @"data": data,
246
       @"data": data,
247
     }];
247
     }];
248
 
248
 
249
-    NSString *source = @"document.dispatchEvent(new MessageEvent('message:received'));";
249
+    NSString *source = [NSString stringWithFormat:@"window.%@.messageReceived();", MessageHandlerName];
250
 
250
 
251
     [_webView stringByEvaluatingJavaScriptFromString:source];
251
     [_webView stringByEvaluatingJavaScriptFromString:source];
252
 
252
 
289
 - (void)webViewDidFinishLoad:(UIWebView *)webView
289
 - (void)webViewDidFinishLoad:(UIWebView *)webView
290
 {
290
 {
291
   if (_messagingEnabled) {
291
   if (_messagingEnabled) {
292
-    #if RCT_DEV
293
-    // See isNative in lodash
294
-    NSString *testPostMessageNative = @"String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage')";
295
-    BOOL postMessageIsNative = [
296
-      [webView stringByEvaluatingJavaScriptFromString:testPostMessageNative]
297
-      isEqualToString:@"true"
298
-    ];
299
-    if (!postMessageIsNative) {
300
-      RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
301
-    }
302
-    #endif
303
     NSString *source = [NSString stringWithFormat:
292
     NSString *source = [NSString stringWithFormat:
304
       @"(function() {"
293
       @"(function() {"
305
-        "window.originalPostMessage = window.postMessage;"
306
-
307
-        "var messageQueue = [];"
308
-        "var messagePending = false;"
309
-
310
-        "function processQueue() {"
311
-          "if (!messageQueue.length || messagePending) return;"
312
-          "messagePending = true;"
313
-          "window.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
314
-        "}"
315
-
316
-        "window.postMessage = function(data) {"
317
-          "messageQueue.push(String(data));"
318
-          "processQueue();"
319
-        "};"
320
-
321
-        "document.addEventListener('message:received', function(e) {"
322
-          "messagePending = false;"
323
-          "processQueue();"
324
-        "});"
325
-      "})();", RNCJSNavigationScheme, kPostMessageHost
294
+       "  var messageQueue = [];"
295
+       "  var messagePending = false;"
296
+
297
+       "  function processQueue () {"
298
+       "    if (!messageQueue.length || messagePending) return;"
299
+       "    messagePending = true;"
300
+       "    document.location = '%@://%@?' + encodeURIComponent(messageQueue.shift());"
301
+       "  }"
302
+
303
+       "  window.%@ = {"
304
+       "    postMessage: function (data) {"
305
+       "      messageQueue.push(String(data));"
306
+       "      processQueue();"
307
+       "    },"
308
+       "    messageReceived: function () {"
309
+       "      messagePending = false;"
310
+       "      processQueue();"
311
+       "    }"
312
+       "  };"
313
+       "})();", RNCJSNavigationScheme, MessageHandlerName, MessageHandlerName
326
     ];
314
     ];
327
     [webView stringByEvaluatingJavaScriptFromString:source];
315
     [webView stringByEvaluatingJavaScriptFromString:source];
328
   }
316
   }

+ 23
- 33
ios/RNCWKWebView.m View File

13
 
13
 
14
 #import "objc/runtime.h"
14
 #import "objc/runtime.h"
15
 
15
 
16
-static NSString *const MessageHanderName = @"ReactNative";
16
+static NSString *const MessageHandlerName = @"ReactNativeWebview";
17
 
17
 
18
 // runtime trick to remove WKWebView keyboard default toolbar
18
 // runtime trick to remove WKWebView keyboard default toolbar
19
 // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
19
 // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
101
       wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
101
       wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
102
     }
102
     }
103
     wkWebViewConfig.userContentController = [WKUserContentController new];
103
     wkWebViewConfig.userContentController = [WKUserContentController new];
104
-    [wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
104
+
105
+    if (_messagingEnabled) {
106
+      [wkWebViewConfig.userContentController addScriptMessageHandler:self name:MessageHandlerName];
107
+
108
+      NSString *source = [NSString stringWithFormat:
109
+        @"window.%@ = {"
110
+         "  postMessage: function (data) {"
111
+         "    window.webkit.messageHandlers.%@.postMessage(String(data));"
112
+         "  }"
113
+         "};", MessageHandlerName, MessageHandlerName
114
+      ];
115
+
116
+      WKUserScript *script = [[WKUserScript alloc] initWithSource:source injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES];
117
+      [wkWebViewConfig.userContentController addUserScript:script];
118
+    }
119
+
105
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
120
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
106
 #if WEBKIT_IOS_10_APIS_AVAILABLE
121
 #if WEBKIT_IOS_10_APIS_AVAILABLE
107
     wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
122
     wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
148
 - (void)removeFromSuperview
163
 - (void)removeFromSuperview
149
 {
164
 {
150
     if (_webView) {
165
     if (_webView) {
151
-        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
166
+        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
152
         [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
167
         [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
153
         [_webView removeFromSuperview];
168
         [_webView removeFromSuperview];
154
         _webView = nil;
169
         _webView = nil;
184
 
199
 
185
 /**
200
 /**
186
  * This method is called whenever JavaScript running within the web view calls:
201
  * This method is called whenever JavaScript running within the web view calls:
187
- *   - window.webkit.messageHandlers.[MessageHanderName].postMessage
202
+ *   - window.webkit.messageHandlers[MessageHandlerName].postMessage
188
  */
203
  */
189
 - (void)userContentController:(WKUserContentController *)userContentController
204
 - (void)userContentController:(WKUserContentController *)userContentController
190
        didReceiveScriptMessage:(WKScriptMessage *)message
205
        didReceiveScriptMessage:(WKScriptMessage *)message
253
 
268
 
254
 -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
269
 -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
255
 {
270
 {
256
-
257
     if (_webView == nil) {
271
     if (_webView == nil) {
258
         _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
272
         _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
259
         return;
273
         return;
264
     }
278
     }
265
 
279
 
266
     UIView* subview;
280
     UIView* subview;
281
+
267
     for (UIView* view in _webView.scrollView.subviews) {
282
     for (UIView* view in _webView.scrollView.subviews) {
268
         if([[view.class description] hasPrefix:@"WK"])
283
         if([[view.class description] hasPrefix:@"WK"])
269
             subview = view;
284
             subview = view;
303
 {
318
 {
304
   NSDictionary *eventInitDict = @{@"data": message};
319
   NSDictionary *eventInitDict = @{@"data": message};
305
   NSString *source = [NSString
320
   NSString *source = [NSString
306
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
321
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
307
     RCTJSONStringify(eventInitDict, NULL)
322
     RCTJSONStringify(eventInitDict, NULL)
308
   ];
323
   ];
309
-  [self evaluateJS: source thenCall: nil];
324
+  [self injectJavaScript: source];
310
 }
325
 }
311
 
326
 
312
 - (void)layoutSubviews
327
 - (void)layoutSubviews
520
   }];
535
   }];
521
 }
536
 }
522
 
537
 
523
-
524
 /**
538
 /**
525
  * Called when the navigation is complete.
539
  * Called when the navigation is complete.
526
  * @see https://fburl.com/rtys6jlb
540
  * @see https://fburl.com/rtys6jlb
528
 - (void)      webView:(WKWebView *)webView
542
 - (void)      webView:(WKWebView *)webView
529
   didFinishNavigation:(WKNavigation *)navigation
543
   didFinishNavigation:(WKNavigation *)navigation
530
 {
544
 {
531
-  if (_messagingEnabled) {
532
-    #if RCT_DEV
533
-
534
-    // Implementation inspired by Lodash.isNative.
535
-    NSString *isPostMessageNative = @"String(String(window.postMessage) === String(Object.hasOwnProperty).replace('hasOwnProperty', 'postMessage'))";
536
-    [self evaluateJS: isPostMessageNative thenCall: ^(NSString *result) {
537
-      if (! [result isEqualToString:@"true"]) {
538
-        RCTLogError(@"Setting onMessage on a WebView overrides existing values of window.postMessage, but a previous value was defined");
539
-      }
540
-    }];
541
-    #endif
542
-
543
-    NSString *source = [NSString stringWithFormat:
544
-      @"(function() {"
545
-        "window.originalPostMessage = window.postMessage;"
546
-
547
-        "window.postMessage = function(data) {"
548
-          "window.webkit.messageHandlers.%@.postMessage(String(data));"
549
-        "};"
550
-      "})();",
551
-      MessageHanderName
552
-    ];
553
-    [self evaluateJS: source thenCall: nil];
554
-  }
555
-
556
   if (_injectedJavaScript) {
545
   if (_injectedJavaScript) {
557
     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
546
     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
558
       NSMutableDictionary *event = [self baseEvent];
547
       NSMutableDictionary *event = [self baseEvent];
559
       event[@"jsEvaluationValue"] = jsEvaluationValue;
548
       event[@"jsEvaluationValue"] = jsEvaluationValue;
549
+
560
       if (self.onLoadingFinish) {
550
       if (self.onLoadingFinish) {
561
         self.onLoadingFinish(event);
551
         self.onLoadingFinish(event);
562
       }
552
       }

+ 0
- 8
ios/RNCWebView.xcworkspace/contents.xcworkspacedata View File

1
-// !$*UTF8*$!
2
-<?xml version="1.0" encoding="UTF-8"?>
3
-<Workspace
4
-   version = "1.0">
5
-   <FileRef
6
-      location = "group:RNCWebView.xcodeproj">
7
-   </FileRef>
8
-</Workspace>

+ 1
- 1
js/WebView.android.js View File

157
         }
157
         }
158
         thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
158
         thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
159
         domStorageEnabled={this.props.domStorageEnabled}
159
         domStorageEnabled={this.props.domStorageEnabled}
160
-        messagingEnabled={typeof this.props.onMessage === 'function'}
161
         cacheEnabled={this.props.cacheEnabled}
160
         cacheEnabled={this.props.cacheEnabled}
162
         onMessage={this.onMessage}
161
         onMessage={this.onMessage}
162
+        messagingEnabled={typeof this.props.onMessage === 'function'}
163
         overScrollMode={this.props.overScrollMode}
163
         overScrollMode={this.props.overScrollMode}
164
         contentInset={this.props.contentInset}
164
         contentInset={this.props.contentInset}
165
         automaticallyAdjustContentInsets={
165
         automaticallyAdjustContentInsets={

+ 1
- 3
js/WebView.ios.js View File

232
       source = { uri: this.props.url };
232
       source = { uri: this.props.url };
233
     }
233
     }
234
 
234
 
235
-    const messagingEnabled = typeof this.props.onMessage === 'function';
236
-
237
     let NativeWebView = nativeConfig.component;
235
     let NativeWebView = nativeConfig.component;
238
 
236
 
239
     if (this.props.useWebKit) {
237
     if (this.props.useWebKit) {
268
         onLoadingFinish={this._onLoadingFinish}
266
         onLoadingFinish={this._onLoadingFinish}
269
         onLoadingError={this._onLoadingError}
267
         onLoadingError={this._onLoadingError}
270
         onLoadingProgress={this._onLoadingProgress}
268
         onLoadingProgress={this._onLoadingProgress}
271
-        messagingEnabled={messagingEnabled}
272
         onMessage={this._onMessage}
269
         onMessage={this._onMessage}
270
+        messagingEnabled={typeof this.props.onMessage === 'function'}
273
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
271
         onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
274
         scalesPageToFit={scalesPageToFit}
272
         scalesPageToFit={scalesPageToFit}
275
         allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
273
         allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}

+ 4
- 6
js/WebViewTypes.js View File

424
   onNavigationStateChange?: (event: WebViewNavigation) => mixed,
424
   onNavigationStateChange?: (event: WebViewNavigation) => mixed,
425
 
425
 
426
   /**
426
   /**
427
-   * A function that is invoked when the webview calls `window.postMessage`.
428
-   * Setting this property will inject a `postMessage` global into your
429
-   * webview, but will still call pre-existing values of `postMessage`.
427
+   * Function that is invoked when the webview calls `window.ReactNativeWebview.postMessage`.
428
+   * Setting this property will inject this global into your webview.
430
    *
429
    *
431
-   * `window.postMessage` accepts one argument, `data`, which will be
432
-   * available on the event object, `event.nativeEvent.data`. `data`
433
-   * must be a string.
430
+   * `window.ReactNativeWebview.postMessage` accepts one argument, `data`, which will be
431
+   * available on the event object, `event.nativeEvent.data`. `data` must be a string.
434
    */
432
    */
435
   onMessage?: (event: WebViewMessageEvent) => mixed,
433
   onMessage?: (event: WebViewMessageEvent) => mixed,
436
 
434