'Chrome extension: Checking if content script has been injected or not
I'm developing a Chrome extension. Instead of using manifest.json to match content script for all URLs, I lazily inject the content script by calling chrome.tabs.executeScript when user do click the extension icon.
What I'm trying is to avoid executing the script more than once. So I have following code in my content script:
if (!window.ALREADY_INJECTED_FLAG) {
window.ALREADY_INJECTED_FLAG = true
init() // <- All side effects go here
}
Question #1, is this safe enough to naively call chrome.tabs.executeScript every time the extension icon got clicked? In other words, is this idempotent?
Question #2, is there a similar method for chrome.tabs.insertCSS?
It seems impossible to check the content script inject status in the backgroud script since it can not access the DOM of web page. I've tried a ping/pong method for checking any content script instance is alive. But this introduces an overhead and complexity of designing the ping-timeout.
Question #3, any better method for background script to check the inject status of content script, so I can just prevent calling chrome.tabs.executeScript every time when user clicked the icon?
Thanks in advance!
Solution 1:[1]
Rob W's option 3 worked great for me. Basically the background script pings the content script and if there's no response it will add all the necessary files. I only do this when a tab is activated to avoid complications of having to add to every single open tab in the background:
background.js
chrome.tabs.onActivated.addListener(function(activeInfo){
tabId = activeInfo.tabId
chrome.tabs.sendMessage(tabId, {text: "are_you_there_content_script?"}, function(msg) {
msg = msg || {};
if (msg.status != 'yes') {
chrome.tabs.insertCSS(tabId, {file: "css/mystyle.css"});
chrome.tabs.executeScript(tabId, {file: "js/content.js"});
}
});
});
content.js
chrome.runtime.onMessage.addListener(function (msg, sender, sendResponse) {
if (msg.text === 'are_you_there_content_script?') {
sendResponse({status: "yes"});
}
});
Solution 2:[2]
Just a side note to the great answer from Rob.
I've found the Chrome extension from Pocket is using a similar method. In their dynamic injected script:
if (window.thePKT_BM)
window.thePKT_BM.save();
else {
var PKT_BM_OVERLAY = function(a) {
// ... tons of code
},
$(document).ready(function() {
if (!window.thePKT_BM) {
var a = new PKT_BM;
window.thePKT_BM = a,
a.init()
}
window.thePKT_BM.save()
}
)
}
Solution 3:[3]
For MV3 Chrome extension, I use this code, no chrome.runtime.lastError "leaking" as well:
In Background/Extension page (Popup for example)
private async injectIfNotAsync(tabId: number) {
let injected = false;
try {
injected = await new Promise((r, rej) => {
chrome.tabs.sendMessage(tabId, { op: "confirm" }, (res: boolean) => {
const err = chrome.runtime.lastError;
if (err) {
rej(err);
}
r(res);
});
});
} catch {
injected = false;
}
if (injected) { return tabId; }
await chrome.scripting.executeScript({
target: {
tabId
},
files: ["/js/InjectScript.js"]
});
return tabId;
}
NOTE that currently in Chrome/Edge 96, chrome.tabs.sendMessage does NOT return a Promise that waits for sendResponse although the documentation says so.
In content script:
const extId = chrome.runtime.id;
class InjectionScript{
init() {
chrome.runtime.onMessage.addListener((...params) => this.onMessage(...params));
}
onMessage(msg: any, sender: ChrSender, sendRes: SendRes) {
if (sender.id != extId || !msg?.op) { return; }
switch (msg.op) {
case "confirm":
console.debug("Already injected");
return void sendRes(true);
// Other ops
default:
console.error("Unknown OP: " + msg.op);
}
}
}
new InjectionScript().init();
What it does:
When user opens the extension popup for example, attempt to ask the current tab to "confirm".
If the script isn't injected yet, no response would be found and
chrome.runtime.lastErrorwould have value, rejecting the promise.If the script was already injected, a
trueresponse would result in the background script not performing it again.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | GivP |
| Solution 2 | KF Lin |
| Solution 3 | Luke Vo |
