- Notifications
You must be signed in to change notification settings - Fork 8.2k
/
Copy pathindex.md
1015 lines (728 loc) · 66 KB
/
index.md
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
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: JavaScript モジュール
slug: Web/JavaScript/Guide/Modules
l10n:
sourceCommit: 5b20f5f4265f988f80f513db0e4b35c7e0cd70dc
---
{{jsSidebar("JavaScript Guide")}}{{Previous("Web/JavaScript/Guide/Meta_programming")}}
本章では、JavaScript のモジュールを使い始めるために必要なことすべてを紹介します。
## モジュールの背景
JavaScript のプログラムはとても小さいものから始まりました。初期の用途は、必要に応じてウェブページにちょっとした対話的な機能を追加する独立したスクリプト処理がほとんどであったため、大きなスクリプトは通常必要ありませんでした。そして何年かが過ぎ、今や大量の JavaScript を持つ完全なアプリケーションをブラウザーで実行することはもちろん、JavaScript を他のコンテキスト(例えば [Node.js](/ja/docs/Glossary/Node.js))で使うこともあります。
複雑なプロジェクトでは、必要に応じて JavaScript プログラムを別個のモジュールに分割し、インポートできる仕組みが必要です。 Node.js は長年この機能を提供しており、モジュールの利用を可能にする JavaScript ライブラリーやフレームワークも数多くあります(例えば、他の [CommonJS](https://ja.wikipedia.org/wiki/CommonJS) や、[AMD](https://github.com/amdjs/amdjs-api/blob/master/AMD.md) ベースのモジュールシステムである [RequireJS](https://requirejs.org/)、 [webpack](https://webpack.js.org/) や [Babel](https://babeljs.io/))。
現行のブラウザーはすべて、トランスパイルを必要とせずにモジュール機能にネイティブで対応しています。これは良いことであるに違いありません。ブラウザーはモジュールの読み込みを最適化することができ、ライブラリーを使用してクライアント側で余分な処理や余分なラウンドトリップを行うよりも効率的です。しかし、 webpack のようなバンドラーが不要になるわけではありません。バンドラーは、コードを合理的なサイズの塊に分割する作業に依然として優れており、また、ミニファイ、デッドコードの排除、ツリーシェイクなどの最適化も可能です。
## 例の紹介
モジュールの使い方を紹介するために、GitHub 上に[一連の例](https://github.com/mdn/js-examples/tree/main/module-examples)を作りました。これらは、ウェブページに [`<canvas>`](/ja/docs/Web/HTML/Reference/Elements/canvas) 要素を追加し、そのキャンバス上にいくつかの異なる図形(と、それに関するレポート)を描画するモジュールの例です。
このような機能はあまり役に立ちませんが、モジュールの説明が明確になるように意図的に単純にしています。
> [!NOTE]
> 使用例をダウンロードしてローカル実行する場合、ローカルのウェブサーバー上で実行する必要があります。
## 基本的な構造の例
最初の例 ([basic-modules](https://github.com/mdn/js-examples/tree/main/module-examples/basic-modules) を参照) は、次のようなファイル構造になっています。
```plain
index.html
main.js
modules/
canvas.js
square.js
```
> [!NOTE]
> このガイドの使用例のファイル構造は、全て基本的に同一ですので、上記のファイル構造をよく見ることになるでしょう。
modules ディレクトリーには、次の 2 つのモジュールがあります。
-`canvas.js` — キャンバスの設定に関する次の関数を持ちます。
-`create()` — 指定された `width` と `height` を持つキャンバスを、指定された ID を持つラッパー [`<div>`](/ja/docs/Web/HTML/Reference/Elements/div) の中に作成し、そのラッパー div 自体を指定された親要素の中に追加します。返値は、キャンバスの 2D コンテキストとラッパーの ID を持つオブジェクトです。
-`createReportList()` — 順序なしリストを指定されたラッパー要素の中に作成し、これをレポートデータを出力するために使うことができます。返値は、リストの ID です。
-`square.js` — 次のものを持ちます。
-`name` —文字列 'square' を内容とする定数です。
-`draw()` — 正方形を、指定されたキャンバス上に、指定された辺の長さ、位置、色を使って描画します。返値は、正方形の辺の長さ、位置、色を持つオブジェクトです。
-`reportArea()` — 指定された辺の長さを持つ正方形の面積を、指定されたレポート用のリストに書き出します。
-`reportPerimeter()` — 指定された辺の長さを持つ正方形の周囲の長さを、指定されたレポート用のリストに書き出します。
### 余談 — .mjs と .js
この記事ではモジュールファイルに `.js` の拡張子を使用していますが、他の記事では `.mjs` という拡張子が使用されているのを目にすることがあるかもしれません。例えば、[V8 のドキュメント](https://v8.dev/features/modules#mjs)ではこれを推奨しています。理由は以下の通りです。
- どのファイルがモジュールで、どのファイルが通常の JavaScript であるかを明確にすることができます。
- これにより、[Node.js](https://nodejs.org/api/esm.html#esm_enabling) のようなランタイムや [Babel](https://babeljs.io/docs/options#sourcetype) のようなビルドツールで、モジュールファイルがモジュールとして解析されるようになります。
しかし、少なくとも今のところは `.js` を使い続けることにしました。ブラウザーでモジュールを正しく動作させるためには、サーバーが `Content-Type` ヘッダーで JavaScript の MIME タイプ、例えば `text/javascript` などを含めて提供していることを確認する必要があります。そうしないと、"The server responded with a non-JavaScript MIME type" のような厳格な MIME タイプチェックエラーが表示され、ブラウザーは JavaScript を実行しません。ほとんどのサーバーでは、`.js` ファイルにはすでに正しい MIME タイプが設定されていますが、`.mjs` ファイルにはまだ設定されていません。すでに `.mjs` ファイルを正しく提供しているサーバーには、[GitHub Pages](https://pages.github.com/) や Node.js の [`http-server`](https://github.com/http-party/http-server#readme) などがあります。
これは、すでにそのような環境を使用している場合や、今はまだ使用していないが、何をしているか知っていてアクセスできる場合には問題ありません(つまり、`.mjs` ファイルに正しい [`Content-Type`](/ja/docs/Web/HTTP/Reference/Headers/Content-Type) を設定するようにサーバーを設定することができます)。しかし、あなたがファイルを提供しているサーバーを制御できない場合には、混乱を引き起こす可能性があります。
この記事では学習と移植性を考慮して、`.js` を使用することにしました。
通常の JavaScript ファイルに `.js` を使用するのと比較して、モジュールに `.mjs` を使用することの明確さを本当に重視しているが、上記の問題に直面したくない場合は、開発中に `.mjs` を使用し、ビルドステップで `.js` に変換することをおすすめします。
また、次の点にも注意してください。
- 一部のツールは `.mjs` に対応していないことがあります。
- モジュールが指し示されているとき、それを示すために `<script type="module">` 属性を使用してください。
## モジュール機能のエクスポート
モジュールが持つ機能にアクセスするために最初に必要なことは、そのような機能をエクスポートすることです。これは {{jsxref("Statements/export", "export")}} 文を使って行います。
最も簡単な使い方は、モジュール外部に公開したい項目の前に `export` をつけることです。
```js
exportconstname="square";
exportfunctiondraw(ctx, length, x, y, color) {
ctx.fillStyle= color;
ctx.fillRect(x, y, length, length);
return { length, x, y, color };
}
```
エクスポートできるものは、関数、`var`、`let`、`const`、および後述するクラスです。これらは最上位の階層にある必要があります。例えば、関数内で `export` を使うことはできません。
エクスポートしたい全ての項目をエクスポートするより便利な方法は、モジュールファイルの末尾に単一の export 文を追加し、その後にエクスポートしたい機能のカンマ区切りリストを中かっこで囲んで続けることです。例えば次のようにします。
```js
export { name, draw, reportArea, reportPerimeter };
```
## スクリプトへの機能のインポート
モジュールから何らかの機能をエクスポートした後は、それらを使えるようにするためにスクリプトにインポートする必要があります。その最も単純な方法は次のとおりです。
```js
import { name, draw, reportArea, reportPerimeter } from"./modules/square.js";
```
{{jsxref("Statements/import", "import")}} 文の後ろに、中かっこで囲まれたインポートしたい機能のカンマ区切りリストを続け、その後ろに `from` キーワードと、モジュール指定子を続けます。
モジュール指定子は、JavaScript 環境がモジュールファイルへのパスを解決できる文字列を提供します。
ブラウザーでは、これはサイトルートからの相対パスとなり、`basic-modules` の例では `/js-examples/module-examples/basic-modules` となります。
しかし、ここでは代わりにドット(`.`)構文を使用して、「現在の場所」を意味しており、その後に探そうとしているファイルへの相対パスを記述しています。相対パスの方が短いし、URL の移植性も高いので、この例はサイト階層の別の場所に移しても作業することができますから、絶対パス全体を毎回書き出すよりもずっとよいでしょう。
そのため、次のようなパスは、
```bash
/js-examples/module-examples/basic-modules/modules/square.js
```
次のように書くことができます。
```bash
./modules/square.js
```
このような書き方の動作している例は [`main.js`](https://github.com/mdn/js-examples/blob/main/module-examples/basic-modules/main.js) にあります。
> [!NOTE]
> モジュールシステムの中には、相対パスでも絶対パスでもなく、ファイル拡張子もない `modules/square` のようなモジュール指定を使用するものがあります。
> このような指定子は、最初に[インポートマップ](#importing_modules_using_import_maps)を定義しておけば、ブラウザー環境でも使用できます。
スクリプトへ機能をインポートすると、同じファイル内で定義されているのと同じように使うことができます。次のコードは、`main.js` でインポートに続く部分です。
```js
constmyCanvas=create("myCanvas", document.body, 480, 320);
constreportList=createReportList(myCanvas.id);
constsquare1=draw(myCanvas.ctx, 50, 50, 100, "blue");
reportArea(square1.length, reportList);
reportPerimeter(square1.length, reportList);
```
> [!NOTE]
> インポートされた値は、エクスポートされた機能の読み取り専用ビューとなります。`const` 変数と同様に、インポートされた変数を再代入することはできませんが、オブジェクト値のプロパティを変更することは可能です。値を再代入することができるのは、その値をエクスポートしているモジュールだけです。例として、[`import` のリファレンス](/ja/docs/Web/JavaScript/Reference/Statements/import#imported_values_can_only_be_modified_by_the_exporter) を参照してください。
## インポートマップを使用したモジュールのインポート
ブラウザーがモジュールをインポートするのに、絶対 URL か、文書のベース URL を使用して解決される相対 URL であるモジュール指定子を使用する方法は、前述したとおりです。
```js
import { nameassquareName, draw } from"./shapes/square.js";
import { nameascircleName } from"https://example.com/shapes/circle.js";
```
[インポートマップ](/ja/docs/Web/HTML/Reference/Elements/script/type/importmap)により、モジュールをインポートするときに、モジュール指定子でほぼ全ての好きなテキストを代わりに指定することができます。このマップは、モジュールの URL が解決されたときにテキストを置き換える対応する値を提供します。
例えば、下記のインポートマップの `imports` キーは、「モジュール指定マップ」JSON オブジェクトを定義し、プロパティ名をモジュール指定子として使用でき、ブラウザーがモジュール URL を解決する際に対応する値が代入されます。
値は、絶対 URL または相対 URL でなければなりません。
相対 URL は、インポートマップを含む文書の[ベース URL](/ja/docs/Web/HTML/Reference/Elements/base) を使用して絶対 URL アドレスに解決されます。
```html
<scripttype="importmap">
{
"imports": {
"shapes":"./shapes/square.js",
"shapes/square":"./modules/shapes/square.js",
"https://example.com/shapes/square.js":"./shapes/square.js",
"https://example.com/shapes/":"/shapes/square/",
"../shapes/square":"./shapes/square.js"
}
}
</script>
```
インポートマップは `<script>` 要素の中の [JSON オブジェクト](/ja/docs/Web/HTML/Reference/Elements/script/type/importmap#json_のインポートマップ表現) で、 `type` 属性を [`importmap`](/ja/docs/Web/HTML/Reference/Elements/script/type/importmap) に設定して定義することができます。
文書内に置けるインポートマップは 1 つだけで、静的インポートと動的インポートの両方でどのモジュールが読み込まれるかを解決するために使用できるので、モジュールをインポートする `<script>` 要素の前に宣言する必要があります。
インポートマップは文書内の特定の要素にのみ適用されることに注意してください。仕様では、ワーカーやワークレットのコンテキストでインポートマップを適用する方法についてはカバーされていません。 <!-- https://github.com/WICG/import-maps/issues/2 -->
このマップで、上記のプロパティ名をモジュール指定子として使用することができるようになりました。
モジュール指定子キーに末尾のスラッシュがない場合は、モジュール指定子キー全体が照合されて置換されます。
例を説明すると、下記はモジュール名と一致し、URL を別のパスに再マップしています。
```js
// Bare module names as module specifiers
import { nameassquareNameOne } from"shapes";
import { nameassquareNameTwo } from"shapes/square";
// Remap a URL to another URL
import { nameassquareNameThree } from"https://example.com/shapes/square.js";
```
モジュール指定子が末尾にスラッシュがある場合、値が同様にスラッシュを持つ必要があり、キーは「パス接頭辞」として照合されます。
これにより、URL の全クラスを再マッピングすることができます。
```js
// Remap a URL as a prefix ( https://example.com/shapes/)
import { nameassquareNameFour } from"https://example.com/shapes/moduleshapes/square.js";
```
インポートマップ内の複数のキーがモジュール指定子を有効に一致することがあります。
例えば、`shapes/circle/` というモジュール指定子は、`shapes/` と `shapes/circle/` というモジュール指定子キーと一致する可能性があります。
この場合、ブラウザーは最も具体的な(最も長い)モジュール指定キーに一致するものを選択します。
インポートマップは、(Node.js のように)素のモジュール名を使用してモジュールをインポートすることができ、ファイル拡張子の有無にかかわらず、パッケージからのインポートをシミュレートすることも可能です。
上記では示していませんが、モジュールをインポートするスクリプトのパスに基づいて、特定のバージョンのライブラリーをインポートすることもできます。
一般的に、これらは開発者がより人間に優しいインポートコードを書くことを可能にし、サイトで使用されるモジュールの異なるバージョンと依存関係を管理することを容易にします。
これにより、ブラウザーとサーバーの両方で同じ JavaScript ライブラリーを使用するために必要な労力を縮小することができます。
以下の節では、上記で説明した様々な機能について、さらに詳しく説明します。
### 機能検出
インポートマップに対応しているかどうかは、[`HTMLScriptElement.supports()`](/ja/docs/Web/API/HTMLScriptElement/supports_static) 静的メソッドを使用してチェックすることができます(これ自体は広く対応しています)。
```js
if (HTMLScriptElement.supports?.("importmap")) {
console.log("Browser supports import maps.");
}
```
### モジュールの素の名前でのインポート
Node.js のような一部の JavaScript 環境では、モジュール指定子に素の名前を使用することができます。
これは、環境がモジュール名をファイルシステム内の標準的な場所に解決することができるため、動作します。
例えば、 "square" モジュールをインポートするために、以下の構文を使用することができます。
```js
import { name, draw, reportArea, reportPerimeter } from"square";
```
ブラウザーで素の名前を使用するには、インポートマップが必要です。これは、ブラウザーがモジュール指定子を URL に解決するために必要な情報を提供します(JavaScript は、モジュールの場所に解決できないモジュール指定子をインポートしようとすると `TypeError` を発生します)。
下記は `square` というモジュール指定子のキーを定義したマップですが、この場合、相対アドレスの値に割り当てられました。
```html
<script type="importmap">
{
"imports": {
"square":"./shapes/square.js"
}
}
</script>
```
このマップにより、モジュールをインポートするときに素の名前を使用することができるようになりました。
```js
import { nameassquareName, draw } from"square";
```
### モジュールのパスの再マッピング
モジュール指定子マップの項目で、指定子キーとその関連値に末尾のフォワードスラッシュ (`/`) がある場合、パス接頭辞として使用することができます。
これにより、インポート URL の集合全体を、ある場所から別の場所に再マッピングすることができます。
また、Node の環境で見られるような「パッケージとモジュール」の作業をエミュレートするために使用することもできます。
> [!NOTE]
> 末尾の `/` は、モジュール指定子キーがモジュール指定子の一部として指定することができることを示します。
> これが存在しない場合、ブラウザーはモジュール指定子キー全体にのみ一致します(置換します)。
#### モジュールのパッケージ
以下の JSON インポートマップ定義は、`lodash` を素の名前として、モジュール指定辞 `lodash/` をパス `/node_modules/lodash-es/` (文書のベース URL に解決)に割り当てたものです。
```json
{
"imports": {
"lodash":"/node_modules/lodash-es/lodash.js",
"lodash/":"/node_modules/lodash-es/"
}
}
```
このマッピングを使用すると、素の名前を使用する「パッケージ」全体と、(パスマッピングを使用する)その中のモジュールの両方をインポートすることができます。
```js
import_from"lodash";
importfpfrom"lodash/fp.js";
```
上記の `fp` を `.js` というファイル拡張子なしでインポートすることは可能ですが、パスを使用するのではなく、`lodash/fp` というように、そのファイルに対して素のモジュール指定子キーを作成する必要があります。
これは、1 つのモジュールだけなら妥当かもしれませんが、多くのモジュールをインポートしたい場合には、拡大縮小することになります。
#### 一般的な URL 再マッピング
モジュール指定キーはパスである必要はなく、絶対 URL(または `./`, `../`, `/` のような URL ライクな相対パス)であってもかまいません。
これは、リソースへの絶対パスを持つモジュールを自分自身でローカルリソースと再マッピングしたい場合に有用な場合があります。
```json
{
"imports": {
"https://www.unpkg.com/moment/":"/node_modules/moment/"
}
}
```
### バージョン管理のためのスコープ付きモジュール
Node のような環境では、モジュールとその依存関係を管理するために npm のようなパッケージマネージャーを使用します。
パッケージマネージャーは、各モジュールが他のモジュールやその依存関係から確実に区切られるようにします。
その結果、複雑なアプリケーションでは、モジュールグラフの異なる部分に複数の異なるバージョンで同じモジュールを複数回記載することができますが、ユーザーはこの複雑さについて考える必要はありません。
> [!NOTE]
> 相対パスを使用してバージョン管理を行うこともできますが、この方法は他にも、自分のプロジェクトに特定の構造を強制し、素のモジュール名を使用することができないなどの点で劣ります。
インポートマップも同様に、アプリケーションに複数のバージョンの依存関係を保有し、同じモジュール指定子を使用してそれらを参照することができます。
これを実装するために `scopes` キーを使用します。このキーでは、インポートを実行するスクリプトのパスに応じて使用されるモジュール指定子マップを提供することができます。
下記の例では、これを実演しています。
```json
{
"imports": {
"cool-module":"/node_modules/cool-module/index.js"
},
"scopes": {
"/node_modules/dependency/": {
"cool-module":"/node_modules/some/other/location/cool-module/index.js"
}
}
}
```
このマッピングでは、 `/node_modules/dependency/` を格納した URL のスクリプトが `cool-module` をインポートしている場合、 `/node_modules/some/other/location/cool-module/index.js` にあるバージョンが使用されます。
`imports` のマップは、スコープされたマップに一致するスコープがない場合、または一致するスコープに一致する指定するものが格納されていない場合に、予備として使用されます。例えば、`cool-module` がスコープパスに一致しないスクリプトからインポートされた場合、代わりに `imports` のモジュール指定子マップを使用し、 `/node_modules/cool-module/index.js` にあるバージョンにマッピングします。
なお、スコープを選択するために使用されるパスは、アドレスの解決方法には影響しません。
割り当てられたパスの値がスコープのパスと一致する必要はありませんし、相対パスは依然としてインポートマップを格納するスクリプトのベース URL に解決されます。
モジュール指定子マップの場合と同様に、多くのスコープキーを保有することができ、これらには重複するパスが格納される可能性があります。
複数のスコープがリファラーURLに一致する場合、最も固有のスコープパスが最初に(最も長いスコープキーが)指定子を指定しないか調べられます。
ブラウザーは、一致する仕様がない場合、次に一致するほとんどのスコープパスにフォールバックし、さらにその先に進みます。
一致するスコープのいずれにも一致する指定子がない場合、ブラウザーは `imports` キーのモジュール指定子マップに一致する指定子があるかどうかを調べます。
### ハッシュ化されたファイル名の割り当てによるキャッシュの改善
ウェブサイトで使用されるスクリプトファイルは、キャッシュを容易にするためにハッシュ化されたファイル名にすることがよくあります。
この手法の欠点は、モジュールが変更された場合、そのハッシュ化されたファイル名を使用してそれをインポートするすべてのモジュールも更新/再生成する必要があることです。
このため、潜在的に更新のカスケードが発生し、ネットワークリソースを浪費することになります。
インポートマップは、この問題に対する便利な解決策を提供します。
アプリケーションやスクリプトは、固有のハッシュ化されたファイル名ではなく、代わりにモジュール名(アドレス)のハッシュ化されていないバージョンに依存します。
下記のようなインポートマップは、実際のスクリプトファイルへのマッピングを提供します。
```json
{
"imports": {
"main_script":"/node/srcs/application-fg7744e1b.js",
"dependency_script":"/node/srcs/dependency-3qn7e4b1q.js"
}
}
```
もし `dependency_script` が変更された場合、ファイル名に格納されているハッシュも変更されます。この場合、モジュールの名前の変更を反映するためにインポート マップを更新するだけでよくなります。
import 文の指定子は変わらないので、これに依存する JavaScript コードのソースを更新する必要はありません。
## JavaScript 以外のリソースの読み込み
統一されたモジュールアーキテクチャがもたらす魅力的な機能のひとつに、JavaScript以外のリソースをモジュールとして読み込む機能があります。例えば、 JSON を JavaScript オブジェクトとして、または CSS を {{domxref("CSSStyleSheet")}} オブジェクトとしてインポートすることができます。
インポートするリソースの種類を明示的に宣言する必要があります。 既定では、ブラウザーはリソースが JavaScript であると想定し、解決されたリソースがそれ以外の場合にはエラーが発生します。 JSON、CSS、またはその他のリソースをインポートするには、[import 属性](/ja/docs/Web/JavaScript/Reference/Statements/import/with)構文を使用します。
```js
importcolorsfrom"./colors.json" with { type: "json" };
importstylesfrom"./styles.css" with { type: "css" };
```
ブラウザーはモジュール型の検証も行います。例えば、`./data.json` が JSON ファイルに解決されない場合は失敗します。これにより、データをインポートするだけで、誤ってコードが実行されないことを保証します。インポートが正常に完了すると、インポートした値を通常の JavaScript オブジェクトまたは `CSSStyleSheet` オブジェクトとして使用することができます。
```js
console.log(colors.map((color) =>color.value));
document.adoptedStyleSheets= [styles];
```
## HTML にモジュールを適用する
次に `main.js` モジュールを HTML ページに適用する必要があります。これは少し重要な点に違いがありますが、通常のスクリプトをページに適用する方法ととてもよく似ています。
最初に `type="module"` を [`<script>`](/ja/docs/Web/HTML/Reference/Elements/script) 要素に含めることで、そのスクリプトがモジュールであることを宣言します。`main.js` をインポートするには、次のようにします。
```html
<script type="module" src="main.js"></script>
```
また、JavaScript コードを `<script>` 要素の本文内に配置することで、モジュールのスクリプトを HTML ファイルに直接埋め込むこともできます。
```html
<script type="module">
/* ここに JavaScript モジュールコード */
</script>
```
`import` および `export` 文はモジュール内でのみ使用することができ、通常のスクリプトでは使用できません。 `<script>` 要素に `type="module"` 属性がなく、他のモジュールをインポートしようとした場合、エラーが発生します。例えば次のような場合です。
```html example-bad
<script>
import_from"lodash"; // SyntaxError: import declarations may only appear at top level of a module
// ...
</script>
<script src="a-module-using-import-statements.js"></script>
<!--SyntaxError:import declarations may only appear at top level of a module -->
```
通常、すべてのモジュールを個別のファイルで定義する必要があります。 HTML にインラインで宣言されたモジュールは、他のモジュールをインポートすることはできますが、それらがエクスポートする何らかの情報は、他のモジュールからアクセスすることはできません(URL を保有していないため)。
> [!NOTE]
> モジュールとその依存関係は [`<link>`](/ja/docs/Web/HTML/Reference/Elements/link) 要素で [`rel="modulepreload"`](/ja/docs/Web/HTML/Reference/Attributes/rel/modulepreload) を指定することで、事前読み込みすることができます。
> これにより、モジュールを使用する時点での読み込み時間を大幅に縮小することができます。
## モジュールとクラシックスクリプトとのその他の違い
- ローカルでテストしようとするときは注意してください。ローカルから(つまり `file://` URL を使って)HTML ファイルを読み込もうとすると、JavaScript モジュールのセキュリティ要件のために、CORS エラーが発生します。テストはサーバー経由で行う必要があります。
- また、モジュール内部で定義されたスクリプトの動作は、クラシックスクリプト内部のものと異なるかもしれません。これは、モジュール内部では自動的に{{jsxref("Strict_mode", "厳格モード", "", 1)}}が使われるからです。
- モジュールのスクリプトを読み込むときに `defer` 属性([`<script>` の属性](/ja/docs/Web/HTML/Reference/Elements/script#属性) を参照)を使う必要はありません。モジュールは自動的に遅延実行されます。
- モジュールは、複数の `<script>` タグで参照されていても一度しか実行されません。
- 最後ですが重要なこととして明らかにしておきますが、モジュールの機能は単独のスクリプトのスコープにインポートされます。つまり、インポートされた機能はグローバルスコープから利用することはできません。それゆえ、インポートされた機能はインポートしたスクリプトの内部からしかアクセスできず、例えば JavaScript コンソールからはアクセスできません。文法エラーは開発者ツール上に表示されますが、使えることを期待するデバッグ技術の中には使えないものがあるでしょう。
モジュールで定義した変数は、グローバルオブジェクトに明示的に割り当てられない限り、そのモジュールのスコープに属します。他にも、グローバル定義する変数は、モジュール内で利用できます。例えば、以下のコードが指定された場合は次のようになります。
```html
<!doctype html>
<html lang="en-US">
<head>
<meta charset="UTF-8" />
<title></title>
<link rel="stylesheet" href="" />
</head>
<body>
<div id="main"></div>
<script>
// A var statement creates a global variable.
var text = "Hello";
</script>
<script type="module" src="./render.js"></script>
</body>
</html>
```
```js
/* render.js */
document.getElementById("main").innerText = text;
```
グローバル変数 `text` と `document` はモジュール内で利用できるので、ページにはまだ `Hello` が表示されます。(この例から、モジュールは必ずしも import/export 文を必要としないことにも注意してください。必要なことは、エントリーポイントに `type="module"` があることだけです)。
## デフォルトエクスポートと名前付きエクスポート
これまでエクスポートした機能は、**名前付きエクスポート (named export)** というものです。それぞれの項目(関数、`const` など)は、エクスポート時にその名前を参照されて、インポート時にもその名前で参照されます。
エクスポートの種類には、他に**デフォルトエクスポート (default export)** と呼ばれるものもあります。これは、モジュールがデフォルトの機能を簡単に持つことができるように設計されたもので、また JavaScript のモジュールが既存の CommonJS や AMD のモジュールシステムと相互運用できるようになります (JsonOrendorff による [ES6InDepth:Modules](https://hacks.mozilla.org/2015/08/es6-in-depth-modules/) で上手く説明されています。"Default exports" で検索してみてください)。
どのように動作するか説明するので、使用例をみてみましょう。basic-modules の `square.js` に、ランダムな色、大きさ、位置の正方形を描く `randomSquare()` という関数があります。この関数をデフォルトとしてエクスポートしたいので、ファイルの末尾に次の内容を書きます。
```js
export default randomSquare;
```
中かっこがないことに注意してください。
または、`export default` を関数に追加して、次のように匿名関数のように定義することもできます。
```js
export default function (ctx) {
// …
}
```
`main.js` では、次のようにしてデフォルトの関数をインポートします。
```js
import randomSquare from "./modules/square.js";
```
インポートの時にも中かっこがないことに注意してください。これは、デフォルトエクスポートはモジュールごとにひとつしか作れず、`randomSquare` がそれであることがわかっているからです。上記は、基本的に次の簡略表現です。
```js
import { default as randomSquare } from "./modules/square.js";
```
> [!NOTE]
> エクスポートされる項目の名前を変更するために使われる as 構文については、以下の [インポートやエクスポートの名前を変更する](#インポートやエクスポートの名前を変更する)の節で説明します。
## 名前の衝突を避ける
これまでのところ、キャンバスに図形を描く私たちのモジュールは正常に動作しているようです。しかし、円や三角形など別の図形を描くモジュールを追加しようとしたらどうなるでしょう? そのような図形にも `draw()` や `reportArea()` のような関数があるかもしれません。もし同じ名前を持つ異なる関数を同じトップレベルのモジュールファイルにインポートしようとすると、最終的に名前の衝突によるエラーが起きるでしょう。
幸いなことに、これに対処する方法はいくつかあります。それらについて、次のセクションで見ていきましょう。
## インポートやエクスポートの名前を変更する
`import` 文や `export` 文の中かっこの中では、キーワード `as` と新しい名前を使うことで、トップレベルのモジュールでその機能を使うときの名前を変更することができます。
例えば、次のどちらも同じ仕事をしますが、少し異なる方法で行います。
```js
// module.js の内部
export { function1 as newFunctionName, function2 as anotherNewFunctionName };
// main.js の内部
import { newFunctionName, anotherNewFunctionName } from "./modules/module.js";
```
```js
// module.js の内部
export { function1, function2 };
// main.js の内部
import {
function1 as newFunctionName,
function2 as anotherNewFunctionName,
} from "./modules/module.js";
```
実際の例を見てみましょう。[renaming](https://github.com/mdn/js-examples/tree/main/module-examples/renaming) ディレクトリーでは、前の使用例と同じモジュールを使っていますが、円や三角形を描画するためのモジュールである `circle.js` と `triangle.js` も追加しています。
それぞれのモジュール内部では、同じ名前を持つ機能がエクスポートされており、それゆえそれぞれの末尾の `export` 文は次のように同一であることがわかります。
```js
export { name, draw, reportArea, reportPerimeter };
```
これらを `main.js` にインポートするために、次のようにするとします。
```js
import { name, draw, reportArea, reportPerimeter } from "./modules/square.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/circle.js";
import { name, draw, reportArea, reportPerimeter } from "./modules/triangle.js";
```
すると、ブラウザーは "SyntaxError: redeclaration of import name" (構文エラー: インポート名の再宣言) (Firefox の場合) のようなエラーを発生させるでしょう。
そのため、それぞれが固有の名前を持つようにするために、次のようにインポートの名前を変える必要があります。
```js
import {
name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter,
} from "./modules/square.js";
import {
name as circleName,
draw as drawCircle,
reportArea as reportCircleArea,
reportPerimeter as reportCirclePerimeter,
} from "./modules/circle.js";
import {
name as triangleName,
draw as drawTriangle,
reportArea as reportTriangleArea,
reportPerimeter as reportTrianglePerimeter,
} from "./modules/triangle.js";
```
他の方法として、例えば次のようにすることで、モジュールファイル側でこの問題を解決することもできます。
```js
// in square.js
export {
name as squareName,
draw as drawSquare,
reportArea as reportSquareArea,
reportPerimeter as reportSquarePerimeter,
};
```
```js
// in main.js
import {
squareName,
drawSquare,
reportSquareArea,
reportSquarePerimeter,
} from "./modules/square.js";
```
これも同じように機能します。どちらのスタイルを取るかはあなた次第ですが、モジュール側のコードはそのままにしてインポート側を変更する方が、間違いなく賢明です。これは、制御できないサードパーティーのモジュールからインポートするときには、特に意味があります。
## モジュールオブジェクトの作成
上記のインポート方法は正常に動作しますが、少し使いづらく冗長です。よりよい方法は、モジュール内のそれぞれの機能を、モジュールオブジェクトの中にインポートすることです。その構文は次のとおりです。
```js
import * as Module from "./modules/module.js";
```
これは、`module.js` の中にある全てのエクスポートを取得して、それらを `Module` というオブジェクトのメンバーとして利用できるようにすることで、独自の名前空間を持たせるような効果があります。次のようにして使います。
```js
Module.function1();
Module.function2();
```
実際の使用例を見てみましょう。[module-objects](https://github.com/mdn/js-examples/tree/main/module-examples/module-objects) ディレクトリーでは、また同じ例を使っていますが、この新しい構文を利用するために書き直されています。モジュール内のエクスポートは、いずれも次の単純な構文を使っています。
```js
export { name, draw, reportArea, reportPerimeter };
```
一方でインポートは次のようなものです。
```js
import * as Canvas from "./modules/canvas.js";
import * as Square from "./modules/square.js";
import * as Circle from "./modules/circle.js";
import * as Triangle from "./modules/triangle.js";
```
どの場合も、その指定されたオブジェクト名の配下からモジュールのインポートにアクセスできます。例えば次のようにして使います。
```js
const square1 = Square.draw(myCanvas.ctx, 50, 50, 100, "blue");
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);
```
このように (必要な箇所にオブジェクトの名前を含むようにさえすれば) コードは以前と同じように書くことができ、そしてインポートはより簡潔になります。
## モジュールとクラス
最初の方で触れましたが、クラスをエクスポートしたりインポートすることもできます。これがコード上で名前の衝突を避けるもう一つの方法で、もし自分のモジュールを既にオブジェクト指向のスタイルで書いているのであれば、特に便利です。
[classes](https://github.com/mdn/js-examples/tree/main/module-examples/classes) ディレクトリーの中には、私たちの図形を描くモジュールを ES クラスを使って書き直した例があります。例えば [`square.js`](https://github.com/mdn/js-examples/blob/main/module-examples/classes/modules/square.js) ファイルでは、次のように全ての機能を一つのクラスの中に持たせています。
```js
class Square {
constructor(ctx, listId, length, x, y, color) {
// …
}
draw() {
// …
}
// …
}
```
そして、次のようにエクスポートします。
```js
export { Square };
```
[`main.js`](https://github.com/mdn/js-examples/blob/main/module-examples/classes/main.js) では、これを次のようにインポートします。
```js
import { Square } from "./modules/square.js";
```
そして、正方形を描くために次のようにクラスを使います。
```js
const square1 = new Square(myCanvas.ctx, myCanvas.listId, 50, 50, 100, "blue");
square1.draw();
square1.reportArea();
square1.reportPerimeter();
```
## モジュールの集約
複数のモジュールをひとつに集約させたいと思うことがあるかもしれません。依存性の階層は複数になることがあり、いくつかあるサブモジュールをひとつの親モジュールにまとめて管理を単純化したいと思うかもしれません。これは、親モジュールで次の形式によるエクスポート構文を使うことで可能です。
```js
export * from "x.js";
export { name } from "x.js";
```
使用例は [module-aggregation](https://github.com/mdn/js-examples/tree/main/module-examples/module-aggregation) ディレクトリーを参照してください。この例 (クラスを使った以前の例を元にしています) には、`shapes.js` というモジュールが追加されています。これは `circle.js`、`square.js`、`triangle.js` の全ての機能をひとつに集約したものです。また、サブモジュールを `modules` ディレクトリーの中にある `shapes` というサブディレクトリーに移動させています。つまり、この例のモジュール構造は次のようなものです。
```plain
modules/
canvas.js
shapes.js
shapes/
circle.js
square.js
triangle.js
```
それぞれのサブモジュールでは、例えば次のような同じ形式のエクスポートが行われています。
```js
export { Square };
```
その次は集約を行う部分です。[`shapes.js`](https://github.com/mdn/js-examples/blob/main/module-examples/module-aggregation/modules/shapes.js) の内部には次のような行があります。
```js
export { Square } from "./shapes/square.js";
export { Triangle } from "./shapes/triangle.js";
export { Circle } from "./shapes/circle.js";
```
これらは、個々のサブモジュールのエクスポートを取得して、それらを `shapes.js` モジュールから利用できるようにする効果があります。
>**メモ:**`shapes.mjs` の中で参照されているエクスポートは、基本的にそのファイルを経由して転送されるだけで、ファイルの中には存在しません。そのため、同じファイルの中でそれらを使ったコードを書くことはできません。
最後に `main.js` ファイルでは、全てのモジュールのクラスにアクセスするために、次のインポートを書き換えています。
```js
import { Square } from "./modules/square.js";
import { Circle } from "./modules/circle.js";
import { Triangle } from "./modules/triangle.js";
```
書き換え後は、次のような 1 行になります。
```js
import { Square, Circle, Triangle } from "./modules/shapes.js";
```
## 動的なモジュールの読み込み
ブラウザーで利用できる JavaScript モジュールの最新機能は、動的なモジュールの読み込みです。これにより、全てを最初に読み込んでしまうのではなく、必要が生じたときにのみ動的にモジュールを読み込むことができます。これには明らかなパフォーマンス上の利点があります。どのように動作するのか、読んで見てみましょう。
この新しい機能により、[`import()`](/ja/docs/Web/JavaScript/Reference/Operators/import) を関数として呼び出し、そのときの引数としてモジュールへのパスを指定することができます。これは次のように {{jsxref("Promise")}} を返し、エクスポートにアクセスできるモジュールオブジェクト([モジュールオブジェクトの作成](#モジュールオブジェクトの作成)を参照)を使って履行状態になります。
```js
import("./modules/myModule.js").then((module) => {
// モジュールを使って何かをする。
});
```
> [!NOTE]
> 動的インポートは、ブラウザーのメインスレッド、共有ワーカー、専用ワーカーで許可されています。
> しかし、サービスワーカーやワークレットで `import()` を呼び出すと、例外が発生します。
<!-- https://whatpr.org/html/6395/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-specifier,-promisecapability) -->
例を見てみましょう。[dynamic-module-imports](https://github.com/mdn/js-examples/tree/main/module-examples/dynamic-module-imports) ディレクトリーには、以前のクラスの例に基づいた別の使用例があります。しかし、今回は使用例が読み込まれたときにはキャンバスに何も描画しません。その代わり "Circle" (円)、"Square" (正方形)、"Triangle" (三角形) という 3 つのボタンを表示し、それらが押されたとき、対応した図形を描くために必要なモジュールを動的に読み込んで使用します。
この使用例では [`index.html`](https://github.com/mdn/js-examples/blob/main/module-examples/dynamic-module-imports/index.html) と [`main.js`](https://github.com/mdn/js-examples/blob/main/module-examples/dynamic-module-imports/main.js) のみを変更しており、モジュールのエクスポートは以前と同じままです。
`main.js` では、それぞれのボタンへの参照を取得するために、次のように [`document.querySelector()`](/ja/docs/Web/API/Document/querySelector) を使っています。
```js
const squareBtn = document.querySelector(".square");
```
そしてそれぞれのボタンに、押されたときに関連するモジュールを動的に読み込んで図形を描くためのイベントリスナーを設定します。
```js
squareBtn.addEventListener("click", () => {
import("./modules/square.js").then((Module) => {
const square1 = new Module.Square(
myCanvas.ctx,
myCanvas.listId,
50,
50,
100,
"blue",
);
square1.draw();
square1.reportArea();
square1.reportPerimeter();
});
});
```
なお、履行されたプロミスはモジュールオブジェクトを返すので、クラスはそのオブジェクトのサブフィーチャーとなり、これでコンストラクターには `Module.Square( /* ... */ )` のように `Module.` を先頭に付けてアクセスする必要があります。
動的インポートのもう一つの利点は、スクリプト環境であっても常に利用できるということです。したがって、HTMLに既存の `<script>` タグがあり、そのタグに `type="module"` がない場合でも、モジュールとして配布されているコードを動的にインポートして再利用することができます。
```html
<script>
import("./modules/square.js").then((module) => {
// モジュールで何かを行う
});
// 他にも、グローバルスコープで処理をするコードで、まだモジュールにリファクタリングする準備が整っていないコードもあります。
var btn = document.querySelector(".square");
</script>
```
## 最上位の await
最上位の await は、モジュール内で利用できる機能です。つまり、`await` キーワードを使用することができます。これは、モジュールが大きな[非同期関数](/ja/docs/Learn_web_development/Extensions/Async_JS/Introducing)として動作できるようにするもので、親モジュールで使用する前にコードを評価できますが、兄弟モジュールの読み込みをブロックすることはしません。
例を見ていきましょう。この節で記述するすべてのファイルとコードは [`top-level-await`](https://github.com/mdn/js-examples/tree/main/module-examples/top-level-await) ディレクトリーにあり、前回までの例から拡張されています。
まず最初に、別個の [`colors.json`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/data/colors.json) ファイルでカラーパレットを宣言します。
```json
{
"yellow": "#F4D03F",
"green": "#52BE80",
"blue": "#5499C7",
"red": "#CD6155",
"orange": "#F39C12"
}
```
次に、[`getColors.js`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/modules/getColors.js) というモジュールを作成します。このモジュールは読み取りリクエストを使って [`colors.json`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/data/colors.json) ファイルを読み込み、そのデータをオブジェクトとして返すようにします。
```js
// 読み取りリクエスト
const colors = fetch("../data/colors.json").then((response) => response.json());
export default await colors;
```
ここで最後のエクスポート行に注目してください。
キーワード `await` を、定数 `colors` を指定したエクスポートの前に使用しています。これは、このモジュールを含む他のモジュールは、`colors` がダウンロードされ、解釈されるまで待ってから使用することを意味しています。
このモジュールを [`main.js`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/main.js) ファイルに含めてみましょう。
```js
import colors from "./modules/getColors.js";
import { Canvas } from "./modules/canvas.js";
const circleBtn = document.querySelector(".circle");
// …
```
シェイプ関数を呼び出す際に、前回使用された文字列の代わりに `colors` を使用することにします。
```js
const square1 = new Module.Square(
myCanvas.ctx,
myCanvas.listId,
50,
50,
100,
colors.blue,
);
const circle1 = new Module.Circle(
myCanvas.ctx,
myCanvas.listId,
75,
200,
100,
colors.green,
);
const triangle1 = new Module.Triangle(
myCanvas.ctx,
myCanvas.listId,
100,
75,
190,
colors.yellow,
);
```
これは [`main.js`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/main.js) 内のコードが [`getColors.js`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/modules/getColors.js) 内のコードを実行するまで実行されないので有益です。しかし、他のモジュールが読み込まれるのをブロックすることはありません。例えば、[`canvas.js`](https://github.com/mdn/js-examples/blob/main/module-examples/top-level-await/modules/canvas.js) モジュールは、`colors` が読み込まれている間、読み込みを継続します。
## インポート宣言は巻き上げされる
インポート宣言が[巻き上げが行われます](/ja/docs/Glossary/Hoisting)。この場合、インポートされた値は、宣言した場所よりも前にモジュールのコードで利用できるということ、そして、インポートされたモジュールの副作用は、モジュールの残りのコードが実行し始める前に生じるというということです。
例えば、`main.js` でコードの途中で `Canvas` をインポートしても、これは動作します。
```js
// …
const myCanvas = new Canvas("myCanvas", document.body, 480, 320);
myCanvas.create();
import { Canvas } from "./modules/canvas.js";
myCanvas.createReportList();
// …
```
それでも、コードの一番上にインポートをすべて配置することは良い習慣とされており、依存関係の分析が容易になります。
## 循環インポート
モジュールは他のモジュールをインポートすることができ、それらのモジュールは他のモジュールをインポートすることができ、といった具合に、モジュールは他のモジュールをインポートすることができます。これは「依存グラフ」と呼ばれる[有向グラフ](https://ja.wikipedia.org/wiki/グラフ理論#有向グラフ)を形成します。理想的な世界では、このグラフは[循環しません](https://ja.wikipedia.org/wiki/有向非巡回グラフ)。この場合、深さ優先探索を使用してグラフを評価することができます。
しかし、循環はしばしば避けられません。モジュール `a` がモジュール `b` をインポートしている場合、`b` が直接または間接的に `a` に依存していると、循環インポートが発生します。
```js
// -- a.js --
import { b } from "./b.js";
// -- b.js --
import { a } from "./a.js";
// Cycle:
// a.js ───> b.js
// ^ │
// └─────────┘
```
循環インポートは常に失敗するわけではありません。インポートされた変数の値は、その変数を実際に使用する際にのみ取得され(したがって、[ライブバインディング](/ja/docs/Web/JavaScript/Reference/Statements/import#インポートした値はエクスポートしたモジュールだけが変更できる)が可能になります)、その時点において変数が未初期化の状態である場合にのみ、 [`ReferenceError`](/ja/docs/Web/JavaScript/Reference/Errors/Cant_access_lexical_declaration_before_init) が発生します。
```js
// -- a.js --
import { b } from "./b.js";
setTimeout(() => {
console.log(b); // 1
}, 10);
export const a = 2;
// -- b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // 2
}, 10);
export const b = 1;
```
この例では、`a` と `b` の両方が非同期で使用されています。そのため、モジュールが評価される時点では、 `b` も `a` も実際に読み込まれることはなく、残りのコードは通常通り実行され、 2 つの `export` 宣言により `a` と `b` の値が生成されます。その後、タイムアウト後に `a` と `b` の両方が利用できるようになり、 2 つの `console.log` 文も通常通り実行されます。
コードを変更して `a` を同期的に使用すると、モジュール評価は失敗します。
```js
// -- a.js (entry module) --
import { b } from "./b.js";
export const a = 2;
// -- b.js --
import { a } from "./a.js";
console.log(a); // ReferenceError: Cannot access 'a' before initialization
export const b = 1;
```
これは、JavaScript で `a.js` を評価する際、`a.js` の依存関係である `b.js` を最初の段階で評価する必要があるためです。しかし、`b.js` は `a` を使用しており、`a` はまだ利用できません。
一方、コードを変更して `b` を同期的に、`a` を非同期的に使用するようにすると、モジュール評価は成功します。
```js
// -- a.js (entry module) --
import { b } from "./b.js";
console.log(b); // 1
export const a = 2;
// -- b.js --
import { a } from "./a.js";
setTimeout(() => {
console.log(a); // 2
}, 10);
export const b = 1;
```
これは、 `b.js` の評価が正常に完了するため、 `a.js` が評価される際に `b` の値が利用できるためです。
自分のプロジェクトでは、循環インポートは通常避けるべきです。なぜなら、コードにエラーの可能性が生じるからです。一般的な循環除去テクニックには、以下のようなものがあります。
-2 つのモジュールを 1 つに統合する。
- 共有コードを 3 つ目のモジュールに移す。
- あるモジュールから他のモジュールにコードを移す。
しかし、ライブラリーが互いに依存している場合にも循環インポートが発生することがあり、修正するのはより困難です。
## 「同型」モジュールの作成
モジュールの導入により、JavaScript の環境では、コードをモジュール方式で配布し、再利用することが奨励されています。しかし、それは必ずしも JavaScript コードの一部がすべての環境で実行できることを意味しているわけではありません。例えば、ユーザーのパスワードの SHA ハッシュを生成するモジュールを開発したとします。ブラウザーのフロントエンドで使用することはできますか? Node.js サーバーで使用することはできますか?答えは、「場合による」です。
前回示したように、モジュールは依然としてグローバル変数にアクセスすることができます。モジュールが `window` のようなグローバルを参照する場合、ブラウザーでは実行できますが、Node.js サーバーでは `window` が利用できないため、エラーが発生します。同様に、コードが機能するために `process` へのアクセスを必要とする場合、それは Node.js でしか使用できません。
モジュールの再利用性を最大化するために、コードを「同型」にする、つまり、どのランタイムでも同じ挙動を示すようにすることがよく推奨されます。これは、一般的に3つの方法で達成されます。
- モジュールを「コア」と「バインディング」に分割します。「コア」では、ハッシュを計算するような純粋な JavaScript のロジックに焦点を当て、DOM、ネットワーク、ファイルシステムへのアクセスは一切行わず、ユーティリティ関数を公開します。「バインディング」では、グローバルコンテキストからの読み書きができるようにします。例えば、「ブラウザーバインディング」では入力ボックスから、「Node バインディング」では `process.env` から値を読み込むことができますが、どちらの配置から読み込んだ値も同じコア関数に接続し、同じように処理されることにします。コアはどの環境でもインポートして同じように使用することができ、通常軽量であるバインディングだけをプラットフォームに固有であるようにします。
- 特定のグローバルが使用される前に存在するかどうかを検出します。例えば、`typeof window === "undefined"` と判定された場合、おそらく Node.js 環境であるため、DOM を読むべきではないことが分かります。
```js
// myModule.js
let password;
if (typeof process !== "undefined") {
// Node.js で実行中。 `process.env` から読み出す
password = process.env.PASSWORD;
} else if (typeof window !== "undefined") {
// ブラウザーで実行中。入力ボックスから読み出す
password = document.getElementById("password").value;
}
```
これは、2 つの分岐が実際に同じ動作(「同型」)で終わるのであれば、環境設定としては好ましいものです。同じ機能を提供することが不可能な場合、あるいは、未使用の部分が多いまま大量のコードを読み込む必要がある場合は、代わりに異なる「バインディング」を使用するのがよいでしょう。
- 足りない機能の代替を提供するために、ポリフィルを使用します。例えば、Node.js で v18 以降しか対応していない [`fetch`](/ja/docs/Web/API/Fetch_API) 関数を使用したい場合、 [`node-fetch`](https://www.npmjs.com/package/node-fetch) が提供するような同様の API を使用することができます。動的インポートによって条件付きで行うことができます。
```js
// myModule.js
if (typeof fetch === "undefined") {
// We are running in Node.js; use node-fetch
globalThis.fetch = (await import("node-fetch")).default;
}
// …
```
[`globalThis`](/ja/docs/Web/JavaScript/Reference/Global_Objects/globalThis) 変数は、どの環境でも利用できるグローバルオブジェクトで、モジュール内でグローバル変数を読み込んだり作成したりしたい場合に有益です。
これらの実践は、モジュールに固有のものではありません。それでも、コードの再利用性やモジュール化の流れから、使用可能な限り多くの人に楽しんでもらえるように、コードをクロスプラットフォームにすることが推奨されています。Node.js のようなランタイムも、ウェブとの相互運用性を高めるために、使用可能な範囲で積極的にウェブ API を実装しています。
## トラブルシューティング