aboutsummaryrefslogtreecommitdiff
path: root/files/zh-cn/web/javascript/guide/modules/index.html
blob: e3db44e9a0436c21ecb6ebf37a546d00ce878b6a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
---
title: JavaScript modules 模块
slug: Web/JavaScript/Guide/Modules
tags:
  - JavaScript
  - 导入
  - 导出
  - 指南
  - 模块
translation_of: Web/JavaScript/Guide/Modules
---
<div>{{jsSidebar("JavaScript Guide")}}{{Previous("Web/JavaScript/Guide/Meta_programming")}}</div>

<p>这篇指南会给你入门 JavaScript 模块的全部信息。</p>

<h2 id="模块化的背景">模块化的背景</h2>

<p>JavaScript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境(例如 <a href="/en-US/docs/Glossary/Node.js">Node.js</a>)。</p>

<p>因此,近年来,有必要开始考虑提供一种<u>将 JavaScript 程序拆分为可按需导入的单独模块</u>的机制。Node.js 已经提供这个能力很长时间了,还有很多的 JavaScript 库和框架 已经开始了模块的使用(例如, <a href="https://en.wikipedia.org/wiki/CommonJS">CommonJS</a> 和基于 <a href="https://github.com/amdjs/amdjs-api/blob/master/AMD.md">AMD</a> 的其他模块系统 如 <a href="https://requirejs.org/">RequireJS</a>, 以及最新的 <a href="https://webpack.github.io/">Webpack</a> 和 <a href="https://babeljs.io/">Babel</a>)。</p>

<p>好消息是,最新的浏览器开始原生支持模块功能了,这是本文要重点讲述的。这会是一个好事情 — 浏览器能够最优化加载模块,使它比使用库更有效率:使用库通常需要做额外的客户端处理。</p>

<h2 id="浏览器支持">浏览器支持</h2>

<p>使用JavaScript 模块依赖于<code>import</code><code>export</code>,浏览器兼容性如下(绿色方框中的数字对应相应平台上支持该功能的发布版本):</p>

<h3 id="import">import</h3>

<p>{{Compat("javascript.statements.import")}}</p>

<h3 id="export">export</h3>

<p>{{Compat("javascript.statements.export")}}</p>

<h2 id="介绍一个例子">介绍一个例子</h2>

<p>为了演示模块的使用,我们创建了一个 <a href="https://github.com/mdn/js-examples/tree/master/modules">simple set of examples</a> ,你可以在Github上找到。这个例子演示了一个简单的模块的集合用来在web页面上创建了一个{{htmlelement("canvas")}} 标签,在canvas上绘制 (并记录有关的信息) 不同形状。</p>

<p>这的确有点简单,但是保持足够简单能够清晰地演示模块。</p>

<div class="note">
<p><strong>备注:</strong>如果你想去下载这个例子在本地运行,你需要通过本地web 服务器去运行。</p>
</div>

<h2 id="基本的示例文件的结构">基本的示例文件的结构</h2>

<p>在我们的第一个例子 (see <a href="https://github.com/mdn/js-examples/tree/master/modules/basic-modules">basic-modules</a>) 文件结构如下:</p>

<pre>index.html
main.mjs
modules/
    canvas.mjs
    square.mjs</pre>

<div class="note">
<p><strong>备注:</strong>在这个指南的全部示例项目的文件结构是基本相同的; 需要熟悉上面的内容</p>
</div>

<p>modules 目录下的两个模块的描述如下:</p>

<ul>
 <li><code>canvas.mjs</code> — 包含与设置画布相关的功能:

  <ul>
   <li><code>create()</code> —在指定ID的包装器{{htmlelement("div")}}内创建指定<code>width</code> 和<code>height</code> 的画布,该ID本身附加在指定的父元素内。 返回包含画布的2D上下文和包装器ID的对象。</li>
   <li><code>createReportList()</code> — 创建一个附加在指定包装器元素内的无序列表,该列表可用于将报告数据输出到。 返回列表的ID。</li>
  </ul>
 </li>
 <li><code>square.mjs</code> — 包含:
  <ul>
   <li><code>name</code> — 包含字符串'square'的常量。</li>
   <li><code>draw()</code> — 在指定画布上绘制一个正方形,具有指定的大小,位置和颜色。 返回包含正方形大小,位置和颜色的对象。</li>
   <li><code>reportArea()</code> — 在给定长度的情况下,将正方形区域写入特定报告列表。</li>
   <li><code>reportPerimeter()</code> — 在给定长度的情况下,将正方形的周长写入特定的报告列表。</li>
  </ul>
 </li>
</ul>

<div class="note">
<p><strong>备注:</strong>在原生JavaScript模块中, 扩展名 <code>.mjs</code> 非常重要,因为使用 MIME-type 为<code>javascript/esm</code> 来导入文件(其他的JavaScript 兼容 MIME-type 像 <code>application/javascript</code> 也可以), 它避免了严格的 MIME 类型检查错误,像 "The server responded with a non-JavaScript MIME type". 除此之外,  <code>.mjs</code> 的扩展名很明了(比如这个就是一个模块,而不是一个传统 JavaScript文件),还能够和其他工具互相适用. 看这个 <a href="https://v8.dev/features/modules#mjs">Google's note for further details</a>.</p>
</div>

<h2 id=".mjs_与_.js"><code>.mjs</code> 与 <code>.js</code></h2>

<p>纵观此文,我们使用 <code>.js</code> 扩展名的模块文件,但在其它一些文章中,你可能会看到 <code>.mjs</code> 扩展名的使用。<a href="https://v8.dev/features/modules#mjs">V8推荐了这样的做法</a>,比如有下列理由:</p>

<ul>
 <li>比较清晰,这可以指出哪些文件是模块,哪些是常规的 JavaScript。</li>
 <li>这能保证你的模块可以被运行时环境和构建工具识别,比如  <a href="https://nodejs.org/api/esm.html#esm_enabling">Node.js</a> 和 <a href="https://babeljs.io/docs/en/options#sourcetype">Babel</a></li>
</ul>

<p>但是我们决定继续使用 <code>.js</code> 扩展名,未来可能会更改。为了使模块可以在浏览器中正常地工作,你需要确保你的服务器能够正常地处理 <code>Content-Type</code> 头,其应该包含 JavaScript 的MIME 类型  <code>text/javascript</code>。如果没有这么做,你可能会得到 一个严格 MIME 类型检查错误:“The server responded with a non-JavaScript MIME type (服务器返回了非 JavaScript MIME 类型)”,并且浏览器会拒绝执行相应的 JavaScript 代码。多数服务器可以正确地处理 <code>.js</code> 文件的类型,但是 <code>.mjs</code> 还不行。已经可以正常响应 <code>.mjs</code> 的服务器有 <a href="https://pages.github.com/">GitHub 页面</a> 和 Node.js 的 <code><a href="https://github.com/http-party/http-server#readme">http-server</a></code></p>

<p> 如果你已经在使用相应的环境了,那么一切正常。或者如果你还没有,但你知道你在做什么(比如你可以配置服务器以为 <code>.mjs</code> 设置正确的 <code>Content-Type</code>)。但如果你不能控制提供服务,或者用于公开文件发布的服务器,这可能会导致混乱。</p>

<p>为了学习和保证代码的可移植的目的,我们建议使用 <code>.js</code></p>

<p>如果你认为使用 <code>.mjs</code> 仅用于模块带来的清晰性非常重要,但不想引入上面描述的相应问题,你可以仅在开发过程中使用 <code>.mjs</code>,而在构建过程中将其转换为 <code>.js</code></p>

<p>另注意:</p>

<ul>
 <li>一些工具不支持 <code>.mjs</code>,比如 <a href="https://www.typescriptlang.org/">TypeScript</a></li>
 <li><code>&lt;script type="module"&gt;</code> 属性用于指示引入的模块,你会在下面看到。</li>
</ul>

<h2 id="导出模块的功能">导出模块的功能</h2>

<p>为了获得模块的功能要做的第一件事是把它们导出来。使用 <code><a href="/en-US/docs/Web/JavaScript/Reference/Statements/export">export</a></code> 语句来完成。</p>

<p>最简单的方法是把它(指上面的export语句)放到你想要导出的项前面,比如:</p>

<pre class="brush: js">export const name = 'square';

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}</pre>

<p>你能够导出函数,<code>var</code><code>let</code><code>const</code>, 和等会会看到的类。export要放在最外层;比如你不能够在函数内使用<code>export</code></p>

<p>一个更方便的方法导出所有你想要导出的模块的方法是在模块文件的末尾使用一个export 语句, 语句是用花括号括起来的用逗号分割的列表。比如:</p>

<pre class="brush: js">export { name, draw, reportArea, reportPerimeter };</pre>

<h2 id="导入功能到你的脚本">导入功能到你的脚本</h2>

<p>你想在模块外面使用一些功能,那你就需要导入他们才能使用。最简单的就像下面这样的:</p>

<pre>import { name, draw, reportArea, reportPerimeter } from '/js-examples/modules/basic-modules/modules/square.mjs';</pre>

<p>使用 <code><a href="/en-US/docs/Web/JavaScript/Reference/Statements/import">import</a></code> 语句,然后你被花括号包围的用逗号分隔的你想导入的功能列表,然后是关键字from,然后是模块文件的路径。模块文件的路径是相对于站点根目录的相对路径,对于我们的<code>basic-modules</code> 应该是<code> /js-examples/modules/basic-modules</code></p>

<p>当然,我们写的路径有一点不同---我们使用点语法意味 “当前路径”,跟随着包含我们想要找的文件的路径。这比每次都要写下整个相对路径要好得多,因为它更短,使得URL 可移植 ---如果在站点层中你把它移动到不同的路径下面仍然能够工作。(修订版 1889482)</p>

<p>那么看看例子吧:</p>

<pre>/js/examples/modules/basic-modules/modules/square.mjs</pre>

<p>变成了</p>

<pre>./modules/square.mjs</pre>

<p>你可以在<code><a href="https://github.com/mdn/js-examples/blob/master/modules/basic-modules/main.js">main.mjs</a></code>中看到这些。</p>

<div class="note">
<p><strong>备注:</strong>在一些模块系统中你可以忽略文件扩展名(比如<code>'/model/squre'</code> .这在原生JavaScript 模块系统中不工作。<del>此外,记住你需要包含最前面的正斜杠。</del> (修订版 1889482)</p>
</div>

<p>因为你导入了这些功能到你的脚本文件,你可以像定义在相同的文件中的一样去使用它。下面展示的是在 <code>main.mjs</code> 中的import 语句下面的内容。</p>

<pre class="brush: js">let myCanvas = create('myCanvas', document.body, 480, 320);
let reportList = createReportList(myCanvas.id);

let square1 = draw(myCanvas.ctx, 50, 50, 100, 'blue');
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);
</pre>

<h2 id="应用模块到你的HTML">应用模块到你的HTML</h2>

<p>现在我们只需要将main.mjs模块应用到我们的HTML页面。 这与我们将常规脚本应用于页面的方式非常相似,但有一些显着的差异。</p>

<p>首先,你需要把 <code>type="module"</code> 放到 {{htmlelement("script")}} 标签中, 来声明这个脚本是一个模块:</p>

<pre>&lt;script type="module" src="main.mjs"&gt;&lt;/script&gt;</pre>

<p>你导入模块功能的脚本基本是作为顶级模块。 如果省略它,Firefox就会给出错误“SyntaxError: import declarations may only appear at top level of a module。</p>

<p>你只能在模块内部使用 <code>import</code><code>export</code> 语句 ;不是普通脚本文件。</p>

<div class="note">
<p><strong>备注:</strong>您还可以将模块导入内部脚本,只要包含 <code>type="module"</code>,例如 <code>&lt;script type="module"&gt; //include script here &lt;/script&gt;</code>.</p>
</div>

<h2 id="其他模块与标准脚本的不同">其他模块与标准脚本的不同</h2>

