如何在Chrome扩展程序的内容脚本中导入ES6模块

133

在Chrome 61中,JavaScript模块的支持已经添加。现在我正在使用Chrome 63。

我正在尝试在Chrome扩展程序内容脚本中使用import/export语法来使用模块。

manifest.json文件中:

"content_scripts": [
    {
        "js": [
            "content.js"
        ],
    }
]

my-script.js(与content.js位于同一目录)中:


'use strict';

const injectFunction = () => window.alert('hello world');

export default injectFunction;

content.js中:

'use strict';

import injectFunction from './my-script.js';
injectFunction();

我收到了这个错误:Uncaught SyntaxError: Unexpected identifier

如果我把导入语法改成 import {injectFunction} from './my-script.js'; 我会得到这个错误:Uncaught SyntaxError: Unexpected token {

在Chrome扩展中的content.js中使用这种语法是否有问题(因为在HTML中必须使用<script type="module" src="script.js">语法),还是我做错了什么?看起来谷歌会忽略对扩展的支持似乎很奇怪。


1
Chrome支持带星号的模块:仅在脚本为模块脚本时才有效。import语句是同步的,这与普通JavaScript不兼容。 - Derek 朕會功夫
如何将脚本制作成模块脚本? - Ragnar
1
你不需要做任何操作。只需将 my-script.js 的路径添加到 manifest.json 中的列表中,它将按照您指定的顺序加载。 - Derek 朕會功夫
1
你能否提供一个示例作为答案,以便它有可能被标记为正确答案? - Ragnar
11
内容脚本是与页面脚本不同的特殊脚本,通过完全不同的机制注入,因此不支持ES6模块。您可以在清单文件中列出脚本文件,这样它们就会被注入并在相同的执行上下文中运行,即共享全局隔离的世界。 - wOxxOm
12个回答

92

使用动态 import()函数。

与使用<script>元素的不安全解决方法不同,这种方法在相同的 JS 安全环境(内容脚本的隔离世界)中运行,因此您导入的模块仍然可以访问最初的内容脚本的全局变量和函数,包括内置的chromeAPI,如chrome.runtime.sendMessage

content_script.js中,它看起来像:

(async () => {
  const src = chrome.runtime.getURL("your/content_main.js");
  const contentMain = await import(src);
  contentMain.main();
})();
你还需要在清单文件的Web可访问资源中声明导入的脚本:

// ManifestV3

  "web_accessible_resources": [{
     "matches": ["<all_urls>"],
     "resources": ["your/content_main.js"]
   }],

//清单V2

  "web_accessible_resources": [
     "your/content_main.js"
  ]

查看更多详情:

希望对您有所帮助。


16
好的解决方案。值得一提的是,使用这种技术导入的脚本需要在清单文件中作为“web_accessible_resources”进行白名单处理。如果没有将它们列入白名单,就会导致“GET chrome-extension://invalid/ net:: ERR_FAILED”错误。这可能不明显,因为常规内容脚本不需要被列入白名单。 - Petr Srníček
1
几周前这个方法对我有效,但现在在Chrome >= 71中抛出了“请求方案'chrome-extension'不受支持 在sw.js:42”。通过逐步执行代码,我发现sw.js正在使用fetch从扩展沙盒加载我的js文件。我试图确保我没有改变其他东西,但如果有人看到相同的行为,请确认一下。 - SimplGy
2
上面链接的错误已经解决,这个策略应该在Firefox 89中可行。它已经在最新的Nightly版本中运行! - eritbh
1
需要注意的是,您只需要动态导入初始脚本,嵌套导入可以使用 esm 格式完成。 - Madacol
2
如果你正在使用webpack,可以在import调用内部添加/*webpackIgnore: true*/,以便让webpack不去处理这个导入(如果没有这个方法,它对我来说是无效的)。 - fisch2
显示剩余7条评论

49

声明

首先需要说明的是,截至2018年1月,内容脚本不支持模块。这种解决方法通过将包含模块的script标记嵌入到页面中,从而绕过了此限制,并使其返回到您的扩展程序。

这是一种不安全的解决方法!

网页脚本(或其他扩展程序)可以利用Object.prototype和其他原型对象上的setter/getter,代理函数和/或全局对象来提取/欺骗数据,因为script元素内部的代码运行在页面的JS上下文中,而不是默认情况下内容脚本运行的安全隔离JS环境中。

一个安全的解决方法是另一个答案中展示的动态import()

解决方法

这是我的manifest.json

    "content_scripts": [ {
       "js": [
         "content.js"
       ]
    }],
    "web_accessible_resources": [
       "main.js",
       "my-script.js"
    ]

请注意,我在web_accessible_resources中有两个脚本。

这是我的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);

这将把main.js作为模块脚本插入到网页中。

我的所有业务逻辑现在都在main.js中。

为了使这种方法起作用,main.js(以及我将import的所有脚本)必须在清单文件中的web_accessible_resources中。

示例用法:my-script.js

    'use strict';
    
    const injectFunction = () => window.alert('hello world');
    
    export {injectFunction};

main.js 中,这是导入脚本的示例:

    'use strict';
    
    import {injectFunction} from './my-script.js';
    injectFunction();

这个有效!没有错误被抛出,我很开心。 :)


32
不错的解决方案。唯一的问题是chrome.runtime APIs不可用了 :( - Vincent McNabb
9
例如,如果您在上面的 main.js 文件中使用了 chrome.runtime.onMessage.addListener((request, sender, sendResponse), () => ...) ,则该处理程序将永远不会被调用。这是一个设计问题,无法解决。但是,我正在使用 Webpack 将我的扩展编译成一个单独的文件,因此对我来说并不是问题。 - Vincent McNabb
2
另一个缺点是:“模块”是异步的。如果您正在尝试注入应在目标页面自己的js之前执行的js,则不再起作用。当然,这可能取决于个人的需求。 - Offirmo
16
我不建议使用这种模式,因为它会从你的内容脚本中移除上下文隔离。此外,它还允许网站检测到你的扩展程序,这是一种安全漏洞。 - HaNdTriX
3
没有人在推荐它,这是一种解决方法,直到浏览器实现跟上来为止。 @ ragnar:感谢您清晰明了的例子;值得一提的是,在Firefox 56中这种技巧似乎也很有效(需要启用dom.moduleScripts.enabled)。 - Cauterite
显示剩余3条评论

27

import在内容脚本中不可用。

下面是使用全局范围的解决方法。

由于内容脚本位于其自己的'隔离世界'中,它们共享相同的全局命名空间。只有在manifest.json中声明的内容脚本才能访问它。

以下是实现:

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);

同样的方式,您可以将可重用函数和其他演员因素分解为内容脚本文件中的import
注意:内容脚本的全局命名空间对于除内容脚本之外的任何页面都不可用,因此几乎没有全局范围污染。
如果需要导入一些库,则必须使用类似于Parcel的打包工具将您的内容脚本文件与所需的库打包到一个huge-content-script.js中,然后在manifest.json中提及它。
附言:globalThis的文档

2
很好。如果你不习惯的话,这并不是一种容易保持的心态。 - Manolis Pap
1
这对我不起作用,我收到了一个错误:“未捕获的 SyntaxError: 意外的标记 'export'”。 - fguillen
1
@fguillen,请从您的文件中删除export关键字。 - avalanche1
1
重要提示!manifest.json 文件中脚本的顺序非常关键。如果您依赖于一个在当前调用脚本下面的脚本,它将无法使用。 - Dror
回答+评论在这里-绝对的救星! - neonwatty
有没有办法通过这种方法解决no-undefno-unused-vars的eslint错误? - Kolyunya

22

最好的方法是使用像webpack或Rollup这样的打包工具。

我只用了基本的配置就成功了。

const path = require('path');

module.exports = {
  entry: {
    background: './background.js',
    content: './content.js',
  },
  output: {
    filename: '[name].js',
    path: path.resolve(__dirname, '../build')
  }
};

使用该命令运行文件

webpack --config ./ext/webpack-ext.config.js

打包工具将相关的文件组合在一起,我们可以在 Chrome 扩展中使用模块化! :D

您需要将所有其他文件(如清单和静态文件)保留在构建文件夹中。

尝试一些操作,最终您将找到使其正常工作的方法!


3
这怎么没得到更多赞?比动态导入和其他黑科技好太多了 :) - amiregelz
1
你有没有尝试过在Vite中使用它?它使用Rollup,但我的捆绑文件仍然包含“import”。 - Nathan
3
使用Rollup代替 → https://www.extend-chrome.dev/rollup-plugin#usage - Madacol

9
我在尝试解决同样的问题时偶然发现了这个问题。
无论如何,我认为有一个更简单的解决方案可以将您自己的自定义模块注入到您的内容脚本中。我正在研究Jquery注入的方式,我发现您可以通过创建一个IIFE(立即调用函数表达式)并在manifest.json中声明它来实现相同的效果。
大致上是这样的:
在您的manifest.json中:
"content_scripts": [
{
  "matches": ["https://*"],
  "css": ["css/popup.css"],
  "js": ["helpers/helpers.js"]
}],

然后在你的helpers/helpers.js中创建一个IIFE:

