Chrome Extension: Message Passing (Sending the DOM to popup.js) returns 'null'

I would like to use a Chrome Extension to download the current page's DOM. I'm not sure why, but when my download occurs, the result is just a text file with either 'null' or 'undefined', rather than the DOM. I've tried to assimilate the knowledge from here and here, but I can't seem to get the message from content.js through to popup.js.

Additionally, I'm not sure why this actually works. When I read the docs, it seems like I need to send the message from popup.js to content.js by selecting the active tab:

chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
    chrome.tabs.sendMessage(tabs[0].id, {message: 'getMessageFromContent'}, function(response) {
        //Code to handle response from content.js
    }
});

My current code:

content.js

var page_html = DOMtoString(document);
chrome.runtime.sendMessage({method: 'downloadPageDOM', pageDOM: thisPage});

function DOMtoString(document_root) { ... }

background.js

chrome.tabs.query({currentWindow: true, active: true}, function(tab) {
    var page_html;
    chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
        if (request.message == 'downloadPageDOM')
            page_html = request.pageDOM;
        else if (request.message == 'getPageDOM')
            sendResponse(page_html);
    });
}); 

popup.js

document.addEventListener('DOMContentLoaded', function() {
    var download_button = document.getElementById('download_button');
    download_button.addEventListener('click', function() {
        chrome.runtime.sendMessage({message:'getPageDOM'}, function(response) {
            download(response, "download.html", "text/html");
        });
    });
});

function download(data, fileName, mimeType) { ... }

I feel like I'm missing a crucial understanding of how message passing works. If anyone could take a second to help me understand why the file that downloads just has 'null', I would sincerely appreciate it.

Answers:

Answer

You're over-complicating this, which leads to a lot of logical errors.

You've set up the background page to act like a message proxy, and the content script itself triggers updating your page_html variable. Then the popup pulls that data with another message.

Note that page_html will not contain the current tab's data in any case: you're overwriting this data with the last loaded tab.


What you can do is completely cut out the middleman (i.e. background.js). I guess you got confused by the fact that sending a message TO a popup is a generally a bad idea (no guarantee it's open), but the other way around is usually safe (and you can make it always safe).

Solution 1 (bad, but here for educational purposes)

The logic of your app is: once the user clicks the button, make the snapshot at that moment. So, instead of making your content script do its work immediately, add a message listener:

// content.js
chrome.runtime.onMessage(function(message, sender, sendResponse) {
  else if (request.message == 'getPageDOM')
    sendResponse(DOMtoString(document));
});

function DOMtoString(document_root) { ... }

And in your popup, request it:

// popup.js
// (Inside the click listener)
chrome.tabs.query({currentWindow: true, active: true}, function(tabs) {
  // Note that sending a message to a content script is different
  chrome.tabs.sendMessage(tabs[0].id, {message:'getPageDOM'}, function(response) {
    download(response, "download.html", "text/html");
  });
});

However, this solution is not 100% robust. It will fail if the content script is not injected into the page (and this can happen). But it's possible to fix this.

Solution 2

Let's not assume the content script is injected. In fact, most of the time you don't NEED to inject it automatically, only when the user clicks your button.

So, remove the content script from the manifest, make sure you have host permissions ("<all_urls>" works well, though consider activeTab permission), and the use programmatic injection.

There is a little-used form of programmatic injection that collects the value of the last executed statement. We're going to use that.

// content.js
DOMtoString(document); // This will be the last executed statement

function DOMtoString(document_root) { ... }

In the popup, execute script, collect results:

// popup.js
// (Inside the click listener)
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
  chrome.tabs.executeScript(tabs[0].id, {file: "content.js"}, function(data) {
    // Data is an array of values, in case it was executed in multiple tabs/frames
    download(data[0], "download.html", "text/html");
  });
});

NOTE: All of the above assumes that your function DOMtoString actually works.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.