■はじめに
こんにちは。白水(しらみず)と申します。
普段は不動産スタートアップでフロントエンドエンジニアとしてお仕事しています。
今回「EcmaScripModule」を使うにあたって知っておくべき注意点について書いています。
Shiramizu_Junya lit.link(リットリンク)
■モジュールとは何か?
モジュールとは、1つの部品を意味します。
通常、モジュールはクラスや便利な関数です。
上記画像のように、sayNameという関数をsample01.jsに定義して、export文を使って外部からアクセスできるようにしています。
main.jsでは、sayName関数をimportして、呼び出せるようにしています。
import
は、現在のファイルからの相対パス または 絶対パスで指定します。
このようにモジュール機能によって、役割ごとにファイルを分けたコードを書くことができ、1つのファイルが巨大になったりしないよう、保守性を高めることができます。
■モジュールを使う際の注意点
◆注意点①:type="module"の付与に関して
<body>
<script type="module" src="./src/main.js"></script>
</body>
モジュール使う際は、type="module"
を付与する必要があります。
◆注意点②:プロトコルに関して
fileプロトコルでWeb ページをローカルで開いた場合、importやexportは使えません。
そのため、VSCodeのLiveServerのようなWebサーバーを使用する必要があります。
◆注意点③:厳格モード関して
// main.js
import { testStrictMode } from './sample01.js';
testStrictMode();
// 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になります。
◆注意点④:モジュールスコープに関して
// main.js
import { greeting } from './moduleA.js';
import { showGreeting } from './moduleB.js';
console.log(message); // 🟥 ReferenceError: message is not defined
greeting(); // こんにちは
showGreeting(); // こんにちは
// moduleA.js
const message = 'こんにちは'; // このモジュール内でのみアクセス可能
export const greeting = () => {
console.log(message);
}
// 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
がすることで利用できるようになります。
<body>
<script type="module">
const userName = "佐藤";
</script>
<script type="module">
console.log(user);
</script>
</body>
そのため、上記のようにtype="module"のscriptを2つ作って、別のスコープの変数にアクセスすることもできません。
◆注意点④:importの実行回数
<!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>
main.jsというモジュールを2回読み込んでも、ネットワーク上では1回しか読み込まれません。
// moduleA.js
export const admin = {
name: '佐藤',
};
// main.js
import { admin } from './moduleA.js';
admin.name = '田中';
import { log } from './moduleB.js';
log();
// moduleB.js
import { admin } from './moduleA.js';
export const log = () => {
console.log(admin); // > {name: '田中'}
};
main.js
でmoduleA.js
のadminの値を書き換えると、moduleB.js
のadminの値も書き換わります。
◆注意点⑤:モジュールの遅延評価
<!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は完全に構築済み)
という実行順序になります。
◆注意点⑥:パスの指定
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';
という読み込みはエラーになります。
import { admin } from './moduleA.js';
console.log(admin);
./
を付与して、相対パスで読み込むことはできます。
■importのmeta情報
import { admin } from './moduleA.js';
admin.name = '田中';
import { log } from './moduleB.js';
log();
console.dir(import.meta);
import.meta
には、モジュールに関する情報を含んでいます。