3

I have developed a website which I intend to display inside a webview, within a Chrome App. This works fine.

Now, I want to use postMessage from the website, to send messages out of the webview and into the containing Chrome App. This is done via top.postMessage inside the webview.

I've tried the following event listeners:

webView.contentWindow.addEventListener('message', messageHandler);

webView.addEventListener('message', messageHandler);

window.addEventListener('message', messageHandler);

document.addEventListener('message', messageHandler);

I have successfully implemented the following event listeners. All of which work as expected: contentload, dialog and consolemessage.

Unless I can get this to work, I am considering using consolemessage to send messages from the webview to the container - something I find unappealing, and I suspect it won't work when not using the developer mode.

Xan
  • 74,770
  • 16
  • 179
  • 206
frodeborli
  • 1,537
  • 1
  • 21
  • 30
  • Have you considered using [`"externally_connectable"`](https://developer.chrome.com/apps/messaging#external-webpage) route, if it's for specific webpage? – Xan May 21 '15 at 08:22
  • @Xan I have, but I am reluctant. This website could some times be opened in a browser window, as well as via the Chrome App simultaneously. The "app" would receive messages from a tab inside Chrome as well as from it's own webview, and I'd have to handle that - making the code more ugly again. – frodeborli May 21 '15 at 08:24

3 Answers3

6

The webview sample has a good demo of using postMessage to send messages between an app and an external page loaded in a webview.

Here are the key pieces of code.

  1. In the app, listen to the loadstop event of the webview and send an initial message to the page. You can restrict this message to specific domains or pages.

    wv1.addEventListener('loadstop', sendInitialMessage);
    
    function sendInitialMessage(e) {
     // only send the message if the page was loaded from googledrive hosting
     e.target.contentWindow.postMessage("initial message", "https://googledrive.com/host/*");
    }
    
  2. In the external page, listen for the message event and save off the source and origin.

    window.addEventListener('message', onMessage);
    
    var appWindow, appOrigin;
    
    function onMessage(e) {
     appWindow = e.source;
     appOrigin = e.origin;
    }
    

    Then the page can use those objects to post a message back to the app.

    function doSendMessage() {
     if (appWindow && appOrigin) {
      appWindow.postMessage("this is a message from the page!", appOrigin);
     } 
    }
    
  3. The app should also listen to the message event to receive the messages from the external page.

    window.addEventListener('message', function(e) {
     log("[???] messagereceived: " + e.data);
    });
    
Sarah Elan
  • 2,465
  • 1
  • 23
  • 45
  • This solution doesn't make use of the Chrome APIs created specifically for page/app communication purposes, and because of that is somewhat convoluted and requires more synchronization efforts (e.g. you can't call `doSendMessage` before `onMessage` has done its job, etc.). See my answer for comparison. – Sergey Shevchenko May 21 '15 at 19:11
  • 1
    I don't like using the Chrome APIs, if it is at all possible to use the standard APIs. The contained web app will also be hosted within other non-chrome containers, such as MSHTML or webkit - and the less chrome spesific code it uses, the better. – frodeborli May 26 '15 at 08:21
  • The `sendInitialMessage` method fails because `e.target` is undefined, any ideas on avoiding this error? Thanks for your reply. – andreszs May 18 '18 at 21:21
  • You could bypass the need for waiting for `onMessage` by referring to the embedding page as `window.parent`. See the docs for more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage – JacksonHaenchen Nov 01 '18 at 00:10
4

The reason that the embedded web page is unable to post messages to the app, is because the embedded web page does not have a reference to the app.

top.postMessage is not a reference to the app. top would work if you were trying to access the topmost frame, within the same webview.

To be able to send messages to the app, the web page needs a reference to the app. The easiest way to do this, is by having the app send the first message to the frame - a "hello"-message.

From the app:

// Initialize communications
webView.contentWindow.postMessage('hello, webpage!', 'https://your.web.page/*');
addEventListener('message', function(e) {

    // I expect this check to work, but I have not tested it.
    if (e.source != webView.contentWindow)
        return;

    // Handle e.data however you want.
});

In the web page:

var messageSource, messageOrigin;
addEventListener('message', function(e) {
    if (!messageSource) {

        /*
         * Once we have a messageSource, we should not allow anybody to change
         * messageSource again
         */

        if (e.data == "hello, webpage!") {

            /*
             * If possible, you should validate the `e.origin` value here. It could 
             * possibly come from somewhere else. However, this is quite safe as it 
             * stands, since there only is a very narrow time window where the app 
             * is open willing to accept the "hello, webpage!" message.
             *
             * Another way of validating, is by having the app wait for the 
             * "hello, host!" message. If that response is not received within a second
             * the app host could simply reload the app.
             */

            messageSource = e.source;
            messageOrigin = e.origin;
            messageSource.postMessage("hello, host!", messageOrigin);
        }
    } else {
        // Handle messages however you like. This will simply respond to every message:
        messageSource.postMessage('Your message: ' + e.data, messageOrigin);
    }
});
frodeborli
  • 1,537
  • 1
  • 21
  • 30
  • `webView.contentWindow` is undefined, it seems something has changed in the webview plugin. Any ideas how to use it now? Thanks for your reply. – andreszs May 18 '18 at 21:30
  • @andreszs Is it possible the webview has not yet loaded? See Sarah's answer for using the event to wait for the webview to finish loading before interacting with it. – JacksonHaenchen Nov 01 '18 at 00:06
3
  • In the guest page inside the contained webview, use chrome.runtime.sendMessage() to send messages to the containing app.

  • In the app, use chrome.runtime.onMessage.addListener() to listen to those messages.

Note that you can message any app this way, not only the one containing your webview, but you'll need to know the app's ID for that, and use onMessageExternal instead of onMessage. For the containing app, the ID is optional.

Here's a working example of this mechanism. It's a Polymer element, but that doesn't change the mechanism: designerProxy_ is the equivalent of your guest page; registerDesignerProxyListener_ is the equivalent of your app.

Sergey Shevchenko
  • 1,750
  • 14
  • 13
  • 1
    That would require the page to be listed in `"externally_connectable"` in the manifest, no? – Xan May 26 '15 at 08:43