Implement message passing!

Summary:
@public

This diff implements message passing between the `WKWebView` and React Native. As with `<WebView/>`, we can only send/receive strings.

**Usage:**
1. Set `messagingEnabled` to `true`.
1. To send data from the web view to React Native, call `postMessage(data)` within the web view. This forces React Native to execute the `onMessage` prop on the `WKWebView` component. `onMessage` will be called with an event `e`, where `e.nativeEvent.data` will be the data you passed into `postMessage`.
1. To send data from React Native to the web view, call `UIManager.dispatchViewManagerCommand` to dispatch the `UIManager.RCTWKWebView.Commands.postMessage` command. Look at [[ https://fburl.com/u1wusf2f | this part of the existing `<WebView/>` ]] component for more details. After you make the call, React Native will dispatch a `'message'` event to the `document` object within the webview. You can listen to the event by doing `document.addEventListener('message', callback)`. Let the event dispatched be `e`. Then, `e.data` is the data you sent over from React Native.

[[ P58627181 | This Playground.js ]] illustrates the usage.

Reviewed By: shergin

Differential Revision: D6304850

fbshipit-source-id: 29075ef753296e9fb5a9cddeb1ad0f4ff7e28650
This commit is contained in:
Ramanpreet Nara 2018-08-16 13:33:58 -07:00 committed by Facebook Github Bot
parent 3703927e7e
commit 7a6dd9807c
4 changed files with 91 additions and 10 deletions

View File

@ -18,6 +18,9 @@
@property (nonatomic, weak) id<RCTWKWebViewDelegate> delegate; @property (nonatomic, weak) id<RCTWKWebViewDelegate> delegate;
@property (nonatomic, copy) NSDictionary *source; @property (nonatomic, copy) NSDictionary *source;
@property (nonatomic, assign) BOOL messagingEnabled;
@property (nonatomic, copy) NSString *injectedJavaScript; @property (nonatomic, copy) NSString *injectedJavaScript;
- (void)postMessage:(NSString *)message;
@end @end

View File

@ -1,15 +1,15 @@
#import "RCTWKWebView.h" #import "RCTWKWebView.h"
#import <WebKit/WebKit.h> #import <WebKit/WebKit.h>
#import <React/RCTConvert.h> #import <React/RCTConvert.h>
#import "RCTAutoInsetsProtocol.h" #import "RCTAutoInsetsProtocol.h"
@interface RCTWKWebView () <WKUIDelegate, WKNavigationDelegate> static NSString *const MessageHanderName = @"ReactNative";
@interface RCTWKWebView () <WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler>
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart; @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish; @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError; @property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@property (nonatomic, copy) RCTDirectEventBlock onMessage;
@property (nonatomic, copy) WKWebView *webView; @property (nonatomic, copy) WKWebView *webView;
@end @end
@ -26,7 +26,11 @@
{ {
if ((self = [super initWithFrame:frame])) { if ((self = [super initWithFrame:frame])) {
super.backgroundColor = [UIColor clearColor]; super.backgroundColor = [UIColor clearColor];
_webView = [[WKWebView alloc] initWithFrame:self.bounds]; WKWebViewConfiguration *wkWebViewConfig = [WKWebViewConfiguration new];
wkWebViewConfig.userContentController = [WKUserContentController new];
[wkWebViewConfig.userContentController addScriptMessageHandler: self name: MessageHanderName];
_webView = [[WKWebView alloc] initWithFrame:self.bounds configuration: wkWebViewConfig];
_webView.UIDelegate = self; _webView.UIDelegate = self;
_webView.navigationDelegate = self; _webView.navigationDelegate = self;
[self addSubview:_webView]; [self addSubview:_webView];
@ -34,6 +38,20 @@
return self; return self;
} }
/**
* This method is called whenever JavaScript running within the web view calls:
* - window.webkit.messageHandlers.[MessageHanderName].postMessage
*/
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message
{
if (_onMessage != nil) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{@"data": message.body}];
_onMessage(event);
}
}
- (void)setSource:(NSDictionary *)source - (void)setSource:(NSDictionary *)source
{ {
if (![_source isEqualToDictionary:source]) { if (![_source isEqualToDictionary:source]) {
@ -67,9 +85,21 @@
} }
} }
- (void)postMessage:(NSString *)message
{
NSDictionary *eventInitDict = @{@"data": message};
NSString *source = [NSString
stringWithFormat:@"document.dispatchEvent(new MessageEvent('message', %@));",
RCTJSONStringify(eventInitDict, NULL)
];
[self evaluateJS: source thenCall: nil];
}
- (void)layoutSubviews - (void)layoutSubviews
{ {
[super layoutSubviews]; [super layoutSubviews];
// Ensure webview takes the position and dimensions of RCTWKWebView
_webView.frame = self.bounds; _webView.frame = self.bounds;
} }
@ -161,7 +191,7 @@
thenCall: (void (^)(NSString*)) callback thenCall: (void (^)(NSString*)) callback
{ {
[self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) { [self.webView evaluateJavaScript: js completionHandler: ^(id result, NSError *error) {
if (error == nil) { if (error == nil && callback != nil) {
callback([NSString stringWithFormat:@"%@", result]); callback([NSString stringWithFormat:@"%@", result]);
} }
}]; }];
@ -175,6 +205,31 @@
- (void) webView:(WKWebView *)webView - (void) webView:(WKWebView *)webView
didFinishNavigation:(WKNavigation *)navigation 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) { if (_injectedJavaScript) {
[self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) { [self evaluateJS: _injectedJavaScript thenCall: ^(NSString *jsEvaluationValue) {
NSMutableDictionary *event = [self baseEvent]; NSMutableDictionary *event = [self baseEvent];

View File

@ -0,0 +1,6 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#import <React/RCTViewManager.h>
@interface RCTWKWebViewManager : RCTViewManager
@end

View File

@ -1,8 +1,7 @@
#import "RCTViewManager.h" #import "RCTWKWebViewManager.h"
#import "RCTWKWebView.h"
@interface RCTWKWebViewManager : RCTViewManager #import "RCTUIManager.h"
@end #import "RCTWKWebView.h"
@implementation RCTWKWebViewManager @implementation RCTWKWebViewManager
@ -19,4 +18,22 @@ RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString) RCT_EXPORT_VIEW_PROPERTY(injectedJavaScript, NSString)
/**
* Expose methods to enable messaging the webview.
*/
RCT_EXPORT_VIEW_PROPERTY(messagingEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onMessage, RCTDirectEventBlock)
RCT_EXPORT_METHOD(postMessage:(nonnull NSNumber *)reactTag message:(NSString *)message)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, RCTWKWebView *> *viewRegistry) {
RCTWKWebView *view = viewRegistry[reactTag];
if (![view isKindOfClass:[RCTWKWebView class]]) {
RCTLogError(@"Invalid view returned from registry, expecting RCTWebView, got: %@", view);
} else {
[view postMessage:message];
}
}];
}
@end @end