Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

window.postMessage is not ready right away because of WKUserScriptInjectionTime atDocumentEnd #166

Closed
henrylearn2rock opened this issue Jul 19, 2018 · 16 comments

Comments

@henrylearn2rock
Copy link

henrylearn2rock commented Jul 19, 2018

Version: 1.21
(iOS: 11.4)

window.webkit.messageHandlers.reactNative.postMessage works as documented, but window.postMessage({message: 'hello!'}) throws Not enough arguments

When I try window.postMessage({message: 'hello!'}, '*'), the message is not being sent to RN.

Thanks!

@henrylearn2rock henrylearn2rock changed the title window.postMessage does not window.postMessage does not work Jul 19, 2018
@henrylearn2rock
Copy link
Author

It works, but not right away.
Looks like there is a delay similar to facebook/react-native#11594

@shirakaba
Copy link
Collaborator

shirakaba commented Jul 20, 2018

window.postMessage({message: 'hello!'}) throws Not enough arguments

Indeed, it is required to specify the target origin: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

In this library, window.postMessage(data) is an alias for:

window.webkit.messageHandlers.reactNative.postMessage(data);

... Indeed, this alias method shouldn't need a target origin; looks like you're using window.postMessage() (the actual ECMAScript window.postMessage() method) before the alias has been set, which may be why it complains about requiring an origin argument.

The alias is only set upon the - (void)webView:(WKWebView *)webView didFinishNavigation:(__unused WKNavigation *)navigation callback in ios/RCTWKWebView/RCTWKWebView.m. So the whole webpage will need to be loaded before the window.postMessage(data) alias is available.

As a quick guess, you could try running the same aliasing code in an earlier part of the webpage loading lifecycle – in the new injectJavaScript prop:

window.originalPostMessage = window.postMessage;
window.postMessage = function() {
    return window.webkit.messageHandlers.reactNative.postMessage.apply(window.webkit.messageHandlers.reactNative, arguments);
};

Disclaimer: there may well be a valid reason for this aliasing happening only after the page has been loaded; I haven't read the code in detail.

You could consider submitting a PR that moves the aliasing code to that earlier part of the webpage loading lifecycle if you could convincingly determine that there are no issues in doing so.

... Otherwise, just use the non-aliased window.webkit.messageHandlers.reactNative.postMessage method!

@henrylearn2rock henrylearn2rock changed the title window.postMessage does not work window.postMessage not ready right away because of WKUserScriptInjectionTime atDocumentEnd Jul 20, 2018
@henrylearn2rock henrylearn2rock changed the title window.postMessage not ready right away because of WKUserScriptInjectionTime atDocumentEnd window.postMessage is not ready right away because of WKUserScriptInjectionTime atDocumentEnd Jul 20, 2018
@henrylearn2rock
Copy link
Author

Looks like WKUserScriptInjectionTime.atDocumentStart would solve this?

ref: https://developer.apple.com/documentation/webkit/wkuserscript

@shirakaba
Copy link
Collaborator

That's the webpage loading lifecycle event that I was referring to, yes. The injectJavaScript prop injects JavaScript upon that lifecycle event.

@henrylearn2rock
Copy link
Author

https://github.com/CRAlpha/react-native-wkwebview/blob/master/ios/RCTWKWebView/RCTWKWebView.m#L141

Looks like currently the origin param is being dropped?

@shirakaba
Copy link
Collaborator

Yes. It seems that if messagingEnabled is true, then postMessage(data, origin) will be repurposed to simply run the function window.webkit.messageHandlers.reactNative.postMessage(data). origin effectively becomes irrelevant, because the message is implicitly being posted to a certain message handler that isn't interested in it.

I'll be the first to say that I don't see why anyone would ever want to enable such behaviour (I dislike non-explicit behaviours), but I guess it's just a reproduction of what the <WebView> component does.

@insraq
Copy link
Contributor

insraq commented Jul 20, 2018

Yes. This is added to be "compatible" with . I don't like it either. But it helps people migrating from WebView.

@shirakaba
Copy link
Collaborator

shirakaba commented Jul 20, 2018

@insraq Do you support moving this declaration... :

window.originalPostMessage = window.postMessage;
window.postMessage = function() {
    return window.webkit.messageHandlers.reactNative.postMessage.apply(window.webkit.messageHandlers.reactNative, arguments);
};

... to the WKUserScriptInjectionTime.atDocumentStart part of the webpage loading lifecycle?

@insraq
Copy link
Contributor

insraq commented Jul 21, 2018

I think it's fine. Last time someone gave it a try, it did not work out as planned (either this did not work or it broke some other functions). But do feel free to submit a PR and give it a go. I have added you as a contributor so you should be able to merge it. Let me know when you think it's ready for a npm release

@shirakaba
Copy link
Collaborator