<ul>
 <li>你需要注意本地测试 —  如果你通过本地加载Html 文件 (比如一个 <code>file://</code> 路径的文件), 你将会遇到 CORS 错误,因为JavaScript 模块安全性需要。你需要通过一个服务器来测试。</li>
 <li>另请注意,您可能会从模块内部定义的脚本部分获得不同的行为,而不是标准脚本。 这是因为模块自动使用严格模式。</li>
 <li>加载一个模块脚本时不需要使用 <code>defer</code> 属性 (see <a href="/en-US/docs/Web/HTML/Element/script#Attributes"><code>&lt;script&gt;</code> attributes</a>) 模块会自动延迟加载。</li>
 <li>最后一个但不是不重要,你需要明白模块功能导入到单独的脚本文件的范围 — 他们无法在全局获得。因此,你只能在导入这些功能的脚本文件中使用他们,你也无法通过 JavaScript console 中获取到他们,比如,在DevTools 中你仍然能够获取到语法错误,但是你可能无法像你想的那样使用一些debug 技术 </li>
</ul>

<h2 id="默认导出_versus_命名导出">默认导出 versus 命名导出</h2>

<p>到目前为止我们导出的功能都是由<strong>named exports</strong> 组成— 每个项目(无论是函数,常量等)在导出时都由其名称引用,并且该名称也用于在导入时引用它。</p>

<p>还有一种导出类型叫做 <strong>default export</strong> —这样可以很容易地使模块提供默认功能,并且还可以帮助JavaScript模块与现有的CommonJS和AMD模块系统进行互操作(正如<a href="https://hacks.mozilla.org/2015/08/es6-in-depth-modules/">ES6 In Depth: Modules</a> by Jason Orendorff的模块中所解释的那样;搜索“默认导出”“)。</p>

<p>看个例子来解释它如何工作。在我们的基本模块<code>square.mjs</code>中,您可以找到一个名为<code>randomSquare()</code>的函数,它创建一个具有随机颜色,大小和位置的正方形。我们想作为默认导出,所以在文件的底部我们这样写 :</p>

<pre class="brush: js">export default randomSquare;</pre>

<p>注意,不要大括号。</p>

<p>我们可以把 <code>export default</code> 放到函数前面,定义它为一个匿名函数,像这样:</p>

<pre class="brush: js">export default function(ctx) {
  ...
}</pre>

<p>在我们的<code>main.mjs</code> 文件中,我们使用以下行导入默认函数:</p>

<pre class="brush: js">import randomSquare from './modules/square.mjs';</pre>

<p>同样,没有大括号,因为每个模块只允许有一个默认导出, 我们知道 <code>randomSquare</code> 就是需要的那个。上面的那一行相当于下面的缩写:</p>

<pre class="brush: js">import {default as randomSquare} from './modules/square.mjs';</pre>

<div class="note">
<p><strong>备注:</strong>重命名导出项的as语法在下面的{{anch("Renaming imports and exports")}}部分中进行了说明。</p>
</div>

<h2 id="避免命名冲突">避免命名冲突</h2>

<p>到目前为止,我们的canvas 图形绘制模块看起来工作的很好。但是如果我们添加一个绘制其他形状的比如圆形或者矩形的模块会发生什么?这些形状可能会有相关的函数比如 <code>draw()</code><code>reportArea()</code>,等等;如果我们用相同的名字导入不同的函数到顶级模块文件中,我们会收到冲突和错误。</p>

<p>幸运的是,有很多方法来避免。我们将会在下一个节看到。</p>

<h2 id="重命名导出与导入">重命名导出与导入</h2>

<p>在你的 <code>import</code> 和 <code>export</code> 语句的大括号中,可以使用 <code>as</code> 关键字跟一个新的名字,来改变你在顶级模块中将要使用的功能的标识名字。因此,例如,以下两者都会做同样的工作,尽管方式略有不同:</p>

<pre class="brush: js">// inside module.mjs
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};

// inside main.mjs
import { newFunctionName, anotherNewFunctionName } from '/modules/module.mjs';</pre>

<pre class="brush: js">// inside module.mjs
export { function1, function2 };

// inside main.mjs
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from '/modules/module.mjs';</pre>

<p>让我们看一个真实的例子。在我们的<a href="https://github.com/mdn/js-examples/tree/master/modules/renaming">重命名</a>目录中,您将看到与上一个示例中相同的模块系统,除了我们添加了<code>circle.mjs</code><code>triangle.mjs</code>模块以绘制和报告圆和三角形。</p>

