EcmaScripModuleを使う際の注意点

# ■はじめに

こんにちは。白水（しらみず）と申します。
普段は不動産スタートアップでフロントエンドエンジニアとしてお仕事しています。

今回「EcmaScripModule」を使うにあたって知っておくべき注意点について書いています。

[Shiramizu\_Junya lit\.link\(リットリンク\)](https://lit.link/js226)

# ■モジュールとは何か？

モジュールとは、1つの部品を意味します。

通常、モジュールはクラスや便利な関数です。

[モジュール](https://ja.wikipedia.org/wiki/%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB)

![スクリーンショット 2024-11-15 8.39.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307408/a93004f6-55ca-89ce-a266-a461cd634a31.png)


上記画像のように、sayNameという関数をsample01.jsに定義して、export文を使って外部からアクセスできるようにしています。

main.jsでは、sayName関数をimportして、呼び出せるようにしています。

`import` は、現在のファイルからの相対パス または 絶対パスで指定します。

このようにモジュール機能によって、役割ごとにファイルを分けたコードを書くことができ、１つのファイルが巨大になったりしないよう、保守性を高めることができます。

# ■モジュールを使う際の注意点

## ◆注意点①：type="module"の付与に関して

```jsx
<body>
    <script type="module" src="./src/main.js"></script>
</body>
```

モジュール使う際は、`type="module"`を付与する必要があります。

## ◆注意点②：プロトコルに関して

fileプロトコルでWeb ページをローカルで開いた場合、importやexportは使えません。

そのため、VSCodeのLiveServerのようなWebサーバーを使用する必要があります。

[](https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer)

## ◆注意点③：厳格モード関して

```jsx
// main.js
import { testStrictMode } from './sample01.js';
testStrictMode();
```

```jsx
// sample01.js
export const testStrictMode = () => {
	// テスト1: 未宣言変数への代入
	try {
		x = 10; // 変数宣言なしの代入
		console.log('未宣言変数への代入が可能です');
	} catch (e) {
		console.error('未宣言変数への代入でエラー:', e);
		// ReferenceError: x is not defined
	}

	// テスト2: 関数内でのthisの値
	const checkThis = () => {
		console.log('関数内のthisの値:', this);
		// 非strictモード: window
		// strictモード: undefined
	};
	checkThis();
};
```

モジュールは常に `use strict` モードで実行されます。

そのため、未宣言変数への代入はエラーになります。

また、checkThis関数のthisはundefinedになります。

## ◆注意点④：モジュールスコープに関して

```jsx
// main.js
import { greeting } from './moduleA.js';
import { showGreeting } from './moduleB.js';

console.log(message); // 🟥 ReferenceError: message is not defined
greeting(); // こんにちは
showGreeting(); // こんにちは
```

```jsx
// moduleA.js
const message = 'こんにちは'; // このモジュール内でのみアクセス可能

export const greeting = () => {
	console.log(message);
}

```

```jsx
// moduleB.js
import { greeting } from './moduleA.js';

export const showGreeting = () => {
	console.log(message); // 🟥 エラー：messageは参照できない
	greeting(); // OK：エクスポートされた関数は使える
};

```

各モジュールにはモジュールスコープというものがあります。

モジュール内の変数は関数は、他のモジュールからアクセスすることはできません。

`moduleA.js`に`const message = 'こんにちは';`を定義しています。

しかし、`message`はexportしていないため、`main.js`や`moduleB.js`からアクセスするとReferenceErrorになります。

モジュール内の変数は関数に、外部からアクセス可能にしたい場合は、 モジュール内の変数は関数を`export` をして、 `import` がすることで利用できるようになります。

```jsx
<body>
    <script type="module">
        const userName = "佐藤";
    </script>

    <script type="module">
        console.log(user);
    </script>
</body>
```

そのため、上記のようにtype="module"のscriptを２つ作って、別のスコープの変数にアクセスすることもできません。

## ◆注意点④：importの実行回数

```jsx
<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Module Test</title>
</head>

<body>
    <script type="module" src="./src/main.js"></script>
    <script type="module" src="./src/main.js"></script>
</body>

</html>

```

![スクリーンショット 2024-11-16 10.06.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307408/a3922487-e381-0192-8615-18d00edb6874.png)

main.jsというモジュールを２回読み込んでも、ネットワーク上では１回しか読み込まれません。

```jsx
// moduleA.js
export const admin = {
	name: '佐藤',
};
```

```jsx
// main.js
import { admin } from './moduleA.js';

admin.name = '田中';

import { log } from './moduleB.js';
log();

```

```jsx
// moduleB.js
import { admin } from './moduleA.js';

export const log = () => {
	console.log(admin); // > {name: '田中'}
};
```

`main.js`で`moduleA.js`のadminの値を書き換えると、`moduleB.js`のadminの値も書き換わります。

## ◆注意点⑤：モジュールの遅延評価

```jsx
<!DOCTYPE html>
<html>

<head>
    <script type="module">
        console.log('モジュール実行時：');
        console.log('- DOMContentLoaded済み:', document.readyState !== 'loading'); // - DOMContentLoaded済み: true
        console.log('- title要素:', document.querySelector('#title')); // - title要素: <h1 id="title">タイトル</h1>
    </script>

    <script>
        console.log('通常スクリプト実行時：');
        console.log('- DOMContentLoaded済み:', document.readyState !== 'loading'); // - DOMContentLoaded済み: false
        console.log('- title要素:', document.querySelector('#title')); // - title要素: null
    </script>

    <script defer>
        console.log('defer実行時：');
        console.log('- DOMContentLoaded済み:', document.readyState !== 'loading'); // - DOMContentLoaded済み: false
        console.log('- title要素:', document.querySelector('#title')); // - title要素: null
    </script>
</head>

<body>
    <h1 id="title">タイトル</h1>
</body>

</html>

```

`type="module"`を付与すると、モジュールスクリプトは、DOMContentLoadedイベントの後に実行されます。

そのため、通常の`<script>`はDOMの解析が終わっていないので、DOMは取得できません。

`<script defer>`は、HTML解析完了後だがDOMContentLoadedイベントの前に実行されるので、DOMは取得できません。

`<script type="module">`の場合は、DOMContentLoadedイベントの後に実行されるので、必ずDOMを取得できます。

HTML解析開始
↓
通常のスクリプト実行（この時点で#titleは存在しない）
↓
HTML解析続行
↓
defer付きスクリプト実行（この時点でもまだDOMは完全には構築されていない）
↓
HTML解析完了とDOM構築
↓
モジュールスクリプト実行（この時点でDOMは完全に構築済み）

という実行順序になります。

## ◆注意点⑥：パスの指定

```jsx
import { admin } from 'moduleA.js'; // Uncaught TypeError: Failed to resolve module specifier "moduleA.js". Relative references must start with either "/", "./", or "../".
console.log(admin);
```

モジュールをimportするときは、相対パスか絶対パスで指定する必要があります。

そのため、`import { admin } from 'moduleA.js';`という読み込みはエラーになります。

```jsx
import { admin } from './moduleA.js';
console.log(admin);
```

`./`を付与して、相対パスで読み込むことはできます。

# ■importのmeta情報

```jsx
import { admin } from './moduleA.js';

admin.name = '田中';

import { log } from './moduleB.js';
log();

console.dir(import.meta);
```

`import.meta`には、モジュールに関する情報を含んでいます。

# ■資料

[JavaScript モジュール - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Modules)