JavaScriptのモジュールについて

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は基本的には混在して利用出来ません(例えば、CommonJSの規格でexportsされたコードはESmodulesの規格でimportできません)
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)

同じモジュールが複数の他の場所でインポートされる場合でもそのコードは初回のみ実行されます(キャッシュされます)
JavaScriptのモジュールは、デフォルトで変数や関数などを非公開になり、外部に公開して使用したい値はexportして必要なものをimportして使用します
エクスポートしたものはすべてのインポートしているモジュールで利用されます(インポート間で共有されます)

ESmodulesを使う場合

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

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

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

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

//*ちなみにWebpackなどバンドルツールを使用する場合 type=moduleは不要です
<script src="bundle.js"></script>

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

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

{
  "main": "index.js",
  "type": "module", 
}

一部のモジュールのみCommonJSで読み込む時はCommonJSで書かれているファイルをすべて.cjsに変更し、createRequireメソッドでrequire関数を作る一手間が必要です

import { createRequire } from "module";
const require = createRequire(import.meta.url);
 
const { hogehoge } = require("./hoge.cjs");

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 };

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

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

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

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

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

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

静的インポートはコードを読み込んだ時点でインポート先のモジュー ルのトップレベルのコードの実行まで行います
動的インポートはコードを実行する段階で初めてどのモジュールを読み込むかが決定されます

静的インポートではブラウザがモジュールを読み込んだ時点でモジュールのトップレベルコードの実行までを行うので、初期表示が遅くなる傾向にあります
そこで、初期表示時に必要のない機能の読み込みなどは動的にインポートすることができます(対応ブラウザに注意)

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する時に名前は任意です
  • ひとつのファイルからexportできるのは一つだけです

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

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