var Helpers = (function() {
  var getRandomArbitrary = function(min, max) {
    return Math.floor(Math.random() * (max - min)) + min;
  }
  return {
    getRandomArbitrary: getRandomArbitrary
  }
})()

现在,您可以在内容脚本中自由使用您的辅助函数:
Helpers.getRandomArbitrary(0, 10) // voila!

如果你使用这种方法来重构一些通用函数,我认为这是很好的。希望这能帮到你!


1
好主意!如果我说错了,请告诉我,这些内容脚本是在用户每次加载页面时立即包含的吗? - Boiethios
@Boiethios 如果你设置 "run_at": "document_start",它们会在页面加载开始时运行,如果你设置 "document_end",它们会在页面加载结束时运行。 - Thorvarium

4

对于Vite用户

有一个非常棒的插件叫做crxjs,你只需要在vite.config.ts中更新它,并给出你的manifest.json路径(它仅适用于mv3)

按照以下步骤运行你的脚本

1.将crxjs添加到你的项目中

npm install @crxjs/vite-plugin -D

2.创建或更新manifest.json

{
  "manifest_version": 3,
  "name": "CRXJS React Vite Example",
  "version": "1.0.0",
  "action": { "default_popup": "index.html" }
}

3. 更新你的 vite.config.ts 文件,并将路径指向清单

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 }),
  ],
})

运行项目后,现在config.js将被捆绑,您可以在其中导入包。


1
天啊,我一直在自己配置 Vite 直到遇到了这个问题。谢谢,这是有史以来最好的工具! - Yulian

1

简短回答:

你可以通过创建以下文件并在manifest.json中尽早列出它来模仿一些import/export的功能和获得一些好处,这适用于浏览器扩展:

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];
}

然后,像这样从一个文件/模块导出:

exportVars({ var1, var2, var3 }).from('my-utility');

像这样导入到另一个文件/模块中:

const { var1, var3: newNameForVar3 } = importVarsFrom('my-utility');

讨论:
此策略:
  • 允许在浏览器扩展中使用模块化代码,使您可以将代码拆分为多个文件,但不会因不同文件之间共享的全局范围而出现变量冲突。
  • 仍然允许您导出和导入变量到不同的JavaScript文件/模块中。
  • 只引入了两个全局变量,即导出函数和导入函数。
  • 在每个文件中保持完整的浏览器扩展功能(例如chrome.runtime等),这些功能被另一个答案中的方法(目前是接受的答案)使用模块脚本标签嵌入所消除。
  • 使用类似于JavaScript中真正的importexport函数的简洁语法
  • 允许命名空间,可以是导出模块的文件名,类似于JavaScript中真正的importexport命令的工作方式,但不一定是(即名称空间名称可以是任何您想要的名称),并且
  • 允许类似于import { fn as myFn }...的导入时进行变量重命名
为了实现这一点,您的manifest.json需要按照以下方式加载JavaScript:
  • 首先是建立导出/导入函数的文件(在下面的示例中命名为modules-start.js),
  • 其次是导出文件,
  • 最后是导入文件。
当然,您可能有一个既导入又导出的文件。在这种情况下,只需确保它在从中导入的文件之后但在导出到的文件之前列出即可。
下面的代码演示了这种策略。
需要注意的是,每个模块/文件中的所有代码都包含在花括号中。唯一的例外是modules-start.js中的第一行,它将导出和导入函数作为全局变量进行了定义。
下面的代码片段中的代码必须被放置在单个“位置”中。但是,在实际项目中,代码可以分成单独的文件。请注意,即使在这里的人工上下文中(即在下面的单个代码片段中),这种策略也允许其包含的不同代码部分是模块化的,但仍然相互连接。

// 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)}.`);
}

使用此示例,您的manifest.json应按以下顺序列出文件:
{ ...
  "content_scripts": [
    {
      "js": [
        "modules-start.js",
        "my-general-utilities.js",
        "content.js"
      ]
    }
  ], ...
}

1

使用 Rollup 打包工具

完整教程: https://www.extend-chrome.dev/rollup-plugin#usage


简述

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"
  }
}

1

使用 esbuild

参考Dhruvil的回答,这里有一个GitHub仓库展示如何使用esbuild来打包用TypeScript和React编写的内容脚本,从而使您能够导入es6模块。

它还包括将后台服务工作者和弹出窗口进行打包,并使用脚本在本地运行弹出窗口时启用热模块重新加载。


0

在 V2 中的 manifest.json 中添加简单地

注意! 在更改 manifest.json 后,请确保重新加载扩展程序和浏览器选项卡

{ ...
  "content_scripts": [
    {
      "js": [
        "modules-start.js",
        "my-general-utilities.js",
        "content.js"
      ]
    }
  ], ...
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接