shirakaba commented Jul 21, 2018

@insraq Thank you for adding me as a collaborator!

@henrylearn2rock Are you confident to submit a PR for this, or would you prefer to leave it with me?

Looks like it might be as simple as changing the lines in RCTWKWebView.m:

- (void)setupPostMessageScript {
  if (_messagingEnabled) {
    NSString *source=@"window.originalPostMessage = window.postMessage; window.postMessage = function (data) { window.webkit.messageHandlers.reactNative.postMessage(data); }";
    WKUserScript *script = [[WKUserScript alloc] initWithSource:source
                           injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                               forMainFrameOnly:_injectedJavaScriptForMainFrameOnly];
    [_webView.configuration.userContentController addUserScript:script];
  }
}

... replacing WKUserScriptInjectionTimeAtDocumentEnd with WKUserScriptInjectionTimeAtDocumentStart.

Don't think WKWebView.ios.js needs changing.

@henrylearn2rock First, could you try making that one-line change I've mentioned above in your own install of this project (no need to fork the repo, for this initial experiment: just go into YOUR_PROJECT/node_modules/react-native-wkwebview-reborn/ios/RCTWKWebView/RCTWKWebView.m), change that one line, and run your ios app target in YOUR_PROJECT/ios/YOUR_PROJECT.xcodeproj again. Please report back about whether your issues are solved.

@henrylearn2rock
Copy link
Author

henrylearn2rock commented Jul 21, 2018

Good news, it works as expected. After changing that line to WKUserScriptInjectionTimeAtDocumentStart
I tried alert(window.postMessage.length) right away in the JS and it shows 1, meaning it has already been overridden with the one expecting 1 argument.

Then I tried window.postMessage('test') and it ran flawlessly. console.log confirms it arrived over at RN side. So looks like this simple fix works!

@shirakaba I haven't cloned this repo, so pls go ahead! 👍 Thanks!

@shirakaba
Copy link
Collaborator

I've started a PR for this at #167 . Note that I haven't tested anything on my end yet (not sure when I'll be free to do so yet).

@henrylearn2rock What props would I need to enter in order to test this? I haven't used this feature before. I see a reference to messagingEnabled in the source code, but the documentation doesn't mention it. Do I need to set that to true as well as setting the onMessage callback?

@henrylearn2rock
Copy link
Author

onMessage needs to have a callback function, then messagingEnabled will be set to true.

@shirakaba
Copy link
Collaborator

shirakaba commented Jul 28, 2018

Sorry for the delay. I lost a whole day testing window.postMessage(), and found that, whether the aliasing is configured to .atDocumentStart or .atDocumentEnd, my specific use-case was broken (issue #170). The use-case in question was the posting of messages to iframes (in fact, this is probably the original purpose of the onMessage API). This prevented me from assessing the PR itself.

You know, I wonder now whether the reason that the postMessageScript is set up .atDocumentEnd is so that any window.postMessage calls sent while a webpage is still loading (e.g. by the website-maker, not the React Native developer) will not break.

Anyway, regardless of the reasoning, I feel that we should maintain consistency with React Native core (wherein the aliasing is effectively configured to .atDocumentEnd because the configuration occurs during the UIWebView's webViewDidFinishLoad hook) until such time as they change it.

If you want it to be effectively available .atDocumentStart, then add this code to the injectJavaScript prop of your <WKWebView> component – it's a hook that runs upon .atDocumentStart:

window.originalPostMessage = window.postMessage;
window.postMessage = function(data) {
    window.webkit.messageHandlers.reactNative.postMessage(data);
};

@insraq
Copy link
Contributor

insraq commented Jul 28, 2018

I think the reason why WebView injects this at the end of documents is that there're no other options. I don't see any problems moving this to the document start.

Regarding #170, I think we should de-couple the method override from onMessage callback - make it more explicit. I propose:

  1. Move the code to document start
  2. Do not override postMessage when there's an onMessage callback
  3. Add a new prop: overridePostMessage that override postMessage.

@shirakaba
Copy link
Collaborator

I think we should de-couple the method override from onMessage callback

@insraq That all sounds reasonable if we're happy to diverge a little bit from the (to be frank, problematic) implementation of the native <WebView> component – e.g. see the documentation for the onMessage prop:

 /**
 * 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`.
 *
 * `window.postMessage` accepts one argument, `data`, which will be
 * available on the event object, `event.nativeEvent.data`. `data`
 * must be a string.
 */
onMessage: PropTypes.func

If I may draw your attention to PR #176, however, which would allow us to forego the addition of an overridePostMessage prop entirely...

Through this PR, we could furthermore move the code to .atDocumentStart without worrying about breaking websites that send postMessage() actions before the <body> has loaded, nor losing consistency with <WebView>.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants