Access global js variables from js injected by a c

2019-01-12 08:10发布

问题:

I am trying to create an extension that will have a side panel. This side panel will have buttons that will perform actions based on the host page state.

I followed this example to inject the side panel and I am able to wire up a button onClick listener. However, I am unable to access the global js variable. In developer console, in the scope of the host page I am able to see the variable (name of variable - config) that I am after. but when I which to the context of the sidepanel (popup.html) I get the following error -
VM523:1 Uncaught ReferenceError: config is not defined. It seems like popup.html also runs in a separate thread.

How can I access the global js variable for the onClick handler of my button?

My code:

manifest.json

{
    "manifest_version": 2,

    "name": "Hello World",
    "description": "This extension to test html injection",
    "version": "1.0",
    "content_scripts": [{
        "run_at": "document_end",
        "matches": [
            "https://*/*",
            "http://*/*"
        ],
        "js": ["content-script.js"]
    }],
    "browser_action": {
        "default_icon": "icon.png"
    },
    "background": {
        "scripts":["background.js"]
    },
    "permissions": [
        "activeTab"
    ],
    "web_accessible_resources": [
        "popup.html",
        "popup.js"
    ]
}

background.js

chrome.browserAction.onClicked.addListener(function(){
    chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
        chrome.tabs.sendMessage(tabs[0].id,"toggle");
    })
});

content-script.js

chrome.runtime.onMessage.addListener(function(msg, sender){
    if(msg == "toggle"){
        toggle();
    }
})

var iframe = document.createElement('iframe'); 
iframe.style.background = "green";
iframe.style.height = "100%";
iframe.style.width = "0px";
iframe.style.position = "fixed";
iframe.style.top = "0px";
iframe.style.right = "0px";
iframe.style.zIndex = "9000000000000000000";
iframe.frameBorder = "none"; 
iframe.src = chrome.extension.getURL("popup.html")

document.body.appendChild(iframe);

function toggle(){
    if(iframe.style.width == "0px"){
        iframe.style.width="400px";
    }
    else{
        iframe.style.width="0px";
    }
}

popup.html

<head>
<script src="popup.js"> </script>
</head>
<body>
<h1>Hello World</h1>
<button name="toggle" id="toggle" >on</button>
</body>

popup.js

document.addEventListener('DOMContentLoaded', function() {
  document.getElementById("toggle").addEventListener("click", handler);
});

function handler() {
  console.log("Hello");
  console.log(config);
}

回答1:

Since the page JS variables cannot be directly accessed from an extension, you need to insert code in script element so that it runs in the page context and relays the variable into the content script via DOM messaging. Then the content script can relay the message to the iframe.

  • content script:

    runInPage(initConfigExtractor);
    
    chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
      switch (msg) {
        case 'toggle':
          toggle();
          return;
        case 'getConfig':
          window.addEventListener(chrome.runtime.id + '-config', event => {
            sendResponse(event.detail);
          }, {once: true});
          window.dispatchEvent(new Event(chrome.runtime.id));
          return true;
      }
    })
    
    function initConfigExtractor(extensionId) {
      window.addEventListener(extensionId, () => {
        window.dispatchEvent(new CustomEvent(extensionId + '-config', {
          detail: window.config,
        }));
      });
    }
    
    function runInPage(fn) {
      const script = document.createElement('script');
      document.head.appendChild(script).text =
        '((...args) => (' + fn + ')(...args))(' + JSON.stringify(chrome.runtime.id) + ')';
      script.remove();
    }
    
  • iframe script:

    function handler() {
      chrome.tabs.getCurrent(tab => {
        chrome.tabs.sendMessage(tab.id, 'getConfig', config => {
          console.log(config);
          // do something with config
        });
      });  
    }
    
  • alternative case - popup script:

    function handler() {
      chrome.tabs.query({active: true, currentWindow: true}, tabs => {
        chrome.tabs.sendMessage(tabs[0].id, 'getConfig', config => {
          console.log(config);
          // do something with config
        });
      });  
    }
    

So, basically:

  1. the iframe script gets its own tab id and sends a message to the content script
  2. the content script sends a DOM message to a previously inserted page script
  3. the page script listens to that DOM message and sends another DOM message back to the content script
  4. the content script sends it in a response back to the in iframe script.

The whole thing is asynchronous so return true was needed in the onMessage listener to keep the message channel open.