How to access the webpage DOM/HTML from an extension popup or background script?

I'm writing a Chrome extension and trying to overlay a <div> over the current webpage as soon as a button is clicked in the popup.html file.

When I access the document.body.insertBefore method from within popup.html it overlays the <div> on the popup, rather than the current webpage.

Do I have to use messaging between background.html and popup.html in order to access the web page's DOM? I would like to do everything in popup.html, and to use jQuery too, if possible.

69470 次浏览

ManifestV3 service worker doesn't have any DOM/document/window.

ManifestV3/V2 extension pages (and the scripts inside) have their own DOM, document, window, and a chrome-extension:// URL (use devtools for that part of the extension to inspect it).

You need a content script to access DOM of web pages and interact with a tab's contents. Content scripts will execute in the tab as a part of that page, not as a part of the extension, so don't load your content script(s) in the extension page, use the following methods:

Method 1. Declarative

manifest.json:

"content_scripts": [{
"matches": ["*://*.example.com/*"],
"js": ["contentScript.js"]
}],

It will run once when the page loads. After that happens, use messaging but note, it can't send DOM elements, Map, Set, ArrayBuffer, classes, functions, and so on - it can only send JSON-compatible simple objects and types so you'll need to manually extract the required data and pass it as a simple array or object.

Method 2. Programmatic

  • ManifestV2:

    Use chrome.tabs.executeScript to inject a content script on demand.

    The callback of this method receives results of the last expression in the content script so it can be used to extract data which must be JSON-compatible, see method 1 note above.

    Required permissions in manifest.json:

    • Best case: "activeTab", suitable for a response to a user action (usually a click on the extension icon in the toolbar). Doesn't show a permission warning when installing the extension.

    • Usual: "*://*.example.com/" plus any other sites you want.

    • Worst case: "<all_urls>" or "*://*/", "http://*/", "https://*/" - when submitting into Chrome Web Store all of these put your extension in a super slow review queue because of broad host permissions.

  • ManifestV3 differences to the above:

    Use chrome.scripting.executeScript.

    Required permissions in manifest.json:

    • "scripting" - mandatory
    • "activeTab" - ideal scenario, see notes for ManifestV2 above.

    If ideal scenario is impossible add the allowed sites to host_permissions in manifest.json.

To illustrate programmatic injection let's add that div when a browser action is clicked.

ManifestV3

Don't forget to add the permissions in manifest.json, see the other answer for more info.

  • Simple call:

    async function tabAddDiv() {
    const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
    await chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: inContent1,
    });
    }
    
    
    function inContent1() {
    const el = document.createElement('div');
    el.style.cssText = 'position:fixed; top:0; left:0; right:0; background:red';
    el.textContent = 'DIV';
    document.body.appendChild(el);
    }
    

    Note: in Chrome 91 or older func: should be function:.

  • Calling with parameters and receiving a result

    Requires Chrome 92 as it implemented args.

    Example 1:

    res = await chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: (a, b) => { return [window[a], window[b]]; },
    args: ['foo', 'bar'],
    });
    

    Example 2:

    async function tabAddDiv() {
    const [tab] = await chrome.tabs.query({active: true, currentWindow: true});
    let res;
    try {
    res = await chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: inContent2,
    args: [{ foo: 'bar' }], // arguments must be JSON-serializable
    });
    } catch (e) {
    console.warn(e.message || e);
    return;
    }
    // res[0] contains results for the main page of the tab
    document.body.textContent = JSON.stringify(res[0].result);
    }
    
    
    function inContent2(params) {
    const el = document.createElement('div');
    el.style.cssText = 'position:fixed; top:0; left:0; right:0; background:red';
    el.textContent = params.foo;
    document.body.appendChild(el);
    return {
    success: true,
    html: document.body.innerHTML,
    };
    }
    

ManifestV2

  • Simple call:

    // uses inContent1 from ManifestV3 example above
    chrome.tabs.executeScript({ code: `(${ inContent1 })()` });
    
  • Calling with parameters and receiving a result:

    // uses inContent2 from ManifestV3 example above
    chrome.tabs.executeScript({
    code: `(${ inContent2 })(${ JSON.stringify({ foo: 'bar' }) })`
    }, ([result] = []) => {
    if (!chrome.runtime.lastError) {
    console.log(result); // shown in devtools of the popup window
    }
    });
    

    This example uses automatic conversion of inContent function's code to string, the benefit here is that IDE can apply syntax highlight and linting. The obvious drawback is that the browser wastes time to parse the code, but usually it's less than 1 millisecond thus negligible.