JavaScriptのモジュールについて

JavaScriptのファイル(.js)には、スクリプトとモジュールがあります
コードが大量になると、モジュール(部品)に分割して、必要なモジュールをスクリプトや別のモジュールにインポートできる仕組みが必要です

モジュールのトップレベルで宣言された変数や関数はモジュールスコープとなり、他のモジュールやスクリプトのからそのままでは参照できません
必要なものだけ他のモジュールで利用できるように切り出して利用します

余談:VSCodeで、モジュールで切り出す関数の上に下記のようにコメントをつけると、「切り出した関数を使うスクリプト側の関数」の上でホバーすると説明が表示されます

/**
 * ここに関数の説明
 */
目次
  1. ESmodules と CommonJS
  2. requireとexports(CommonJS)
    1. require
    2. module.exports
  3. importとexport(ESmodules)
    1. ESmodulesを使う場合
    2. import
    3. export
    4. 静的インポートと動的インポート
  4. デフォルトエクスポートと名前付きエクスポート

ESmodules と CommonJS

モジュールの実装方法として、主なものがESmodulesとCommonJSです

ESmodules(ESM)
ES6で策定され、「ブラウザで実行されるJavaScript」でも「Node.js」でも使用可能なモジュール管理の仕組みです
import/export
CommonJS(CJS)
「Node.js」で使用可能なモジュール管理の仕組みです
require/exports

Node.js12からESmodulesがサポートされ、ESmodulesがメインになると思われますが、Node.jsのデフォルトはCommonJSです
ESmodulesとCommonJSは基本的には混在して利用出来ません
*例えば、ESmodulesでexportされたコードはCommonJSの規格でrequireできません
CommonJSの規格でexportsされたコードはESmodulesの規格でimportは可能ですがブラウザで実行はできません

CommonJSの規格でexportsされたモジュールをブラウザで動かすには

