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,7 +109,7 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
109 109
 
110 110
   protected static final String HTML_ENCODING = "UTF-8";
111 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 114
   protected static final String HTTP_METHOD_POST = "POST";
115 115
 
@@ -138,8 +138,9 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
138 138
 
139 139
       if (!mLastLoadFailed) {
140 140
         RNCWebView reactWebView = (RNCWebView) webView;
141
+
141 142
         reactWebView.callInjectedJavaScript();
142
-        reactWebView.linkBridge();
143
+
143 144
         emitFinishEvent(webView, url);
144 145
       }
145 146
     }
@@ -239,6 +240,10 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
239 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 247
       @JavascriptInterface
243 248
       public void postMessage(String message) {
244 249
         mContext.onMessage(message);
@@ -312,11 +317,11 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
312 317
       }
313 318
 
314 319
       messagingEnabled = enabled;
320
+
315 321
       if (enabled) {
316
-        addJavascriptInterface(createRNCWebViewBridge(this), BRIDGE_NAME);
317
-        linkBridge();
322
+        addJavascriptInterface(createRNCWebViewBridge(this), JAVASCRIPT_INTERFACE);
318 323
       } else {
319
-        removeJavascriptInterface(BRIDGE_NAME);
324
+        removeJavascriptInterface(JAVASCRIPT_INTERFACE);
320 325
       }
321 326
     }
322 327
 
@@ -342,30 +347,6 @@ public class RNCWebViewManager extends SimpleViewManager<WebView> {
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 350
     public void onMessage(String message) {
370 351
       dispatchEvent(this, new TopMessageEvent(this.getId(), message));
371 352
     }

+ 2
- 2
docs/Reference.md View File

@@ -196,9 +196,9 @@ Function that is invoked when the `WebView` is loading.
196 196
 
197 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 203
 | Type     | Required |
204 204
 | -------- | -------- |

+ 24
- 36
ios/RNCUIWebView.m View File

@@ -11,7 +11,7 @@
11 11
 
12 12
 NSString *const RNCJSNavigationScheme = @"react-js-navigation";
13 13
 
14
-static NSString *const kPostMessageHost = @"postMessage";
14
+static NSString *const MessageHandlerName = @"ReactNativeWebview";
15 15
 
16 16
 @interface RNCUIWebView () <UIWebViewDelegate, RCTAutoInsetsProtocol>
17 17
 
@@ -86,7 +86,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
86 86
     @"data": message,
87 87
   };
88 88
   NSString *source = [NSString
89
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
89
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
90 90
     RCTJSONStringify(eventInitDict, NULL)
91 91
   ];
92 92
   [_webView stringByEvaluatingJavaScriptFromString:source];
@@ -236,7 +236,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
236 236
     }
237 237
   }
238 238
 
