冷知识。🤔 何为 AMD 匿名模块冲突???

Uncaught Error: Can only have one anonymous define call per script file at c.enqueueDefineAnonymousModule (loader.js:1340:23)

最近踩了一个 amd 匿名模块的坑。

关于 amd

啥是 amd?amd 是 web 前端上古时代的模块化规范。 amd(Asynchronous Module Definition)是​​面向浏览器端的异步模块加载规范​​,核心解决依赖管理与异步加载问题。典型实现库为RequireJS。

虽然现在遍地都是 esm + cjs,但 amd 也应用于许多运行时的插件系统(如 vscode 编辑器)。

基本语法

js 复制代码
    // 模块注册
    define([id?], [dependencies?], factory)
    // 模块导入使用
    require(id, (mod) => {})

amd 的匿名/具名模块

匿名模块指的是 define 时,没有传入模块 id 的模块,反之则是具名模块,如下

js 复制代码
// 匿名模块 a.js
define([], () => {
    return { name: 'a' }
})
// 具名模块 b.js
define('b', [], () => {
    return { name: 'b' }
})

大部分情况下,模块开发者都不需关注模块的 id,使用匿名模块进行 define 符合开发直觉。

在引入匿名模块时,通过 require 函数指定模块的 id 即可,id 也可以是一个 uri。

html 复制代码
<script src='amd_loader.js'></script>
<script>
  // 引入模块
  require(['a'], function(a) {
  });
</script>
<!-- 然后加载器会请求 a.js 并执行,该文件包含: -->
<script src="a.js">
  // a.js 内容
  define([], function() {
    return { name: 'a' }; // 这个匿名模块会被关联到 'a'
  });
</script>

具名模块则用于模块的预定义,即提前声明,后面再通过 require 引入使用,如

html 复制代码
<script src='amd_loader.js'></script>
<script src="b.js">
  // 声明一个 id 为 b 的模块
  define('b', [], function() {
    return { name: 'b' }; // 这个匿名模块会被关联到 'a'
  });
</script>
<script>
  // require 该模块
  require(['b'], function(b) {
    // 使用模块
  });
</script>

提问:假设我引入了一个「声明了两个匿名模块」的模块,会怎样?如下,最终真正引入的是 a1 还是 a2 ???

html 复制代码
<script src='amd_loader.js'></script>
<script>
  require(['a.js'], function(a) {
     console.log(a.name);
  });
</script>
<script src="a.js">
  // a.js 内容
  define([], function() {
    return { name: 'a1' };
  });
  define([], function() {
    return { name: 'a2' }; // 这个匿名模块会被关联到 'a'
  });
</script>

答案是跟 amd 的实现有关系,在 vscode 提供的 amd loader 中,同一文件声明了两个匿名模块会直接抛出错误。

如果是高级版本的 requirejs,则会进行兼容,至少保证不会白屏。

下图为 vscode amd loader 的匿名模块导入检测逻辑。

匿名模块冲突

💩⛰️ 项目中使用到了 monaco editor(一个基于 vscode 的 web 端编辑器),以及在运行时也用到了 js-beautify,prettier 这两个包,进行代码格式化。

在某些页面打开时出现一些奇怪的控制台报错 Can only have one anonymous define call per script file

分析思路

这个报错来自于 vscode 的 loader 文件,用于初始化环境,导入导出插件等用途,是 vscode 十分关键的文件。

vscode 的 loader 文件中包含一段默认逻辑,若当前浏览器不是 amd 环境,vscode 会将初始化一个 amd 环境,即声明 window.define, window.define.amd, window.require 等全局变量,这也会导致当前的浏览器环境直接变成 amd 环境,影响到后续异步模块的初始化行为。

于是打了几个断点,如下,在源码中可以看到 js-beautify 的入口文件 define 了一个匿名模块。也就是只要引入了 js-beautify 就会马上声明一个匿名模块。

编译后的代码(js-beautify)

源代码(js-beautify)

接着,在 prettier 的入口文件中,也有同样的行为,声明了一个匿名模块。

编译后的代码(prettier)

源代码(prettier)

因此,原因就是 define 了两个匿名模块,导致 amd loader 抛出错误。

以上可以等价这段伪代码

html 复制代码
<script src='vscode_amd_loader.js'>
    // 初始化 vscode 的 amd loader...
</script>
<script src="async_js-beautify.js">
    // js-beautify.js 内容,定义匿名模块
    define([
        "./lib/beautify",
        "./lib/beautify-css",
        "./lib/beautify-html"
    ], function(js_beautify, css_beautify, html_beautify) {
        return get_beautify(js_beautify, css_beautify, html_beautify);
    });
</script>
<script src="async_prettier.js">
    // prettier.js 内容,再次定义匿名模块
    define([], factory);
</script>

提前 define 了两个匿名模块,此时会被 vscode amd loader 检测到,最终导致白屏。

修复方式

以下两种方式皆可

  • 不用 vscode 的 amd loader,用 requirejs 先把 amd 环境初始化好

  • 修改 js-beautify 和 prettier 包的匿名模块初始化行为。

完结,不撒花🌸。

相关推荐
福兮说5 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理5 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6666 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
凤城老人6 小时前
从 PyQt6 到 Electron:给 Edge TTS 做一个“多角色配音机“的踩坑手记
javascript·typescript·electron
Dovis(誓平步青云)6 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan7 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_7 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒8 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技8 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu9 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub