How to make your chrome extension access webpage?

There's some situation that your chrome extension may need access to webpage. However, chrome doesn't allow extension directly access to webpage. Here I provide two workaround to grant you access to webpage.

Knowing what do you need.

If you only need access DOM element like width and height, "content script" should be enough for you. If you want access to global variables or functions, additional work will be required.

Access DOM element

Chrome extension cannot talk to webpage directly, it must go through background script. Then there are two common way to communicate between extension scripts, postMessage(long live connection) and sendMessage(one time request). Depend on your case, either one should work. I will use both of them here just to show you how they work.

In this example, I would like to retrieve offset dimension which is in DOM element. The graph below shows how I will do it.

So we first define our scripts in manifest.json
manifest.json

{
   "content_scripts": [
      {
         "matches": ["<all_url>"],
         "js": ["contentScript.js"]
      }
   ]
   "background": {
      "scripts": ["background.js"]
   }
}

background.js

//background script is always running unless extension
//is disabled

//Wait for some one connect to it
let contentPort
chrome.runtime.onConnect.addListener(function(portFrom) {
   if(portFrom.name === 'background-content') {
      //This is how you add listener to a port.
      portFrom.onMessage.addListener(function(message) {
         //Do something to this message(offsetheight and width)
      });
   }
});

//Send a message to a tab which has your content script injected. 
//You should able to use postMessage here as well.
chrome.tabs.sendMessage(YOUR_TARGET_TAB_ID, {action: 'GET_DIMENSION'});

contentScript.js

//start connection in content script
let contentPort = chrome.runtime.connect({
   name: 'background-content'
});

//Listen for runtime message
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
   //Retrieve offset dimension
   if(message.action === 'GET_DIMENSION') {
      contentPort.postMessage({
         type: 'DIMENSION', 
         payload: {
            height: document.body.offsetHeight,
            width: document.body.offsetWidth       
         }
      });
   }
});

Notice, content script live in a "isolated world" which only share DOM element with webpage. Not only content script is in isolated world, executeScript will also execute script in isolated world.

Full Access to webpage

So what if we want more than just DOM element? What if I want to access global variable? This one will need extra work. In this case, for example, if we have a global variable duck. If I want to retrieve this duck, this is what you can do.

Let setup environment first. We need to create a script element in "real" webpage and inject our pageScript.js. pageScript.js will then add a listener to window object. The graph below shows how I set up.

When I post message from background script to content script, content script will fire an event in webpage(content script share DOM element with webpage) and retrieve duck variable duck. After you get the duck, you can either postMessage(cross origin) or dispatchEvent(same origin), depend on your case to send back duck all the way to background script.

Manifest.json is almost the same but adding a web_accessible_resources
manifest.json

{
   "content_scripts": [
      {
         "matches": ["<all_url>"],
         "js": ["contentScript.js"]
      }
   ],
   "background": {
      "scripts": ["background.js"]
   },
   "web_accessible_resources": ["pagescript.js"]
}

Background script is the same but we have new action here because we want more.
background.js

//background script is always running unless extension
//is disabled

//Wait for some one connect to it
let contentPort
chrome.runtime.onConnect.addListener(function(portFrom) {
   if(portFrom.name === 'background-content') {
      //This is how you add listener to a port.
      portFrom.onMessage.addListener(function(message) {
         //Do something to duck
      });
   }
});

//Send a message to a tab which has your content script injected
chrome.tabs.sendMessage(YOUR_TARGET_TAB_ID, {action: 'GET_DUCK'});

More things to go to content script
contentScript.js

//start connection in content script
let contentPort = chrome.runtime.connect({
   name: 'background-content'
});

//Append your pageScript.js to "real" webpage. So will it can full access to webpate.
var s = document.createElement('script');
s.src = chrome.extension.getURL('pageScript.js');
(document.head || document.documentElement).appendChild(s);
//Our pageScript.js only add listener to window object, 
//so we don't need it after it finish its job. But depend your case, 
//you may want to keep it.
s.parentNode.removeChild(s);

//Listen for runtime message
chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
   if(message.action === 'GET_DUCK') {
      //fire an event to get duck
      let event = new CustomEvent('GET_DUCK');
      window.dispatchEvent(event);
   }
});

window.addEventListener('message', function receiveDuck(event) {
   if(event.data.action === 'GOT_DUCK') {
      //Remove this listener, but you can keep it depend on your case
      window.removeEventListener('message', receiveDuck, false);
      contentPort.postMessage({type: 'GOT_DUCK', payload: event.data.payload});
   }
}, false);

pageScript.js

window.addEventListener('GET_DUCK', function getDuckInPage(event) {
   //You can also use dispatchEvent
   window.postMessage({action: 'GOT_DUCK', payload: duck}, '*');
}, false);

Also, you can also sendMessage directly from webpage to extension if you have extension ID and specific domain for page(<all_url> is not allow for this method).

Finally

Chrome put so many limitation on it for some reasons. Especially for security, so make sure you have well secure your code from some attack like XSS attack.