<p>在每个模块中,我们都有<code>export</code> 相同名称的功能,因此每个模块底部都有相同的导出语句:</p>

<pre class="brush: js">export { name, draw, reportArea, reportPerimeter };</pre>

<p>将它们导入<code>main.mjs</code>时,如果我们尝试使用</p>

<pre class="brush: js">import { name, draw, reportArea, reportPerimeter } from './modules/square.mjs';
import { name, draw, reportArea, reportPerimeter } from './modules/circle.mjs';
import { name, draw, reportArea, reportPerimeter } from './modules/triangle.mjs';</pre>

<p>浏览器会抛出一个错误,例如“SyntaxError: redeclaration of import name”(Firefox)。</p>

<p>相反,我们需要重命名导入,使它们是唯一的:</p>

<pre class="brush: js">import { name as squareName,
         draw as drawSquare,
         reportArea as reportSquareArea,
         reportPerimeter as reportSquarePerimeter } from './modules/square.mjs';

import { name as circleName,
         draw as drawCircle,
         reportArea as reportCircleArea,
         reportPerimeter as reportCirclePerimeter } from './modules/circle.mjs';

import { name as triangleName,
        draw as drawTriangle,
        reportArea as reportTriangleArea,
        reportPerimeter as reportTrianglePerimeter } from './modules/triangle.mjs';</pre>

<p>请注意,您可以在模块文件中解决问题,例如</p>

<pre class="brush: js">// in square.mjs
export { name as squareName,
         draw as drawSquare,
         reportArea as reportSquareArea,
         reportPerimeter as reportSquarePerimeter };</pre>

<pre class="brush: js">// in main.mjs
import { squareName, drawSquare, reportSquareArea, reportSquarePerimeter } from '/js-examples/modules/renaming/modules/square.mjs';</pre>

<p>它也会起作用。 你使用什么样的风格取决于你,但是单独保留模块代码并在导入中进行更改可能更有意义。 当您从没有任何控制权的第三方模块导入时,这尤其有意义。</p>

<h2 id="创建模块对象">创建模块对象</h2>

<p>上面的方法工作的挺好,但是有一点点混乱、亢长。一个更好的解决方是,导入每一个模块功能到一个模块功能对象上。可以使用以下语法形式:</p>

<pre class="brush: js">import * as Module from '/modules/module.mjs';</pre>

<p>这将获取<code>module.mjs</code>中所有可用的导出,并使它们可以作为对象模块的成员使用,从而有效地为其提供自己的命名空间。 例如:</p>

<pre class="brush: js">Module.function1()
Module.function2()
etc.</pre>

<p>再次,让我们看一个真实的例子。如果您转到我们的<a href="https://github.com/mdn/js-examples/tree/master/modules/module-objects">module-objects</a>目录,您将再次看到相同的示例,但利用上述的新语法进行重写。在模块中,导出都是以下简单形式:</p>

<pre class="brush: js">export { name, draw, reportArea, reportPerimeter };</pre>

<p>另一方面,导入看起来像这样:</p>

<pre class="brush: js">import * as Canvas from './modules/canvas.mjs';

import * as Square from '/./modules/square.mjs';
import * as Circle from './modules/circle.mjs';
import * as Triangle from './modules/triangle.mjs';</pre>

<p>在每种情况下,您现在可以访问指定对象名称下面的模块导入</p>

<pre class="brush: js">let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);</pre>

<p>因此,您现在可以像以前一样编写代码(只要您在需要时包含对象名称),并且导入更加整洁。</p>

<h2 id="模块与类(class)">模块与类(class)</h2>

<p>正如我们之前提到的那样,您还可以导出和导入类; 这是避免代码冲突的另一种选择,如果您已经以面向对象的方式编写了模块代码,那么它尤其有用。</p>

<p>您可以在我们的<a href="https://github.com/mdn/js-examples/tree/master/modules/classes">classes</a> 目录中看到使用ES类重写的形状绘制模块的示例。 例如,<code><a href="https://github.com/mdn/js-examples/blob/master/modules/classes/modules/square.js">square.mjs</a></code> 文件现在包含单个类中的所有功能:</p>