239
-  if (isJSNavigation && [request.URL.host isEqualToString:kPostMessageHost]) {
239
+  if (isJSNavigation && [request.URL.host isEqualToString:MessageHandlerName]) {
240 240
     NSString *data = request.URL.query;
241 241
     data = [data stringByReplacingOccurrencesOfString:@"+" withString:@" "];
242 242
     data = [data stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
@@ -246,7 +246,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
246 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 251
     [_webView stringByEvaluatingJavaScriptFromString:source];
252 252
 
@@ -289,40 +289,28 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:(NSCoder *)aDecoder)
289 289
 - (void)webViewDidFinishLoad:(UIWebView *)webView
290 290
 {
291 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 292
     NSString *source = [NSString stringWithFormat:
304 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 315
     [webView stringByEvaluatingJavaScriptFromString:source];
328 316
   }

+ 23
- 33
ios/RNCWKWebView.m View File

@@ -13,7 +13,7 @@
13 13
 
14 14
 #import "objc/runtime.h"
15 15
 
16
-static NSString *const MessageHanderName = @"ReactNative";
16
+static NSString *const MessageHandlerName = @"ReactNativeWebview";
17 17
 
18 18
 // runtime trick to remove WKWebView keyboard default toolbar
19 19
 // see: http://stackoverflow.com/questions/19033292/ios-7-uiwebview-keyboard-issue/19042279#19042279
@@ -101,7 +101,22 @@ static NSString *const MessageHanderName = @"ReactNative";
101 101
       wkWebViewConfig.processPool = [[RNCWKProcessPoolManager sharedManager] sharedProcessPool];
102 102
     }
103 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 120
     wkWebViewConfig.allowsInlineMediaPlayback = _allowsInlineMediaPlayback;
106 121
 #if WEBKIT_IOS_10_APIS_AVAILABLE
107 122
     wkWebViewConfig.mediaTypesRequiringUserActionForPlayback = _mediaPlaybackRequiresUserAction
@@ -148,7 +163,7 @@ static NSString *const MessageHanderName = @"ReactNative";
148 163
 - (void)removeFromSuperview
149 164
 {
150 165
     if (_webView) {
151
-        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHanderName];
166
+        [_webView.configuration.userContentController removeScriptMessageHandlerForName:MessageHandlerName];
152 167
         [_webView removeObserver:self forKeyPath:@"estimatedProgress"];
153 168
         [_webView removeFromSuperview];
154 169
         _webView = nil;
@@ -184,7 +199,7 @@ static NSString *const MessageHanderName = @"ReactNative";
184 199
 
185 200
 /**
186 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 204
 - (void)userContentController:(WKUserContentController *)userContentController
190 205
        didReceiveScriptMessage:(WKScriptMessage *)message
@@ -253,7 +268,6 @@ static NSString *const MessageHanderName = @"ReactNative";
253 268
 
254 269
 -(void)setHideKeyboardAccessoryView:(BOOL)hideKeyboardAccessoryView
255 270
 {
256
-
257 271
     if (_webView == nil) {
258 272
         _savedHideKeyboardAccessoryView = hideKeyboardAccessoryView;
259 273
         return;
@@ -264,6 +278,7 @@ static NSString *const MessageHanderName = @"ReactNative";
264 278
     }
265 279
 
266 280
     UIView* subview;
281
+
267 282
     for (UIView* view in _webView.scrollView.subviews) {
268 283
         if([[view.class description] hasPrefix:@"WK"])
269 284
             subview = view;
@@ -303,10 +318,10 @@ static NSString *const MessageHanderName = @"ReactNative";
303 318
 {
304 319
   NSDictionary *eventInitDict = @{@"data": message};
305 320
   NSString *source = [NSString
306
-    stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
321
+    stringWithFormat:@"window.dispatchEvent(new MessageEvent('message', %@));",
307 322
     RCTJSONStringify(eventInitDict, NULL)
308 323
   ];
309
-  [self evaluateJS: source thenCall: nil];
324
+  [self injectJavaScript: source];
310 325
 }
311 326
 
312 327
 - (void)layoutSubviews
@@ -520,7 +535,6 @@ static NSString *const MessageHanderName = @"ReactNative";
520 535
   }];
521 536
 }
522 537
 
523
-
524 538
 /**
525 539
  * Called when the navigation is complete.
526 540
  * @see https://fburl.com/rtys6jlb
@@ -528,35 +542,11 @@ static NSString *const MessageHanderName = @"ReactNative";
528 542
 - (void)      webView:(WKWebView *)webView
529 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 545
   if (_injectedJavaScript) {
557 546
     [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
558 547
       NSMutableDictionary *event = [self baseEvent];
559 548
       event[@"jsEvaluationValue"] = jsEvaluationValue;
549
+
560 550
       if (self.onLoadingFinish) {
561 551
         self.onLoadingFinish(event);
562 552
       }

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

@@ -1,8 +0,0 @@
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,9 +157,9 @@ class WebView extends React.Component<WebViewSharedProps, State> {
157 157
         }
158 158
         thirdPartyCookiesEnabled={this.props.thirdPartyCookiesEnabled}
159 159
         domStorageEnabled={this.props.domStorageEnabled}
160
-        messagingEnabled={typeof this.props.onMessage === 'function'}
161 160
         cacheEnabled={this.props.cacheEnabled}
162 161
         onMessage={this.onMessage}
162
+        messagingEnabled={typeof this.props.onMessage === 'function'}
163 163
         overScrollMode={this.props.overScrollMode}
164 164
         contentInset={this.props.contentInset}
165 165
         automaticallyAdjustContentInsets={

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

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

+ 4
- 6
js/WebViewTypes.js View File

@@ -424,13 +424,11 @@ export type WebViewSharedProps = $ReadOnly<{|
424 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 433
   onMessage?: (event: WebViewMessageEvent) => mixed,
436 434