JavaScriptの初期段階では、クラス、パッケージ、モジュールといった概念が存在しませんでした。そのため、大規模な開発が進むにつれて、変数の競合や依存関係の管理、コードの組織化が困難になるという課題が浮き彫りになりました。これらの問題を解決するために、フロントエンドのモジュール化という概念が誕生しました。
モジュール化の目的とメリット
モジュール化とは、複雑なプログラムを独立した機能単位(ファイル)に分割し、それぞれを組み合わせて利用する手法を指します。主な利点は以下の4点です。
- 名前空間の分離による変数汚染や競合の回避
- コードの再利用性の向上
- 保守・メンテナンスの効率化
- モジュール間の依存関係を明示的に管理
初期のモジュール化手法
関数によるカプセル化
JavaScriptのスコープは関数単位であるため、関連する処理を一つの関数にまとめるのが最初のステップでした。
function processUserData() {
// 処理ロジック
}
function fetchUserList() {
// 処理ロジック
}
しかし、これではグローバルな名前空間に関数が散らばり、他のスクリプトと名前が衝突するリスクが依然として残ります。
オブジェクトリテラルによる管理
関連するデータとメソッドを一つのオブジェクトにまとめる手法です。
const userModule = {
cache: [],
addUser(name) {
this.cache.push(name);
},
getUsers() {
return this.cache;
}
};
userModule.addUser("Tanaka");
外部からプロパティ(cacheなど)を直接書き換えられるリスクがあり、情報の隠蔽が不完全です。
即時実行関数式 (IIFE)
クロージャを利用して、内部の実装を隠蔽し、外部に公開したいインターフェースのみを返す手法です。
const storageService = (function() {
let _dataStore = []; // プライベート変数
function save(item) {
_dataStore.push(item);
}
function getAll() {
return [..._dataStore];
}
return {
save: save,
list: getAll
};
})();
これにより、外部から内部変数を直接操作されることを防げますが、依存関係の解決は依然として手動(scriptタグの順番管理)に依存していました。
主要なモジュール仕様とエコシステム
CommonJS
サーバーサイドJavaScriptであるNode.jsで採用されている仕様です。同期的にモジュールを読み込むのが特徴です。
// logger.js
const prefix = "[INFO]";
function log(msg) {
console.log(`${prefix} ${msg}`);
}
module.exports = { log };
// app.js
const { log } = require('./logger');
log("CommonJSのテスト");
ブラウザ環境では同期読み込みがパフォーマンス低下の原因となるため、Browserifyなどのツールを用いてビルドする必要があります。
AMD (Asynchronous Module Definition)
ブラウザ環境向けに設計された非同期読み込み仕様です。RequireJSがその代表的な実装です。
// 定義
define('mathTool', ['dependency'], function(dep) {
return {
calc: function(a, b) {
return a + b;
}
};
});
// 利用
require(['mathTool'], function(math) {
console.log(math.calc(10, 20));
});
CMD (Common Module Definition)
SeaJSによって提唱された仕様で、「依存関係の近接記述」を推奨しています。AMDが依存関係を前置するのに対し、CMDは必要な場所で読み込みます。
define(function(require, exports, module) {
const utils = require('./utils'); // 必要なタイミングでロード
utils.doSomething();
exports.run = function() {
console.log("Running...");
};
});
UMD (Universal Module Definition)
AMDとCommonJS、そして従来のグローバル変数定義のすべてをサポートするためのパターンです。多くのライブラリ(Vue.jsなど)がこの形式で配布されています。
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['jquery'], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('jquery'));
} else {
root.MyLibrary = factory(root.jQuery);
}
}(typeof self !== 'undefined' ? self : this, function ($) {
return {
version: '1.0.0'
};
}));
ES Modules (Native JS Modules)
ES6から導入された言語標準のモジュール仕様です。静的な解析が可能で、現在ではモダンブラウザやNode.js(最新版)で標準的に利用されます。
// calculator.mjs
export const PI = 3.1415;
export function area(radius) {
return PI * radius * radius;
}
// main.mjs
import { area } from './calculator.mjs';
console.log(area(5));
ブラウザで直接読み込む場合は、<script type="module">を使用します。古い環境に対応させる場合は、BabelやWebpackなどのトランスパイラ・バンドラを使用するのが一般的です。
パッケージマネージャー (npm)
npm (Node Package Manager) は、世界最大のソフトウェアレジストリであり、モジュールの配布と依存関係管理の中心地です。
package.json: プロジェクトの情報や依存パッケージのリストを管理node_modules: インストールされたモジュールの実体
# モジュールのインストール例
npm install axios --save
実践例:UMD形式のユーティリティ作成
以下は、CommonJS、AMD、ブラウザ環境のすべてに対応したフィボナッチ数列計算モジュールの例です。
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof module === 'object' && module.exports) {
module.exports = factory();
} else {
global.FibTool = factory();
}
}(this, function () {
return {
getVal: function (n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
let temp = a + b;
a = b;
b = temp;
}
return b;
}
};
}));