admin管理员组文章数量:1134247
In Chrome 61, support for modules in JavaScript was added. Right now I am running Chrome 63.
I am trying to use import
/export
syntax in Chrome extension content script to use modules.
In manifest.json
:
"content_scripts": [
{
"js": [
"content.js"
],
}
]
In my-script.js
(same directory as content.js
):
'use strict';
const injectFunction = () => window.alert('hello world');
export default injectFunction;
In content.js
:
'use strict';
import injectFunction from './my-script.js';
injectFunction();
I receive this error: Uncaught SyntaxError: Unexpected identifier
If I change the import syntax to import {injectFunction} from './my-script.js';
I get this error: Uncaught SyntaxError: Unexpected token {
Is there some issue with using this syntax in content.js
in Chrome extension (since in HTML you have to use <script type="module" src="script.js">
syntax), or am I doing something wrong? It seems strange that Google would ignore support for extensions.
In Chrome 61, support for modules in JavaScript was added. Right now I am running Chrome 63.
I am trying to use import
/export
syntax in Chrome extension content script to use modules.
In manifest.json
:
"content_scripts": [
{
"js": [
"content.js"
],
}
]
In my-script.js
(same directory as content.js
):
'use strict';
const injectFunction = () => window.alert('hello world');
export default injectFunction;
In content.js
:
'use strict';
import injectFunction from './my-script.js';
injectFunction();
I receive this error: Uncaught SyntaxError: Unexpected identifier
If I change the import syntax to import {injectFunction} from './my-script.js';
I get this error: Uncaught SyntaxError: Unexpected token {
Is there some issue with using this syntax in content.js
in Chrome extension (since in HTML you have to use <script type="module" src="script.js">
syntax), or am I doing something wrong? It seems strange that Google would ignore support for extensions.
14 Answers
Reset to default 118Asynchronous dynamic import()
function for ES modules
Unlike the unsafe workaround using a <script>
element, this one runs in the same safe JS environment (assuming you use the default world i.e. not MAIN
), where your imported module can still access the global variables and functions of the initial content script, including the built-in chrome
API like chrome.runtime.sendMessage
.
☢ Beware it can be blocked by the website's service worker: https://crbug.com/352364581
In content_script.js
, it looks like
(async () => {
const src = chrome.runtime.getURL("your/content_main.js");
const contentMain = await import(src);
contentMain.main();
})();
You'll also need to declare the imported scripts in manifest's Web Accessible Resources:
// ManifestV3
"web_accessible_resources": [{
"matches": ["<all_urls>"],
"resources": ["your/content_main.js"]
}],
// ManifestV2
"web_accessible_resources": [
"your/content_main.js"
]
For more details:
- How to use ES6 “import” with Chrome Extension
- Working Example of ES6 import in Chrome Extension
chrome.runtime.getURL
Synchronous "import" workaround for normal non-module scripts
Use a normal non-module script, add its name to "js"
in "content_scripts" before your main content script, then simpy use the global variables/functions directly (example).
Disclaimer
First of all, it’s important to say that content scripts don’t support modules as of January 2018. This workaround sidesteps the limitation by embedding module script
tag into the page that leads back to your extension.
THIS IS AN UNSAFE WORKAROUND!
A web page script (or another extension) can exploit your code and extract/spoof the data by using setters/getters on Object.prototype
and other prototypes, proxying functions and/or global objects, because the code inside a script
element runs in the JS context of the page, not in the safe isolated JS environment where content scripts run by default.
A safe workaround is the dynamic import()
shown in another answer here.
Workaround
This is my manifest.json
:
"content_scripts": [ {
"js": [
"content.js"
]
}],
"web_accessible_resources": [
"main.js",
"my-script.js"
]
Note that I have two scripts in web_accessible_resources
.
This is my content.js
:
'use strict';
const script = document.createElement('script');
script.setAttribute("type", "module");
script.setAttribute("src", chrome.extension.getURL('main.js'));
const head = document.head || document.getElementsByTagName("head")[0] || document.documentElement;
head.insertBefore(script, head.lastChild);
This will insert main.js
into the webpage as a module script.
All my business logic is now in main.js
.
For this method to work, main.js
(as well as all scripts that I will import
) must be in web_accessible_resources
in the manifest.
Example Usage: my-script.js
'use strict';
const injectFunction = () => window.alert('hello world');
export {injectFunction};
And in main.js
this is an example of importing the script:
'use strict';
import {injectFunction} from './my-script.js';
injectFunction();
This works! No errors are thrown, and I am happy. :)
Static import
statement is not available in content scripts.
A workaround using global scope
Content scripts live in their own 'isolated world' sharing the same isolated global namespace that is only accessible to content scripts of your extension (either declared in manifest.json or injected programmatically) and not to the page scripts.
Warning: if your content script was declared/injected in the
MAIN
world in a ManifestV3 extension, this approach can't be used as your globals will be exposed to the page. Instead, combine all dependent scripts in one bundle with an IIFE.
Here's the implementation:
manifest.json
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": [
"content-scripts/globals.js",
"content-scripts/script1.js",
"content-scripts/script2.js"
]
}
],
globals.js
globalThis.foo = 123;
script1.js
some_fn_that_needs_foo(globalThis.foo);
Same way you can factor out re-usable functions and other actors you would otherwise import
in content script files.
N.B.: global namespace of content scripts is not available to any pages besides content scripts - so there is little to no global scope pollution.
In case you need to import some libs - you will have to use a bundler like Parcel
to package up your content script files along with the needed libs into one huge-content-script.js
and then metion it in manifest.json
.
P.S.: docs on globalThis
The best way would be to use bundlers like webpack or Rollup.
I got away with basic configuration
const path = require('path');
module.exports = {
entry: {
background: './background.js',
content: './content.js',
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, '../build')
}
};
Run the file with the command
webpack --config ./ext/webpack-ext.config.js
Bundlers combine the related files and we can use modularisation in chrome extensions! :D
You will need to keep all other files like manifest and static files in build folder.
Play around with it and you will eventually find a way to make it work!
I just stumbled across this question while trying to solve the same thing myself.
Anyways, I think there's a simpler solution to injecting your own custom modules into your content script. I was looking at how Jquery is injected and it occurs to me you can do the same thing by creating an IIFE (Immediately Invoked Function Expression), and declaring it in your manifest.json
It goes something like this:
In your manifest.json:
"content_scripts": [
{
"matches": ["https://*"],
"css": ["css/popup.css"],
"js": ["helpers/helpers.js"]
}],
Then just create an IIFE in your helpers/helpers.js:
var Helpers = (function() {
var getRandomArbitrary = function(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
return {
getRandomArbitrary: getRandomArbitrary
}
})()
Now, you can freely use your helper functions in your content script:
Helpers.getRandomArbitrary(0, 10) // voila!
I think it's great if you use this method to refactor some of your generic functions. Hope this helps!
For Vite Users: CRXJS
There is this awesome plugin called crxjs, you just need to update it in vite.config.ts
and give the path to your manifest.json
(it works only with manifets_version >= 3
).
Follow the below steps to get your script running:
- Add crxjs to your project:
npm install --save-dev @crxjs/vite-plugin
- Create or update
manifest.json
:{ "manifest_version": 3, "name": "CRXJS React Vite Example", "version": "1.0.0", "action": { "default_popup": "index.html" } }
- Update your
vite.config.ts
file with path to the manifest:import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { crx } from '@crxjs/vite-plugin' import manifest from './manifest.json' export default defineConfig({ plugins: [ react(), crx({ manifest }), ], })
After this setup, run your project. Now config.js
will be bundled, and you can import packages in it.
Using Rollup bundler
full tutorial: https://www.extend-chrome.dev/rollup-plugin#usage
TL;DR
npm i -D rollup\
rollup-plugin-chrome-extension@latest\
@rollup/plugin-node-resolve\
@rollup/plugin-commonjs
rollup.config.js
:
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { chromeExtension, simpleReloader } from 'rollup-plugin-chrome-extension'
export default {
input: 'src/manifest.json',
output: {
dir: 'dist',
format: 'esm',
},
plugins: [
// always put chromeExtension() before other plugins
chromeExtension(),
simpleReloader(),
// the plugins below are optional
resolve(),
commonjs(),
],
}
package.json
:
{
"scripts": {
"build": "rollup -c",
"start": "rollup -c -w"
}
}
Using esbuild
Further to Dhruvil's answer, here's a GitHub repo showing how to use esbuild to bundle content scripts written in TypeScript and React - therefore enabling you to import es6 modules.
It also includes bundling the background service worker and popup, with scripts that enable Hot Module Reloading when running the popup locally.
Short Answer:
You can mimic some of the functionality and get some of the benefits of import
/export
in browser extensions by creating the following file and listing it early in your manifest.json
:
let exportVars, importVarsFrom;
{
const modules = {};
exportVars = varsObj => ({
from(nameSpace) {
modules[nameSpace] || (modules[nameSpace] = {});
for (let [k,v] of Object.entries(varsObj)) {
modules[nameSpace][k] = v;
}
}
});
importVarsFrom = nameSpace => modules[nameSpace];
}
Then, export from one file/module like this:
exportVars({ var1, var2, var3 }).from('my-utility');
Import into another file/module like this:
const { var1, var3: newNameForVar3 } = importVarsFrom('my-utility');
Discussion:
This strategy:
- allows modular code in a browser extension such that you can split code into multiple files but don't have variable clashes due to shared global scope between different files,
- still allows you to export and import variables out of and into different JavaScript files/modules,
- introduces only two global variables, namely the exporting function and the importing function,
- maintains full browser extension functionality in each file (e.g.
chrome.runtime
, etc.) that is eliminated by, e.g., the approach in another answer (currently the accepted answer) using module script tag embedding, - uses a concise syntax similar to the true
import
andexport
functions in JavaScript, - allows name-spacing which could be the file names of the exporting modules in a manner similar to how the true
import
andexport
commands work in JavaScript, but doesn't have to be (i.e. the name-space names could be anything you want), and - allows variable renaming upon import similar to how
import { fn as myFn }...
works.
To do this, your manifest.json
needs to load your JavaScript as follows:
- the file establishing the exporting/importing functions first (named
modules-start.js
in the example below), - the exporting files next, and
- the importing files last.
Of course, you might have a file that both imports and exports. In that case, just ensure it is listed after the files it imports from but before the files it exports to.
Working Example
The following code demonstrates this strategy.
It is important to note that all of the code in each module/file is contained within curly braces. The only exception is the first line in modules-start.js
which establishes the exporting and importing functions as global variables.
The code in the snippet below is necessarily contained in a single "place". In a real project, however, the code could be split into separate files. Note, though, that even in this artificial context here (i.e. within the single code snippet below), this strategy allows the different sections of code it contains to be modular and yet still interconnected.
// modules-start.js:
let exportVars, importVarsFrom; // the only line NOT within curly braces
{
const modules = {};
exportVars = varsObj => ({
from(nameSpace) {
modules[nameSpace] || (modules[nameSpace] = {});
for (let [k,v] of Object.entries(varsObj)) {
modules[nameSpace][k] = v;
}
}
});
importVarsFrom = nameSpace => modules[nameSpace];
}
// *** All of the following is just demo code
// *** showing how to use this export/import functionality:
// my-general-utilities.js (an example file that exports):
{
const wontPolluteTheGlobalScope = 'f';
const myString = wontPolluteTheGlobalScope + 'oo';
const myFunction = (a, b) => a + b;
// the export statement:
exportVars({ myString, myFunction }).from('my-general-utilities');
}
// content.js (an example file that imports):
{
// the import statement:
const { myString, myFunction: sum } = importVarsFrom('my-general-utilities');
console.log(`The imported string is "${myString}".`);
console.log(`The renamed imported function shows that 2 + 3 = ${sum(2,3)}.`);
}
With this example, your manifest.json
should list the files in the following order:
{ ...
"content_scripts": [
{
"js": [
"modules-start.js",
"my-general-utilities.js",
"content.js"
]
}
], ...
}
Quick Setup: use chrome-extension-cli
chrome-extension-cli
package solves this problem by installing and preconfiguring webpack. This allows you to import packages or modules easily
Get Started Immediately
You don’t need to install or configure Webpack.
Webpack comes in preconfigured, so that you can focus on the code.Just create a project, and you’re good to go.
Read More: https://github.com/dutiyesh/chrome-extension-cli
Quick & Simple Bundling With esbuild
If your project requirements permit, consider using a bundler like esbuild
to help avoid compatibility issues by transpiling the syntax into browser-compatible code. The setup is quick and unlike other bundlers such as webpack
, setup of a config file is not required.
Step-by-Step
esbuild
is a fast and modern bundler with quick set-up. Other bundlers like parcel
could work too (see detailed guide).
Installing the Bundler
Run the following command to install
esbuild
as a dependency (see documentation):npm install --save-exact esbuild
Bundling the Scripts
Once installed, you can execute the following command in your root directory (i.e., where
package.json
is located) to bundle your client-side code:esbuild src/content.js --bundle --outfile=dist/bundle.js
Using
esbuild
to bundlecontent.js
will handle all imported modules, includingmy-script.js
, and include them in the outputbundle.js
.Please note that
src
should be replaced with the correct file path forcontent.js
, which I assume is a single entry point. Depending on your project requirements,esbuild
supports set up of multiple entry points too (see documentation).Copying
manifest.json
The
dist
folder should also contain a copy of themanifest.json
file, revised with the updated path to the bundled file.Before:
"content_scripts": [ { "js": [ "content.js" ], } ]
After:
"content_scripts": [ { "js": [ "bundle.js" ], } ]
Explanation
Checking Syntax
Using a bundler like esbuild
will transpile the files as browser-compatible ES syntax by default, meaning it can handle scenarios where there are files with a mix of ES and CommonJS syntax or files with only CommonJS syntax (see documentation).
Browser Compatibility
Browsers can return errors if ES syntax is not a supported version or is not used consistently for imports and exports in scripts. To handle version incompatibility, we can target a specific version of the ES syntax by using the --target
flag in esbuild
(see documentation).
Edit (2024/12/28):
After posting my original answer, I switched to WXT, and it's been working great. It efficiently manages the separate building of multiple entry points without requiring any import
, and it executes much faster than my initial approach. Additionally, since it uses Vite internally, you can configure Vite settings through it if needed.
Original Answer
TL;DR: As a Vite user, I ended up using multiple Vite config files with a script to watch & build the code for now. You can find my sample code here. However, if either or both of the following issues were fixed, I'd change my approach:
- Vite: Generate independent JS bundles with no code sharing between bundles (service workers, multi-page apps, multiple library formats) #12203
- Rollup: Add possibility to disable code splitting / chunks generation #2756
Both have been abandoned by their teams for a long time, though...
Vite users can specify build.rollupOptions
to create an independent JS file that includes all imported code. However, if you also include other files, such as background and popup scripts, you may not be able to use it easily because it still creates separate files if they share imported code. Unfortunately, Rollup currently doesn't support disabling file splitting if you specify multiple inputs.
To handle this using Vite, you need to build bundles for each input so you can specify a single input file for each build.
It would be great if Vite supported building multiple bundles with a single command, but currently it doesn't. As a result, you need to create multiple Vite config files and run multiple vite build
commands. Also, if you want to use vite build --watch
, you need to write a command or a script for it. (In my example, it's source-watch.ts.)
This is a dirty workaround. It takes more time, but at least it solves my problem.
By the way, another answer suggests using a plugin called @crxjs/vite-plugin. You could try it, but currently it only supports Vite 2. Their Vite 3 support is still a work in progress, and Vite 6 is the latest version. Perhaps they'll support Vite 6 soon, but I'm skeptical due to their current activity status. So, for safety, you might not want to rely on it.
Add simply in manifest.json in V2
Note! After changing in manifest.json, make sure to reload the extension and browser tab
{ ...
"content_scripts": [
{
"js": [
"modules-start.js",
"my-general-utilities.js",
"content.js"
]
}
], ...
}
Export the module as a object:
'use strict';
const injectFunction = () => window.alert('hello world');
export {injectFunction};
Then you can import its property:
'use strict';
import {injectFunction} from './my-script.js';
本文标签: javascriptHow to import ES6 modules in content script for Chrome ExtensionStack Overflow
版权声明:本文标题:javascript - How to import ES6 modules in content script for Chrome Extension - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1736762878a1951650.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
import
statement is synchronous which doesn't work well with normal JavaScript. – Derek 朕會功夫 Commented Jan 4, 2018 at 22:24my-script.js
to the list in yourmanifest.json
and it will be loaded according to the order you specified. – Derek 朕會功夫 Commented Jan 4, 2018 at 22:30