0

How to access background script objects form a content script inside chrome extension?

In content script I have:

    // this will store settings
    var settings = {};

    // load settings from background
    chrome.extension.sendMessage({
        name: "get-settings"
    }, function(response) {
        debugger;
        settings = response.data.settings;
    }); 

Inside the background script I have:

    var Settings = function() {
    var me = this;
    // internal, default
    var settingList = {
        serverUrl : "http://automatyka-pl.p4",
        isRecordingEnabled : true,
        isScanEnabled : true
    };

    this.get = function( key ) {
        return settingList[key];
    };
    this.set = function( key , value ) {
        if (settingList[key] != value) {
            var setting = {};
            setting[key] = value;

            chrome.storage.sync.set(setting, function() {
                settingList[key] = value;
            });
        }
        return true;
    };

chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
    if (request.name == 'get-settings') {
        sendResponse({
            data : {
                settings : settings
            }
        });
        return true;
    }
});

var settings = new Settings();

Messaging works, i mean response is send but returned object is empty. Do you know how to solve that?

EDIT Based on your comments and answer will try to add different light to my question.

The actual problem is: How to access background "model" from content script.

Lets assume that content script continuously responds to page DOM changes. Any time changes are detected some processing is made inside content script. But this processing is depended on extension setting. Those setting can be set via page action popup script which informs background model what those settings are.

So, any time page is processed with content script it should be aware of current settings stored inside background script.

As already described pulling settings from background is an asynchronous process, so i need a callback for further processing inside content script. Further processing must wait for settings (so this should be handled synchronously?).

It's hard for my to imagine what program flow should look like in this case.

  1. background loads (setting initialized)
  2. page loads -> content script loads
  3. content script requests settings -> further processing is done inside callback function.
  4. user changes setting, background settings are changed
  5. page change is triggered and content script responds
  6. content script requests settings -> further processing is done inside callback function - but it cannot be the same function like in pt. 3 (content "model" does not have to be initialized)?
Xan
  • 74,770
  • 16
  • 179
  • 206
IT Man
  • 933
  • 3
  • 12
  • 33
  • All chrome.* API with function callbacks is asynchronous. See [How do I return the response from an asynchronous call?](https://stackoverflow.com/q/14220321) – wOxxOm Sep 22 '16 at 17:45
  • got it! it seems i have to reorganize the code. – IT Man Sep 22 '16 at 17:59
  • Possible duplicate of [How do I return the response from an asynchronous call?](http://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – Haibara Ai Sep 23 '16 at 06:12
  • re: #6 just use another callback if needed, yeah. – wOxxOm Sep 23 '16 at 18:14
  • Example added to answer below. You must use another callback to receive changed settings in your content-script. Because of your settings in content-script saved in global property - it will be visible in all contexts in content-script (even in your first callback) where it is not redefined by local settings. There is no universal way to direct access another context's models because of security reasons. – MobDev Sep 25 '16 at 18:03
  • Guys! Thanks for your involvement! Your explanations helped solved this problem. – IT Man Sep 25 '16 at 20:37

2 Answers2

3
  1. sendMessage doesn't transfer the object itself, but only its JSON-ifiable representation, effectively objReceived = JSON.parse(JSON.stringify(objSent)), so since your object's settingList is invisible outside function context it's lost during serialization.

    You can make it public by exposing a stringifiable property

    this.settingList = { foo: 'bar' };
    

    that would be transferred to your content script successfully.

  2. Since messaging is asynchronous, to use the response in the content script you should do it inside the response callback:

    // this will store the settings
    var settings = {};
    
    // load settings from background
    chrome.runtime.sendMessage({
        name: "get-settings"
    }, function(response) {
        settings = response.data.settings;
        onSettingsReady();
    }); 
    
    function onSettingsReady() {
       // put your logic here, settings are set at this point
    }
    
  3. To know if settings changed outside your content-script, in settings setter in background.js send messages to your tab's content-script:

    this.set = function( key , value ) {
        ...
        // notify active tab if settings changed
        chrome.tabs.query({"windowType":"normal"}, function(tabs){
        for( id in tabs ){
            if("id" in tabs[id]){
                chrome.tabs.sendMessage(tabs[id].id,{"action":"update-settings","settings":settings});
            }
        }
    });
        return true;
    };
    
  4. And in content-script listen and process this message:

    chrome.runtime.onMessage.addListener(function(msg){
        if("action" in msg && msg.action == 'update-settings'){
            // You are setting global settings variable, so on it will be visible in another functions too
            settings = msg.settings;
        }
    });
    

More details: https://developer.chrome.com/extensions/runtime#method-sendMessage.

P.S. Use chrome.runtime.sendMessage instead of chrome.extension.sendMessage as the latter is deprecated in Chrome API and totally unsupported in WebExtensions API (Firefox/Edge).

MobDev
  • 1,614
  • 17
  • 17
  • Well, i've made `settingList` public, and indeed it is props can be serialized and retrived back in content script. But anyway because of asynchronous nature of `sendMessage` they settings object inside content script is not initialized before it is needed. – IT Man Sep 23 '16 at 07:36
  • Use callback from your message receiver, like `settings=response.data.settings; ready();` And run other code inside it, like you do inside `document.ready` callback. This is problem not listed in your question example. – MobDev Sep 23 '16 at 09:46
  • And about your second question - just use another callback to receive settings and set global variable settings on it, it would be accessible inside your first callback. – MobDev Sep 25 '16 at 15:38
  • Or attach full usecase of your content-script, will try to help you. – MobDev Sep 25 '16 at 15:40
  • I appreciate your help very much! I've came to quite same conclusions on friday and reorganized code similar to your answer. The difference is that i'm pulling settings from background when content script fires, and have another listener on DOM initialization. In this listner i'm checking both conditions (DOM init, settings init) inside setInterval loop. When those conditions are met, I'm doing further processing. – IT Man Sep 25 '16 at 20:33
  • Glad to help you! :) setInterval is a variant too, but is gives a bit more CPU usage – MobDev Sep 25 '16 at 20:47
0

It would probably make more sense to have another instance of Settings in your content script.

After all, chrome.storage API is available in content scripts.

Of course, you need to watch for changes made in other parts of the extension - but you should be doing so anyway, since you're using chrome.storage.sync and its value can change independently by Chrome Sync.

So, proposed solution:

  1. Add a listener to chrome.storage.onChanged and process changes to update your settingList as needed.
  2. Move the Storage logic to a separate JS "library", e.g. storage.js
  3. Load storage.js in your content script and use it normally.

You may also want to adjust your storage logic so that saved data is actually taken into account - right now it's always the default. You can do something like this:

var defaultSettingList = {
    serverUrl : "http://automatyka-pl.p4",
    isRecordingEnabled : true,
    isScanEnabled : true
};
var settingList = Object.assign({}, defaultSettingList);
chrome.storage.sync.get(defaultSettingList, function(data) {
  settingList = Object.assign(settingList, data);
  // At this point you probably should call the "ready" callback - initial
  //   load has to be async, no way around it
});
Xan
  • 74,770
  • 16
  • 179
  • 206