Universal Module Definition(UMD
webpackなどのモジュールバンドラを使用すれば動かすことができます

requireとexports(CommonJS)

require

requireはJavaScriptファイルを読み込む機能

requireはNode.jsであればそのまま実行できますが、ブラウザではそのまま実行できません
もしブラウザで実行したい場合はwebpackなどのモジュールバンドラが必要です

requireは、エクスポートされたモジュールを戻り値として返します
戻り値を定数に代入し、その定数を使うことでモジュールを扱えます
オブジェクトのプロパティだけを分割代入で読み込むこともできます(コアモジュールの特定の関数しか使わないときなど)

const obj = require("モジュールのパスまたはモジュール名")

const {hoge} = require("モジュールのパスまたはモジュール名")

「npmパッケージ」や「コアモジュール」の場合は「モジュール名」で読み込みます
「自作モジュール」の場合は「モジュールのファイルパス」で読み込みます
*絶対パスでも相対パスでも可
相対パスの時は「./」や「../」をつけて相対パスであることを明示します

*requireはJSONファイルをパースしてオブジェクトとして読み込むこともできます(例えば、ディレクトリ名の場合は、ディレクトリ内のpackage.jsonを読み込みます)

どのようにモジュールを検索するか

  • コアモジュールの場合はコアモジュールを読み込みます
  • パス指定の場合
    • 「/」から始まる場合:ファイルシステムのルートからのパス
    • 拡張子がある場合:ファイルと解釈されます
    • 拡張子がない場合:「js」「.json」「.node」のファイルなどを探し、なければディレクトリ名と解釈され、配下の「package.json」を読む込みます
  • コアモジュールでもパス指定でもない場合(npmモジュール)は「node_modules」内のモジュールを探し、ない場合はルートまで「node_modules」を探します(グローバルにインストールしたモジュールを探します)

*requireでは拡張子を省略することができます
ちなみに、TypeScriptでの開発では拡張子を書くと逆に動かないケースもあります(コンパイル後に生成されたJavaScriptファイルから.tsの方のファイルを見に行ってしまうなど)

「require」で一度呼ばれたファイルはキャッシュされます

module.exports

モジュールをエクスポートする場合は「module.exports」や「exports」を使用します

「moduleオブジェクト」はグローバルオブジェクトで、「exportsプロパティ」を持っています
だから「module.exports」の場合、module.exports = {...} のようにプロパティ名を設定せずに値(オブジェクト・プリミティブ値・関数・クラス)を格納できます

注意:しかし「exports」の場合、exports={...} ではモジュール化になりません(新しいオブジェクトを作っただけです)
exportsの場合は、任意のプロパティ名を設定して再利用する値を格納します

基本的には「module.exports」を利用して、「複数の値をまとめたオブジェクト」でエクスポートするのかな^^;と思います
*module.exportsは良くも悪くも出力しているものの名前を変更できます

const hoge = function(){
  //処理内容
};

module.exports = {
  name: "太郎",
  hoge,
};

exportsの場合は、任意のプロパティ名を設定してエクスポートします

exports.hoge = function(){
   //処理内容
}

exports.hogeObj = {
  name: "太郎"
}

importとexport(ESmodules)

同じモジュールが複数の他の場所でインポートされる場合でもそのコードは初回のみ実行されます(キャッシュされます)
エクスポートしたものはすべてのインポートしているモジュールで利用されます(インポート間で共有されます)

ESmodulesを使う場合

ブラウザで

ブラウザでESmodulesの有効化する場合は、<script>タグに type="module" を付与します
defer属性(スクリプトの実行はHTML解析が終わってから行われます)を使う必要はありません(モジュールは自動的に遅延実行されます)
*インポートされた機能はインポートしたスクリプトの内部からしかアクセスできません(モジュールスコープ
*グローバルスコープの場合、トップレベルの「this」は「windowオブジェクト」ですが、モジュールスコープのトップレベルの「this」は「undefined」です

<script type="module">
//import ...
</script>

<!-- JavaScriptファイルを読み込む場合 -->
<script type="module" src="main.js"></script>

<!-- 古いブラウザはnomodule属性で、フォールバックを提供することが可能です -->
<script nomodule>
//フォールバックコード
</script>

<!-- ちなみにWebpackなどバンドルツールを使用する場合、そもそもモジュールを1つのファイルにまとめているので type=moduleは不要です-->
<script src="bundle.js"></script>

備考:スコープとは変数の名前や関数などの参照できる範囲を決めるものです
スコープの中で定義された変数はスコープの内側でのみ参照でき、スコープの外側からは参照できません

ちなみにモジュールを使わない場合
lib.jsとmain.jsのグローバルスコープはファイルにまたがり共有され、他のファイル内で同じ名前の変数名があるとエラーが発生します
それを回避するために変数のスコープを関数スコープにします

 <script src='scripts/lib.js'></script>
 <script src='scripts/main.js'></script>
function init() {
  const name = 'hoge';

}
init()

//即時関数
(function() {
  const name = 'hoge';

})();

Node.jsで

Node.jsでESmodulesを使う場合は
JavaScriptのファイルをすべて.mjsの拡張子に変更、または
package.jsonに"type": "module"を追加(パッケージ全体がESmodulesをサポートします)します

{
  "type": "module", 
}

*一部のモジュールのみCommonJSで読み込む時はCommonJSで書かれているファイル名をすべて.cjsに変更

「import」形式で「require」を使いたい時(絶対パスでモジュールを読み込みたい時)
「require」関数を作ります
*絶対パスでモジュールを読み込みたい時や
JSONをオブジェクトに変換して読み込みたい時など

 import module from 'module';
 const require = module.createRequire(import.meta.url);

 const hogehoge = require("絶対パス");
 const jsonObj = require("./hoge.json");

そのままでは__dirname(現在のディレクトリのパス)が使えないので

import { dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url));

import

デフォルトエクスポートがある場合は、任意の名前を付けインポートします

import defaultExport from "module-name";

デフォルトエクスポートがない場合
名前付きエクスポートでは、変数名や関数名がそのまま外部から呼び出す際の識別子となります
すべてをインポートする場合は*(ワイルドカード)を使いasキーワードを使って別名を付けます
モジュール名(ここでは「hoge」)を名前空間として用います

import * as hoge from "module-name";

名前付きエクスポートされた値をimportする時は使用する値は{}でカンマ区切りで囲います
asキーワードを使って別名を付けることもできますます

import {name, hoge as 別名} from "./module.js";

モジュールを実行するだけにインポートする場合(値はインポートされない)はfromも不要です

import './my-module.js';

importとrequireの相違点

  • importではStrictモードが自動的に有効になります
  • requireは上から順に読み込んで実行されますが、importはモジュールの読み込みを非同期で行います(Promiseを返します)
  • importでは相対パスのみですが、requireは絶対パスも利用出来ます
  • requireはファイル内のどこにでも書くことができますが、importは必ずファイルの一番上に書く必要があり、またファイルの拡張子は省略出来ません(webpackでimportするモジュールはファイルの拡張子の省略可能)

export

export文は、埋め込みスクリプトでは使えません

モジュールから関数・オブジェクト・プリミティブ値へのライブバインディングのエクスポートを行います
*ライブバインディングは、ESmodulesで導入された概念で、エクスポートモジュールが値を変更すると、変更がインポート側から表示されることを意味します

相違点:CommonJSでは、モジュールのエクスポートはコピーされるので、エクスポート側で起こった変更は表示されません

最も簡単な使い方は、モジュール外部に公開したい項目の前にexport をつけます
エクスポートできるものは、最上位の階層の「関数・var・let・const・クラス」です(例えば関数内のスコープでexportを使うことはできません)

export const name = "太郎";

export function hoge() {
  // 処理内容
}

エクスポートしたい全ての項目をまとめて、カンマ区切りで{}で囲み、名前付きエクスポートができます

 const name = "太郎";

 function hoge() {
  // 処理内容
  }

export { name, hoge };

デフォルトエクスポート
*モジュールでは、1つのファイルに1つだけデフォルトエクスポートが定義できます
そのため複数のexport defaultがあればは、後のexport defaultがの前のexport defaultを上書きしてしまいます

// デフォルトとして事前に定義された機能のエクスポート
export { myFunction as default };

// 個別の機能をデフォルトとしてエクスポート
export default function () { ... }
export default class { .. }

//オブジェクトにまとめる
export default {
field1: value1,
field2: value2
};

静的インポートと動的インポート

標準のインポート構文は静的インポートです

静的インポートはコードを読み込んだ時点でインポート先のモジュー ルのトップレベルのコードの実行まで行います
初期表示が遅くなる傾向にあります

動的インポートはコードを実行する段階で読み込みます
初期表示時に必要のないJavaScriptは動的にインポートすることで読み込み時に必要のないJavaScriptを除くことができます(対応ブラウザに注意)

動的インポートはモジュールのパスを引数にとる関数です
プロミスでモジュールが返却されます

import('/my-module.js')
  .then((modules) => {
    // modulesを使った何らかの処理
  });


//awaitキーワードを使う
async function fun(){
const modules = await import ('/my-module.js');
   // modulesを使った何らかの処理
}
fun();

デフォルトエクスポートと名前付きエクスポート

デフォルトエクスポート(「module.exports」「export default」)
*エクスポートするときに名前を付けても付けなくても良い

  • import(require)する時の名前は任意です
  • 1つのファイルで1つだけ

名前付きエクスポート(「exports」「export」)

  • 決まった名前でimport(require)する必要があります(*asキーワードで別名をつけることはできます)
  • ひとつのファイルから複数exportできます