First off, important warnings:
-
Warning: Chrome 92 or older doesn’t show errors occurred in the service worker – it was a bug, fixed in newer Chrome, which now shows the errors in
chrome://extensions
page. These old versions of Chrome can’t register the background script if an unhandled exception occurs during its compilation (a syntax error like an unclosed parenthesis) or initialization (e.g. accessing an undefined variable), so if you still support old Chrome you may want to wrap the code intry/catch
. -
Warning: Chrome 92 or older requires the worker file to be in the root path (bug).
-
Warning! Don’t import DOM-based libraries like jQuery or axios because service workers don’t have DOM so there’s no
document
,XMLHttpRequest
, and so on. Usefetch
directly or find/write a library that’s based onfetch
and doesn’t usewindow
ordocument
.
0. NPM packages
Use a bundler like webpack.
1. ES modules in Chrome 92 and newer
Enabled by adding "type": "module"
to the declaration of background
in manifest.json.
- Name must start with a path and end with an extension like .js or .mjs
- Static
import
statement can be used. - Dynamic
import()
is not yet implemented (crbug/1198822).
manifest.json:
"background": { "service_worker": "bg.js", "type": "module" },
"minimum_chrome_version": "92",
bg.js:
import {foo} from '/path/file.js';
import './file2.js';
As noted at the beginning of this answer, if you still target Chrome 92 or older, which don’t surface the errors during registration, each imported module should also use try/catch inside where an exception is possible.
2. importScripts
This built-in function synchronously fetches and runs the scripts so their global variables and functions become available immediately.
manifest.json:
"background": { "service_worker": "bg-loader.js" },
bg-loader.js is just a try/catch wrapper for the actual code in separate files:
try {
importScripts('/path/file.js', '/path2/file2.js' /*, and so on */);
} catch (e) {
console.error(e);
}
If some file throws an error, no subsequent files will be imported. If you want to ignore such errors and continue importing, import this file separately in its own try-catch block.
Don’t forget to specify a file extension, typically .js
or .mjs
.
2b. importScripts inside a listener
Per the specification, we must use a service worker’s install
event and import all the scripts that we want to be able to import in an asynchronous event later (technically speaking, anything outside of the initial task of the JS event loop). This handler is called only when the extension is installed or updated or an unpacked extension is reloaded (because it’s equal to an update).
It’s this convoluted in MV3 because service workers were designed for the Web, where remote scripts may be unavailable offline. Hopefully, it’ll be simplified in crbug/1198822.
See also: webpack-target-webextension plugin for WebPack.
const importedScripts = [];
function tryImport(...fileNames) {
try {
const toRun = new Set(fileNames.filter(f => !importedScripts.includes(f)));
if (toRun.size) {
importedScripts.push(...toRun);
importScripts(...toRun);
}
return true;
} catch (e) {
console.error(e);
}
}
self.oninstall = () => {
// The imported script shouldn't do anything, but only declare a global function
// (someComplexScriptAsyncHandler) or use an analog of require() to register a module
tryImport('/js/some-complex-script.js');
};
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.action === 'somethingComplex') {
if (tryImport('/js/some-complex-script.js')) {
// calling a global function from some-complex-script.js
someComplexScriptAsyncHandler(msg, sender, sendResponse);
return true;
}
}
});