<pre class="brush: js">class Square {
  constructor(ctx, listId, length, x, y, color) {
    ...
  }

  draw() {
    ...
  }

  ...
}</pre>

<p>然后我们导出:</p>

<pre class="brush: js">export { Square };</pre>

<p><code><a href="https://github.com/mdn/js-examples/blob/master/modules/classes/main.js">main.mjs</a></code>中,我们像这样导入它:</p>

<pre class="brush: js">import { Square } from './modules/square.mjs';</pre>

<p>然后使用该类绘制我们的方块:</p>

<pre class="brush: js">let square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
square1.draw();
square1.reportArea();
square1.reportPerimeter();</pre>

<h2 id="合并模块">合并模块</h2>

<p>有时你会想要将模块聚合在一起。 您可能有多个级别的依赖项,您希望简化事物,将多个子模块组合到一个父模块中。 这可以使用父模块中以下表单的导出语法:</p>

<pre class="brush: js">export * from 'x.mjs'
export { name } from 'x.mjs'</pre>

<div class="note">
<p><strong>备注:</strong>这实际上是导入后跟导出的简写,即“我导入模块<code>x.mjs</code>,然后重新导出部分或全部导出”。</p>
</div>

<p>有关示例,请参阅我们的<a href="https://github.com/mdn/js-examples/tree/master/modules/module-aggregation">module-aggregation</a>。 在这个例子中(基于我们之前的类示例),我们有一个名为<code>shapes.mjs</code>的额外模块,它将<code>circle.mjs</code><code>square.mjs</code><code>riangle.mjs</code>中的所有功能聚合在一起。 我们还将子模块移动到名为shapes的modules目录中的子目录中。 所以模块结构现在是这样的:</p>

<pre>modules/
  canvas.mjs
  shapes.mjs
  shapes/
    circle.mjs
    square.mjs
    triangle.mjs</pre>

<p>在每个子模块中,输出具有相同的形式,例如,</p>

<pre class="brush: js">export { Square };</pre>

<p>接下来是聚合部分。 在<code><a href="https://github.com/mdn/js-examples/blob/master/modules/module-aggregation/modules/shapes.js">shapes.mjs</a></code>里面,我们包括以下几行:</p>

<pre class="brush: js">export { Square } from '/js-examples/modules/module-aggregation/modules/shapes/square.mjs';
export { Triangle } from '/js-examples/modules/module-aggregation/modules/shapes/triangle.mjs';
export { Circle } from '/js-examples/modules/module-aggregation/modules/shapes/circle.mjs';</pre>

<p>它们从各个子模块中获取导出,并有效地从<code>shapes.mjs</code>模块中获取它们。</p>

<div class="note">
<p><strong>备注:</strong>即使<code>shapes.mjs</code>文件位于modules目录中,我们仍然需要相对于模块根目录编写这些URL,因此需要<code>/modules/</code>。 这是使用JavaScript模块时混淆的常见原因。</p>
</div>

<div class="note">
<p><strong>备注:</strong><code>shapes.mjs</code>中引用的导出基本上通过文件重定向,并且实际上并不存在,因此您将无法在同一文件中编写任何有用的相关代码。</p>
</div>

<p>所以现在在<code>main.mjs</code> 文件中,我们可以通过替换来访问所有三个模块类</p>

<pre class="brush: js">import { Square } from './modules/square.mjs';
import { Circle } from './modules/circle.mjs';
import { Triangle } from './modules/triangle.mjs';</pre>

<p>使用以下单行:</p>

<pre class="brush: js">import { Square, Circle, Triangle } from './modules/shapes.mjs';</pre>

<h2 id="动态加载模块">动态加载模块</h2>

<p>浏览器中可用的JavaScript模块功能的最新部分是动态模块加载。 这允许您仅在需要时动态加载模块,而不必预先加载所有模块。 这有一些明显的性能优势; 让我们继续阅读,看看它是如何工作的。</p>

<p>这个新功能允许您将<code>import()</code>作为函数调用,将其作为参数传递给模块的路径。 它返回一个 <a href="/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise">promise</a>,它用一个模块对象来实现(参见{{anch("Creating a module object")}}),让你可以访问该对象的导出,例如</p>

