そもそもモジュールって何?
まず前提として、アプリケーションはたくさんのプログラムから成り立っています。
この全てをひとつのファイルで管理するとファイルの肥大化を誘発させてしまいます。
ファイルの肥大化による弊害は次のようなものがあります。
- コード量が多くファイルが縦長になる
- ぱっと見どこに何が書いてあるのかわからず、ものすごく見にくい。
- 先に定義した変数を上書きしてしまうことによるバグの発見が遅れる
そのため、規模の大きなアプリケーションはファイルの肥大化による弊害を防ぐためにひとつのファイルにすべてのコードを記載するのではなく、複数のファイルに分けます。この分割したファイルひとつひとつをモジュールといいます。一般的には機能ごとにコードを分けてファイル名を「機能名.js」のように命名するようです。
モジュールを利用するためには
- scriptタグに「type="module"」を付与することでモジュール化
- 使う側のファイルは"モジュールを使いますよ~"という宣言(import)
- 使われる側のモジュール"僕のコード使っていいですよ~"という宣言(export)
という設定が必要になります。
下記ファイル構成を用いて解説します。
- HTMLファイル:「index.html」
- モジュールを使う側のファイル:「import.js」
- 使われる側のモジュールのファイル:「export.js」
書き方は以下のとおりです。
<script type="module" src="import.js"></script>
// 変数のエクスポート
export const language = "英語";
// 関数のエクスポート
export function hello () {
console.log('ハロー')
}
// オブジェクトのエクスポート
export const person = {
name: 'Kuma',
age: 29,
gender: 'male'
}
// 変数のインポート
import { language } from "./export.js";
// 関数のインポート
import { hello } from "./export.js";
// オブジェクトのインポート
import { person } from "./export.js";
モジュールを使うメリット
- 保守性を高められる
- 名前空間を分けることができる
保守性を高められる
保守性を高めるとは、メンテナンスをしやすい状態にする事です。
極端な例ですが、たとえば同じ関数のコードを100箇所に記載しているとします。その関数の内容に変更があった場合、100箇所以上書き換えなければなりません。この関数をモジュールで作成していれば、変更するのはモジュールで定義しているコードだけで済みます。
またコードを分けることでファイルは縦長になりすぎず、コードの見通しも良くなりどこに何を書いてあるのか分かりやすくなります。
このようにモジュールは仕様変更に強い特徴を持ちます。
その効果をコードでみてみましょう。以下のように各jsファイルで関数「hello」をコピペして使っていたとします。
function hello() {
console.log("hello");
}
hello();
function hello() {
console.log("hello");
}
hello();
function hello() {
console.log("hello");
}
hello();
関数「hello」に内容を付け加えたい場合はすべてのスクリプトで追加が必要になります。
これでは非常に保守性は低いです。
function hello() {
console.log("hello");
console.log('see you'); // 追加
}
hello();
function hello() {
console.log("hello");
console.log('see you'); // 追加
}
hello();
function hello() {
console.log("hello");
console.log('see you'); // 追加
}
hello();
ではmoduleA.jsで関数「hello」を定義し、moduleB.js、moduleC.jsからインポートしてみましょう。すっきりした書き方になりました。
export function hello() { // 関数「hello」をエクスポート
console.log("hello");
console.log('see you');
}
hello();
import { hello } from "./moduleA.js" // 関数「hello」をインポート
hello(); // 「hello」,「see you」と表示される
import { hello } from "./moduleA.js" // 関数「hello」をインポート
hello(); // 「hello」,「see you」と表示される
この状態で関数「hello」にbyeを出力する処理を付け加えてみましょう。
moduleA.jsだけを編集すればすべての関数に反映されます。これで保守性は高くなりました。
export function hello() { // 関数「hello」をエクスポート
console.log("hello");
console.log('see you');
console.log('bye');
}
hello();
import { hello } from "./moduleA.js" // 関数「hello」をインポート
hello(); // 「hello」,「see you」,「bye」と表示される
import { hello } from "./moduleA.js" // 関数「hello」をインポート
hello(); // 「hello」,「see you」,「bye」と表示される
名前空間を分けることができる
名前空間とは定義した変数や定数、関数の影響を及ぼす範囲の事をいいます。
モジュールはこの範囲をモジュール内だけに閉じ込め、意図しない変数の上書きなどによるバグを予防することができます。
たとえば以下では定数はコンソール上で「英語」と表示されます。
<script>
const langage = "英語";
console.log(langage); // 「英語」と表示される
</script>
以下では「Identifier 'langage' has already been declared」というエラーになります。
理由はconstの再定義はできないからです。スクリプト内の変数の名前空間が、他のスクリプト内に影響を及ぼしている状態です。
<script>
const langage = "英語";
</script>
<script>
const langage = "日本語"; //「Identifier 'langage' has already been declared」というエラー
console.log(langage);
</script>
ではこうするとどうでしょう。「日本語」と表示されます。
「type="module"」を付与してモジュール化することにより、「langage = "英語"」の及ぼす範囲をスクリプト内に閉じ込めました。
そのためもう一つのスクリプトではconstの宣言をできています。
<script type="module"> // 「type="module"」を付与
const langage = "英語";
</script>
<script>
const langage = "日本語";
console.log(langage); // 「日本語」と表示される
</script>
規模の大きなアプリケーションを作ったり、大人数で共同開発するような場合は、名前が被ってしまうことはおおいに考えられるのでモジュールを使って名前空間を分けることはとても有用になります。
名前付きエクスポート(named export)とimport
名前付きエクスポートのexport
- 変数、定数、関数、オブジェクトの"命名した名前"でエクスポートする
- 一つずつでも複数まとめるのどちらでもエクスポートできる
// 一つずつエクスポートする場合の書き方
export const language = "英語";
export function hello() {
console.log("ハロー");
}
export const person = {
name: "Kuma",
age: 29,
gender: "male"
};
// 複数まとめてエクスポートする場合の書き方
const language = "英語";
function hello() {
console.log("ハロー");
}
const person = {
name: "Kuma",
age: 29,
gender: "male"
};
export { language, hello, person };
名前付きエクスポートのimport
- エクスポートした名前を{}(かぎかっこ)で囲いインポートする
- 一つずつでも複数まとめるのどちらでもインポートできる
// 一つずつインストール
import { language } from "./export.js";
import { hello } from "./export.js";
import { person } from "./export.js";
// 複数まとめてインポート
import { language, hello, person } from "./export";
別名(エイリアス)でエクスポート時/インポート時することもできる
- 使いどころとしては同じ名前でエクスポートされた識別子を区別する時に使う
- 実際の現場ではインポート側でエイリアス名を付与して読み込むことが多い
// languageを「lang」、helloを「hel」、personを「per」としてエクスポート
export { language as lang, hello as hel, person as per };
// langを「lan」、helを「he」、perを「pe」としてインポート
import { lang as lan, hel as he, per as pe } from "./export";
デフォルトエクスポート(default export)とimport
デフォルトエクスポートのexportの書き方
const language = "英語";
export default language;
※ひとつのモジュールにつき1回しか使えない
const language = "英語";
function hello() {
console.log("ハロー");
}
// オブジェクトの宣言
const person = {
name: "Kuma",
age: 29,
gender: "male"
};
export default language;
export default hello; // 2つ以上使おうとするとエラーになる
デフォルトエクスポートのimportの書き方
- {}をつけずにむき出しで記載する
- ひとつしかエクスポートしないので好きな名前でインポートできる
import languageeeee from "./export.js";
名前付きエクスポートとデフォルトエクスポート、どちらを使うべきか
デフォルトエクスポートを使ったほうがよいみたいです。
下記記事が参考になります。
https://engineering.linecorp.com/ja/blog/you-dont-need-default-export/
余談(JavaScriptで利用するモジュールの種類)
JavaScriptにはモジュールの種類は2つあります(厳密には他にもありますが、大きく分けてという意味で)
知っておけば混乱しません。
ESModule形式
・JavaScriptの公式の言語仕様(これをECMAScriptという)として正式に策定されたモジュール
・"JavaScriptの公式の言語仕様"なのでサーバーサイド(node.js)でもブラウザ側のどちらでも使える
・ES2015(ES6)の構文
・ブラウザ(IE)やそのバージョンによっては対応していない場合もある
・開発時(コードを書くとき)はES6で、対応していないブラウザにも読み込めるようにwebpackを利用して変換するやり方が一般的
・importでデータの読み込み、exportでモジュールから外部にデータを出力する
// ESModule形式
import { language } from './moduleA.mjs'; //インポート構文
export const language = "英語"; //エクスポート構文
CommonJS形式
・サーバーサイド側(node.js)のJavaScriptの言語仕様であるCommonJSのモジュール
・requireでデータの読み込み、exportsでモジュールから外部にデータを出力する
// CommonJS形式
import { language } from './moduleA.mjs'; //インポート構文
export const language = "英語"; //エクスポート構文
おわり