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 包的匿名模块初始化行为。
完结,不撒花🌸。