<pre class="brush: js">import('/modules/myModule.mjs')
  .then((module) =&gt; {
    // Do something with the module.
  });</pre>

<p>我们来看一个例子。 在<a href="https://github.com/mdn/js-examples/tree/master/modules/dynamic-module-imports">dynamic-module-imports</a> 目录中,我们有另一个基于类示例的示例。 但是这次我们在示例加载时没有在画布上绘制任何东西。 相反,我们包括三个按钮 - “圆形”,“方形”和“三角形” - 按下时,动态加载所需的模块,然后使用它来绘制相关的形状。</p>

<p>在这个例子中,我们只对<a href="https://github.com/mdn/js-examples/blob/master/modules/dynamic-module-imports/index.html">index.html</a><a href="https://github.com/mdn/js-examples/blob/master/modules/dynamic-module-imports/main.js">main.mjs</a>文件进行了更改 - 模块导出保持与以前相同。</p>

<p><code>main.mjs</code>中,我们使用<code><a href="/en-US/docs/Web/API/Document/querySelector">document.querySelector()</a></code>调用获取了对每个按钮的引用,例如:</p>

<pre class="brush: js">let squareBtn = document.querySelector('.square');</pre>

<p>然后,我们为每个按钮附加一个事件监听器,以便在按下时,相关模块被动态加载并用于绘制形状:</p>

<pre class="brush: js">squareBtn.addEventListener('click', () =&gt; {
  import('/js-examples/modules/dynamic-module-imports/modules/square.mjs').then((Module) =&gt; {
    let square1 = new Module.Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, 'blue');
    square1.draw();
    square1.reportArea();
    square1.reportPerimeter();
  })
});</pre>

<p>请注意,由于promise履行会返回一个模块对象,因此该类成为对象的子特征,因此我们现在需要使用 <code>Module</code>访问构造函数。 在它之前,例如 <code>Module.Square( ... )</code></p>

<h2 id="故障排除">故障排除</h2>

<p>如果为了你的模块有问题,这里有一些提示有可能帮助到你。如果你发现更多的内容欢迎添加进来!</p>

<ul>
 <li>在前面已经提到了,在这里再重申一次: <code>.mjs</code> 后缀的文件需要以 MIME-type 为 <code>javascript/esm</code> 来加载(或者其他的 JavaScript 兼容的 MIME-type ,比如 <code>application/javascript</code>), 否则,你会一个严格的 MIME 类型检查错误,像是这样的 "The server responded with a non-JavaScript MIME type".</li>
 <li>如果你尝试用本地文件加载 HTML 文件 (i.e. with a <code>file://</code> URL), 由于 JavaScript 模块的安全性要求,你会遇到 CORS 错误。你需要通过服务器来做你的测试。GitHub pages is ideal as it also serves <code>.mjs</code> files with the correct MIME type.</li>
 <li>因为<code>.mjs</code> 是一个相当新的文件后缀, 一些操作系统可能无法识别,或者尝试把它替换成别的。比如,我们发现 macOS 悄悄地该我们的 <code>.mjs</code> 后缀的文件后面添加上 <code>.js</code>  然后自动隐藏这个后缀。所以我们的文件实际上都是 <code>x.mjs.js</code>. 当我们关闭自动隐藏文件后缀名,让它去接受认可 <code>.mjs</code>。问题解决。</li>
</ul>

<h2 id="参见">参见</h2>

<ul>
 <li><a href="https://developers.google.com/web/fundamentals/primers/modules#mjs">Using JavaScript modules on the web</a>, by Addy Osmani and Mathias Bynens</li>
 <li><a href="https://hacks.mozilla.org/2018/03/es-modules-a-cartoon-deep-dive/">ES modules: A cartoon deep-dive</a>, Hacks blog post by Lin Clark</li>
 <li><a href="https://hacks.mozilla.org/2015/08/es6-in-depth-modules/">ES6 in Depth: Modules</a>, Hacks blog post by Jason Orendorff</li>
 <li>Axel Rauschmayer's book <a href="http://exploringjs.com/es6/ch_modules.html">Exploring JS: Modules</a></li>
</ul>

<p>{{Previous("Web/JavaScript/Guide/Meta_programming")}}</p>