前端模块化的前世今生(上)https://developer.aliyun.com/article/1411381
4. AMD
(1)概念
CommonJS 的缺点之一是它是同步的,AMD 旨在通过规范中定义的 API 异步加载模块及其依赖项来解决这个问题。AMD 全称为 Asynchronous Module Definition,即异步模块加载机制。它规定了如何定义模块,如何对外输出,如何引入依赖。
AMD规范重要特性就是异步加载。所谓异步加载,就是指同时并发加载所依赖的模块,当所有依赖模块都加载完成之后,再执行当前模块的回调函数。这种加载方式和浏览器环境的性能需求刚好吻合。
① 语法
AMD 规范定义了一个全局函数 define,通过它就可以定义和引用模块,它有 3 个参数:
define(id?, dependencies?, factory);
其包含三个参数:
id
:可选,指模块路径。如果没有提供该参数,模块名称默认为模块加载器请求的指定脚本的路径。dependencies
:可选,指模块数组。它定义了所依赖的模块。依赖模块必须根据模块的工厂函数优先级执行,并且执行的结果应该按照依赖数组中的位置顺序以参数的形式传入工厂函数中。factory
:为模块初始化要执行的函数或对象。如果是函数,那么该函数是单例模式,只会被执行一次;如果是对象,此对象应该为模块的输出值。
除此之外,要想使用此模块,就需要使用规范中定义的 require 函数:
require(dependencies?, callback);
其包含两个参数:
dependencies
:依赖项数组;callback
:加载模块时执行的回调函数。
有关 AMD API 的更详细说明,可以查看 GitHub 上的 AMD API 规范:github.com/amdjs/amdjs…。
② 兼容性
该规范的浏览器兼容性如下:
③ 优缺点
AMD 的优点:
- 异步加载导致更好的启动时间;
- 能够将模块拆分为多个文件;
- 支持构造函数;
- 无需额外工具即可在浏览器中工作。
AMD 的缺点:
- 语法很复杂,学习成本高;
- 需要一个像 RequireJS 这样的加载器库来使用 AMD。
(2)使用
当然,上面只是 AMD 规范的理论,要想理解这个理论在代码中是如何工作的,就需要来看看 AMD 的实际实现。RequireJS 就是 AMD 规范的一种实现,它被描述为“JavaScript 文件和模块加载器”。下面就来看看 RequireJS 是如何使用的。
① 引入RequireJS
可以通过 npm 来安装 RequireJS:
npm i requirejs
也可以在 html 文件引入 require.js
文件:
<scriptdata-main="js/config"src="js/require.js">script>
这里 script
标签有两个属性:
data-main="js/config"
:这是 RequireJS 的入口,也是配置它的地方;src="js/require.js"
:加载脚本的正常方式,会加载require.js
文件。
在 script
标签下添加以下代码来初始化 RequireJS:
<script> require(['config'], function() { //... }) </script>
当页面加载完配置文件之后, require()
中的代码就会运行。这个 script
标签是一个异步调用,这意味着当 RequireJS 通过 src="js/require.js
加载时,它将异步加载 data-main
属性中指定的配置文件。因此,该标签下的任何 JavaScript 代码都可以在 RequireJS 获取时执行配置文件。
那 AMD 中的 require()
和 CommonJS 中的 require()
有什么区别呢?
- AMD
require(
) 接受一个依赖数组和一个回调函数,CommonJSrequire()
接受一个模块 ID; - AMD
require()
是异步的,而 CommonJSrequire()
是同步的。
② 定义 AMD 模块
下面是 AMD 中的一个基本模块定义:
define(['dependency1', 'dependency2'], function() {
// 模块内容});
这个模块定义清楚地显示了其包含两个依赖项和一个函数。
下面来定义一个名为addition.js
的文件,其包含一个执行加法操作的函数,但是没有依赖项:
// addition.js define(function() { return function(a, b) { alert(a + b); } });
再来定义一个名为 calculator.js
的文件:
define(['addition'], function(addition) { addition(7, 9); });
当 RequireJS 看到上面的代码块时,它会去寻找依赖项,并通过将它们作为参数传递给函数来自动将其注入到模块中。
RequireJS 会自动为 addition.js
和 calculator.js
文件创建一个 </code> 标签,并将其放在HTML <code><head></code> 元素中,等待它们加载,然后运行函数,这类似于 <code>require()</code> 的行为。</div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fyjbvedpo6nmku_e416066b848e454ab8408ff5764c68f1.jpg%22%2C%22originWidth%22%3A730%2C%22originHeight%22%3A378%2C%22name%22%3A%2243.webp.jpg%22%2C%22size%22%3A125663%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Atrue%2C%22bottom%22%3Atrue%7D%2C%22width%22%3A730%2C%22height%22%3A378%7D"></span></div><div>下面来更新一下 <code>index.html</code> 文件:<span><br /></span></div><pre><code><span>// index.html</span> <span>require</span>([<span>'config'</span>], <span>function</span>() { <span> <span>require</span>([<span>'calculator'</span>]);</span> <span>});</span> </code></pre><div>当浏览器加载 <code>index.html</code> 文件时,RequireJS 会尝试查找 <code>calculator.js</code> 模块,但是没有找到,所以浏览器也不会有任何反应。那该如何解决这个问题呢?我们必须提供配置文件来告诉 RequireJS 在哪里可以找到 <code>calculator.js</code>(和其他模块),因为它是引用的入口。</div><div>下面是配置文件的基本结构:<span><br /></span></div><pre><code><span>requirejs.<span>config</span>({</span> <span> <span>baseURL</span>: <span>"string"</span>,</span> <span> <span>paths</span>: {},</span> <span> <span>shim</span>: {},</span> <span>});</span> </code></pre><div>这里有三个属性值:</div><ul><li><code>baseURL</code>:告诉 RequireJS 在哪里可以找到模块;</li><li><code>path</code>:这些是与 define() 一起使用的模块的名称。 在路径中,可以使用文件的 CDN,这时 RequireJS 将尝试在本地可用的模块之前加载模块的 CDN 版本;</li><li><code>shim</code>:允许加载未编写为 AMD 模块的库,并允许以正确的顺序加载它们</li></ul><div>我们的配置文件如下:<span><br /></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22requirejs.config(%7B%5Cn%20%20%20%20baseURL%3A%20%5C%22js%5C%22%2C%5Cn%20%20%20%20paths%3A%20%7B%5Cn%20%20%20%20%20%20%20%20%2F%2F%20%E8%BF%99%E7%A7%8D%E6%83%85%E5%86%B5%E4%B8%8B%EF%BC%8C%E6%A8%A1%E5%9D%97%E4%BD%8D%E4%BA%8E%20customScripts%20%E6%96%87%E4%BB%B6%E4%B8%AD%5Cn%20%20%20%20%20%20%20%20addition%3A%20%5C%22customScripts%2Faddition%5C%22%2C%5Cn%20%20%20%20%20%20%20%20calculator%3A%20%5C%22customScripts%2Fcalculator%5C%22%2C%5Cn%20%20%20%20%7D%2C%5Cn%7D)%3B%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22D0U1D%22%7D"></div><div>配置完成之后,重新加载浏览器,就会收到浏览器的弹窗:</div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fyjbvedpo6nmku_714349fa05e94a57ace7ce28c34fdacc.jpg%22%2C%22originWidth%22%3A730%2C%22originHeight%22%3A348%2C%22name%22%3A%2256.webp.jpg%22%2C%22size%22%3A74032%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Atrue%2C%22bottom%22%3Atrue%7D%2C%22width%22%3A730%2C%22height%22%3A348%7D"></span></div><div>这就是在 AMD 中使用 RequireJS 定义模块的方法之一。我们还可以通过指定其路径名来定义模块,该路径名是模块文件在项目目录中的位置。 下面给出一个例子:<span><br /></span></div><pre><code><span>define</span>(<span>"path/to/module"</span>, <span>function</span>() { <span> <span>// 模块内容</span></span> <span>})</span> </code></pre><div>当然,RequireJS 并不鼓励这种方法,因为当我们将模块移动到项目中的另一个位置时,就需要手动更改模块中的路径名。</div><div>在使用 AMD 定义模块时需要注意:</div><ul><li>在依赖项数组中列出的任何内容都必须与工厂函数中的分配相匹配;</li><li>尽量不要将异步代码与同步代码混用。当在 index.html 上编写其他 JavaScript 代码时就是这种情况。</li></ul><div><br /></div><h2 id="EvQgg">5. CMD</h2><div><br /></div><div>CMD 全称为 Common Module Definition,即通用模块定义。CMD 规范整合了 CommonJS 和 AMD 规范的特点。sea.js 是 CMD 规范的一个实现 。</div><div>CMD 定义模块也是通过一个全局函数 <code>define</code> 来实现的,但只有一个参数,该参数既可以是函数也可以是对象:</div><div><span><br /></span></div><pre><code><span>define</span>(factory); </code></pre><div>如果这个参数是对象,那么模块导出的就是对象;如果这个参数为函数,那么这个函数会被传入 3 个参数:<span><br /></span></div><pre><code><span>define</span>(<span>function</span>(<span>require</span>, <span>exports</span>, <span>module</span>) { <span> <span>//...</span></span> <span>});</span> </code></pre><div>这三个参数分别如下: (1)<code>require</code>:一个函数,通过调用它可以引用其他模块,也可以调用 <code>require.async</code> 函数来异步调用模块; (2)<code>exports</code>:一个对象,当定义模块的时候,需要通过向参数 <code>exports</code> 添加属性来导出模块 API; (3)<code>module</code> 是一个对象,它包含 3 个属性:</div><ul><li><code>uri</code>:模块完整的 URI 路径;</li><li><code>dependencies</code>:模块依赖;</li><li><code>exports</code>:模块需要被导出的 API,作用同第二个参数 <code>exports</code>。</li></ul><div>下面来看一个例子,定义一个 <code>increment</code> 模块,引用 <code>math</code> 模块的 <code>add</code> 函数,经过封装后导出成 <code>increment</code> 函数:</div><div><span><br /></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22define(function(require%2C%20exports%2C%20module)%20%7B%5Cn%20%20var%20add%20%3D%20require('math').add%3B%5Cn%20%20exports.increment%20%3D%20function(val)%20%7B%5Cn%20%20%20%20return%20add(val%2C%201)%3B%5Cn%20%20%7D%3B%5Cn%20%20module.id%20%3D%20%5C%22increment%5C%22%3B%5Cn%7D)%3B%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22iOYqr%22%7D"></div><div>CMD 最大的特点就是懒加载,不需要在定义模块的时候声明依赖,可以在模块执行时动态加载依赖。除此之外,CMD 同时支持<strong>同步加载模块</strong>和<strong>异步加载模块</strong>。</div><div><strong>AMD 和 CMD 的两个主要区别如下:</strong></div><ul><li>AMD 需要异步加载模块,而 CMD 在加载模块时,可以同步加载(<code>require</code>),也可以异步加载(<code>require.async</code>)。</li><li>CMD 遵循依赖就近原则,AMD 遵循依赖前置原则。也就是说,在 AMD 中,需要把模块所需要的依赖都提前在依赖数组中声明。而在 CMD 中,只需要在具体代码逻辑内,使用依赖前,把依赖的模块 <code>require</code> 进来。</li></ul><div><br /></div><h2 id="fk8Sf">6. UMD</h2><div><br /></div><div>UMD 全程为 Universal Module Definition,即<strong>统一模块定义</strong>。其实 UMD 并不是一个模块管理规范,而是带有前后端同构思想的模块封装工具。</div><div>UMD 是一组同时支持 AMD 和 CommonJS 的模式,它旨在使代码无论执行代码的环境如何都能正常工作,通过 UMD 可以在合适的环境选择对应的模块规范。比如在 Node.js 环境中采用 CommonJS 模块管理,在浏览器环境且支持 AMD 的情况下采用 AMD 模块,否则导出为全局函数。</div><div>一个UMD模块由两部分组成:</div><ul><li><strong>立即调用函数表达式 (IIFE)</strong>:它会检查使用模块的环境。其有两个参数:<code>root</code> 和 <code>factory</code>。 <code>root</code> 是对全局范围的 this 引用,而 <code>factory</code> 是定义模块的函数。</li><li><strong>匿名函数:</strong> 创建模块,此匿名函数被传递任意数量的参数以指定模块的依赖关系。</li></ul><div>UMD 的代码实现如下:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22(function%20(root%2C%20factory)%20%7B%5Cn%20%20if%20(typeof%20define%20%3D%3D%3D%20'function'%20%26%26%20define.amd)%20%7B%5Cn%20%20%20%20define(%5B%5D%2C%20factory)%3B%5Cn%20%20%7D%20else%20if%20(typeof%20exports%20%3D%3D%3D%20'object')%20%7B%5Cn%20%20%20%20module.exports%2C%5Cn%20%20%20%20module.exports%20%3D%20factory()%3B%5Cn%20%20%7D%20else%20%7B%5Cn%20%20%20%20root.returnExports%20%3D%20factory()%3B%5Cn%20%20%7D%5Cn%7D(this%2C%20function%20()%20%7B%5Cn%20%20%2F%2F%20%E6%A8%A1%E5%9D%97%E5%86%85%E5%AE%B9%E5%AE%9A%E4%B9%89%5Cn%20%20return%20%7B%7D%3B%5Cn%7D))%3B%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22Wg25j%22%7D"></div><div>它的执行过程如下:</div><ol><li>先判断是否支持 Node.js 模块格式(exports 是否存在),存在则使用 Node.js 模块格式;</li><li>再判断是否支持 AMD(define 是否存在),存在则使用 AMD 方式加载模块;</li><li>若两个都不存在,则将模块公开到全局(Window 或 Global)。</li></ol><div><strong>UMD的特点如下:</strong>① UMD 的优点:</div><ul><li>小而简洁;</li><li>适用于服务器端和客户端。</li></ul><div>② UMD 的缺点:</div><ul><li>不容易正确配置。</li></ul><div><br /></div><h2 id="e0Fis">7. ES 模块</h2><div><br /></div><h3 id="OdSGE">(1)概念</h3><div>通过上面的例子,你可能会发现,使用 UMD、AMD、CMD 的代码会变得难以编写和理解。于是在 2015 年,负责 ECMAScript 规范的 TC39 委员会将模块添加为 JavaScript 的内置功能,这些模块称为 ECMAScript模块,简称 ES 模块。</div><div>模块和经典 JavaScript 脚本略有不同:</div><ul><li>模块默认启用**严格模式,**比如分配给未声明的变量会报错:<span><br /></span></li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20type%3D%5C%22module%5C%22%3E%5Cn%20%20a%20%3D%205%3B%20%5Cn%3C%2Fscript%3E%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22ysetF%22%7D"></div><ul><li>模块有一个词法顶级作用域。 这意味着,例如,运行 var foo = 42; 在模块内不会创建名为 foo 的全局变量,可通过浏览器中的 window.foo 访问,尽管在经典JavaScript脚本中会出现这种情况;</li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20type%3D%5C%22module%5C%22%3E%5Cn%20%20let%20person%20%3D%20%5C%22Alok%5C%22%3B%5Cn%3C%2Fscript%3E%5Cn%5Cn%3Cscript%20type%3D%5C%22module%5C%22%3E%5Cn%20%20%20alert(person)%3B%7B%2F*%20Error%3A%20person%20is%20not%20defined%20*%2F%7D%5Cn%3C%2Fscript%3E%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22oxTqr%22%7D"></div><ul><li>模块中的 this 并不引用全局 this,而是 undefined。 (如果需要访问全局 this,可以使用 globalThis);</li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%3E%5Cn%20%20alert(this)%3B%20%7B%2F*%20%E5%85%A8%E5%B1%80%E5%AF%B9%E8%B1%A1%20*%2F%7D%5Cn%3C%2Fscript%3E%5Cn%5Cn%3Cscript%20type%3D%5C%22module%5C%22%3E%5Cn%20%20alert(this)%3B%20%7B%2F*%20undefined%20*%2F%7D%5Cn%3C%2Fscript%3E%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22gkYIW%22%7D"></div><ul><li>新的静态导入和导出语法仅在模块中可用,并不适用于经典脚本。</li><li>顶层 await 在模块中可用,但在经典 JavaScript 脚本中不可用;</li><li>await 不能在模块中的任何地方用作变量名,经典脚本中的变量可以在异步函数之外命名为 await;</li><li>JavaScript 会提升 import 语句。因此,可以在模块中的任何位置定义它们。</li></ul><div>CommonJS 和 AMD 都是在运行时确定依赖关系,即运行时加载,CommonJS 加载的是拷贝。而 ES 模块是在编译时就确定依赖关系,所有加载的其实都是引用,这样做的好处是可以执行静态分析和类型检查。</div><h3 id="2ceg6">(2)语法</h3><h4 id="GAszE">① 导出</h4><div>当导出模块代码时,需要在其前面添加 export 关键词。导出内容可以是变量、函数或类。任何未导出的代码都是模块私有的,无法在该模块之被外访问。ES 模块支持两种类型的导出:</div><ul><li><strong>命名导出:</strong></li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22export%20const%20first%20%3D%20'JavaScript'%3B%5Cnexport%20function%20func()%20%7B%5Cn%20%20%20%20return%20true%3B%5Cn%7D%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22h5NMD%22%7D"></div><div>当然,我们也可以先定义需要导出的变量/函数,最后统一导出这些变量/函数:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22const%20first%20%3D%20'JavaScript'%3B%5Cnconst%20second%20%3D%20'TypeScript'%3B%5Cnfunction%20func()%20%7B%5Cn%20%20%20%20return%20true%3B%5Cn%7D%5Cnexport%20%7Bfirst%2C%20second%2C%20func%7D%3B%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22dmvJy%22%7D"></div><ul><li><strong>默认导出:</strong></li></ul><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22function%20func()%20%7B%5Cn%20%20%20%20return%20true%3B%5Cn%7D%5Cn%5Cnexport%20default%20func%3B%5Cn%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22oiPjC%22%7D"></div><div>当然,也可以直接默认导出:<span><br /></span></div><pre><code><span>export</span> <span>default</span> <span>function</span> <span>func</span>() { <span> <span>return</span> <span>true</span>;</span> <span>}</span> </code></pre><div>默认导出可以省略变量/函数/类名,在导入时可以为其指定任意名称:<span><br /></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%2F%2F%20%E5%AF%BC%E5%87%BA%5Cnexport%20default%20function%20()%20%7B%5Cn%20%20console.log('foo')%3B%5Cn%7D%5Cn%2F%2F%20%E5%AF%BC%E5%85%A5%5Cnimport%20customName%20from%20'.%2Fmodule'%3B%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22G67hn%22%7D"></div><div><strong>注意:</strong> 导入默认模块时不需要大括号,导出默认的变量或方法可以有名字,但是对外是无效的。<code>export default</code> 在一个模块文件中只能使用一次。</div><div>可以使用 as 关键字来重命名需要暴露出的变量或方法,经过重命名后同一变量可以多次暴露出去:<span><br /></span></div><pre><code><span>const</span> first = <span>'test'</span>; <span>export</span> {first <span>as</span> second}; </code></pre><h4 id="uJSky">② 导入</h4><div>使用<strong>命名导出</strong>的模块,可以通过以下方式来导入:<span><br /></span></div><pre><code><span>import</span> {first, second, func} <span>from</span> <span>'./module'</span>; </code></pre><div>使用<strong>默认导出</strong>的模块,可以通过以下方式来引入,导入名称可以自定义,无论导出的名称是什么:<span><br /></span></div><pre><code><span>import</span> customName <span>from</span> <span>'./module.js'</span>; </code></pre><div>导入模块位置可以是相对路径也可以是绝对路径,<code>.js</code>扩展名是可以省略的,如果不带路径而只是模块名,则需要通过配置文件告诉引擎查找的位置:<span><br /></span></div><pre><code><span>import</span> {firstName, lastName} <span>from</span> <span>'./module'</span>; </code></pre><div>可以使用 as 关键字来将导入的变量/函数重命名:<span><br /></span></div><pre><code><span>import</span> { fn <span>as</span> fn1 } <span>from</span> <span>'./profile'</span>; </code></pre><div>在 ES 模块中,默认导入和命名导入是可以同时使用的,比如在 React 组件中:<span><br /></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22import%20React%2C%20%7Busestate%2C%20useEffect%7D%20from%20'react'%3B%5Cnconst%20Comp%20%3D%20()%20%3D%3E%20%7B%5Cn%5Ctreturn%20%3CReact.Fragment%3E...%3C%2FReact.Fragment%3E%20%5Cn%7D%5Cnexport%20default%20Comp%3B%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22ypbsa%22%7D"></div><div>可以使用 as 关键字来加载整个模块,用于从另一个模块中导入所有命名导出,会忽略默认导出:</div><div><span>javascript</span></div><div>复制代码</div><pre><code><span>import</span> * <span>as</span> circle <span>from</span> <span>'./circle'</span>; <span>console</span>.<span>log</span>(<span>'圆面积:'</span> + circle.<span>area</span>(<span>4</span>)); <span>console</span>.<span>log</span>(<span>'圆周长:'</span> + circle.<span>circumference</span>(<span>14</span>)); </code></pre><h4 id="ISAry">③ 动态导入</h4><div>上面我们介绍的都是静态导入,使用静态 import 时,整个模块需要先下载并执行,然后主代码才能执行。有时我们不想预先加载模块,而是按需加载,仅在需要时才加载。这可以提高初始加载时的性能,动态 import 使这成为可能:<span><br /></span></div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22%3Cscript%20type%3D%5C%22module%5C%22%3E%5Cn%20%20(async%20()%20%3D%3E%20%7B%5Cn%20%20%20%20const%20moduleSpecifier%20%3D%20'.%2Flib.mjs'%3B%5Cn%20%20%20%20const%20%7Brepeat%2C%20shout%7D%20%3D%20await%20import(moduleSpecifier)%3B%5Cn%20%20%20%20repeat('hello')%3B%5Cn%20%20%20%20%2F%2F%20%E2%86%92%20'hello%20hello'%5Cn%20%20%20%20shout('Dynamic%20import%20in%20action')%3B%5Cn%20%20%20%20%2F%2F%20%E2%86%92%20'DYNAMIC%20IMPORT%20IN%20ACTION!'%5Cn%20%20%7D)()%3B%5Cn%3C%2Fscript%3E%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%2282beE%22%7D"></div><div>与静态导入不同,动态导入可以在常规脚本中使用。</div><h4 id="D6kxE">④ 其他用法</h4><div>可以使用以下方式来先导入后导出模块内容:</div><div><span>javascript</span></div><div>复制代码</div><pre><code><span>export</span> { foo, bar } <span>from</span> <span>'./module'</span>; </code></pre><div>上面的代码就等同于:</div><div><span>javascript</span></div><div>复制代码</div><pre><code><span>import</span> { foo, bar } <span>from</span> <span>'./module'</span>; <span>export</span> { foo, boo}; </code></pre><div>另一个与模块相关的新功能是<code>import.meta</code>,它是一个给 JavaScript 模块暴露特定上下文的元数据属性的对象。它包含了这个模块的信息,比如说这个模块的 URL。</div><div>默认情况下,图像是相对于 HTML 文档中的当前 URL 加载的。<code>import.meta.url</code>可以改为加载相对于当前模块的图像:</div><div data-card-type="block" data-ready-card="codeblock" data-card-value="data:%7B%22mode%22%3A%22plain%22%2C%22code%22%3A%22function%20loadThumbnail(relativePath)%20%7B%5Cn%20%20const%20url%20%3D%20new%20URL(relativePath%2C%20import.meta.url)%3B%5Cn%20%20const%20image%20%3D%20new%20Image()%3B%5Cn%20%20image.src%20%3D%20url%3B%5Cn%20%20return%20image%3B%5Cn%7D%5Cnconst%20thumbnail%20%3D%20loadThumbnail('..%2Fimg%2Fthumbnail.png')%3B%5Cncontainer.append(thumbnail)%3B%22%2C%22heightLimit%22%3Atrue%2C%22margin%22%3Atrue%2C%22id%22%3A%22EO5Iq%22%7D"></div><h3 id="wHeoE">(3)在浏览器使用</h3><div>目前主流浏览器都支持 ES 模块:</div><div><span data-card-type="inline" data-ready-card="image" data-card-value="data:%7B%22src%22%3A%22https%3A%2F%2Fucc.alicdn.com%2Fpic%2Fdeveloper-ecology%2Fyjbvedpo6nmku_fd404a90993e408da533b00860920597.jpg%22%2C%22originWidth%22%3A1512%2C%22originHeight%22%3A539%2C%22name%22%3A%2266.webp.jpg%22%2C%22size%22%3A252509%2C%22display%22%3A%22inline%22%2C%22align%22%3A%22left%22%2C%22linkTarget%22%3A%22_blank%22%2C%22status%22%3A%22done%22%2C%22style%22%3A%22none%22%2C%22search%22%3A%22%22%2C%22margin%22%3A%7B%22top%22%3Atrue%2C%22bottom%22%3Atrue%7D%2C%22width%22%3A1512%2C%22height%22%3A539%7D"></span></div><div>如果想在浏览器中使用原生 ES 模块方案,只需要在 script 标签上添加 <code>type="module"</code> 属性。通过该属性,浏览器知道这个文件是以模块化的方式运行的。而对于不支持的浏览器,需要通过 <code>nomodule</code> 属性来指定某脚本为 fallback 方案:<span><br /></span></div><pre><code><span><<span>script</span> <span>type</span>=<span>"module"</span>></span> <span> <span>import</span> module1 <span>from</span> <span>'./module1'</span></span> <span></<span>script</span>></span> <span><<span>script</span> <span>nomodule</span> <span>src</span>=<span>"fallback.js"</span>></span><span></<span>script</span>></span> </code></pre><div>支持 <code>type="module"</code> 的浏览器会忽略带有 <code>nomodule</code> 属性的脚本。使用 <code>type="module"</code> 的另一个作用就是进行 ES Next 兼容性的嗅探。因为支持 ES 模块化的浏览器,都支持 ES Promise 等特性。</div><div>由于默认情况下模块是延迟的,因此可能还希望以延迟方式加载 nomodule 脚本:<span><br /></span></div><pre><code><span><<span>script</span> <span>nomodule</span> <span>defer</span> <span>src</span>=<span>"fallback.js"</span>></span><span></<span>script</span>></span> </code></pre><h3 id="s9m4C">(4)在 Node.js 使用</h3><div>上面提到,Node.js 使用的是 CommonJS 模块规范,它也是支持 ES 模块的。在 Node.js 13 之前,ES 模块是一项实验性技术,因此,可以通过使用 <code>.mjs</code> 扩展名保存模块并通过标志访问它来使用模块。</div><div>从 Node.js 13 开始,可以通过以下两种方式使用模块:</div><ul><li>使用 <code>.mjs</code> 扩展名保存模块;</li><li>在最近的文件夹中创建一个 <code>type="module"</code> 的 <code>package.json</code> 文件。</li></ul><div>那如何在小于等于 12 版本的 Node.js 中使用 ES 模块呢?可以在执行脚本启动时加上 <code>--experimental-modules</code>,不过这一用法要求相应的文件后缀名必须为 <code>.mjs</code>:</div><div><span>javascript</span></div><div>复制代码</div><pre><code><span>node --experimental-modules module1.<span>mjs</span></span> <span>import</span> module1 <span>from</span> <span>'./module1.mjs'</span> <span>module1</span> </code></pre><div><strong>参考:</strong></div><blockquote><ul><li><a href="https://link.juejin.cn?spm=a2c6h.13046898.publish-article.23.4b296ffa9O71cl&target=https%3A%2F%2Fv8.dev%2Ffeatures%2Fmodules" target="_blank" ref="nofollow noopener noreferrer">v8.dev/features/mo…</a></li><li><a href="https://link.juejin.cn?spm=a2c6h.13046898.publish-article.24.4b296ffa9O71cl&target=https%3A%2F%2Fblog.logrocket.com%2Fjavascript-reference-guide-js-modules%2F" target="_blank" ref="nofollow noopener noreferrer">blog.logrocket.com/javascript-…</a></li></ul></blockquote><div><br /></div>