はじめに
前回の記事ではJavaScriptのエラーハンドリングとしてtry・catch・finallyの使い方からカスタムエラーの作り方・Promiseのエラー処理・ユーザーへのエラー表示パターンまで解説しました。今回はJavaScriptのモジュールシステムを解説します。「JavaScriptのコードが1つのファイルに集まって長くなってきた」「どこに何が書いてあるかわからなくなってきた」という悩みをモジュールシステムで解決できます。import と export を使ってコードをファイルに分割することで、管理しやすく・再利用しやすい構造が作れます。
1. モジュールとは何か
1.1 モジュールの概念
モジュールとは:
JavaScriptのコードをファイル単位に分割して
必要なものだけを読み込む仕組み。
モジュールを使わない場合の問題点:
❌ すべてのコードが1ファイルに集まる
→ 数百行・数千行になって管理しにくい
❌ 変数・関数がグローバルスコープに漏れる
→ 別のファイルの変数と名前が衝突する
❌ 読み込み順序に依存する
→ HTMLでscriptタグの順番を間違えるとエラー
❌ どこで何が定義されているかわからない
→ 関数を変更するとどこに影響するか追えない
✅ モジュールを使うメリット:
→ ファイルごとに責務が明確になる
→ 変数・関数のスコープがファイル内に閉じる
→ 必要なものだけimportして使える
→ 再利用しやすいコードが書ける
→ 依存関係が明確になる
1.2 ES Modulesとは
ES Modules(ESM):
ECMAScript 2015(ES6)で標準化された
JavaScriptの公式モジュールシステム。
→ import / export を使う
→ モダンブラウザはすべて対応済み
→ Node.js(v12以降)でも使える
→ Vite・webpack などのバンドラーとも相性がよい
以前は CommonJS(CJS)が主流だった:
→ require() / module.exports を使う
→ Node.jsで長く使われてきた書き方
→ 現在もWordPress・古いNode.jsプロジェクトで使われる
この記事ではES Modulesを解説する。
2. exportの使い方
2.1 named export(名前付きエクスポート)
// utils/math.js
// 関数をexportする
export function add( a, b ) {
return a + b;
}
export function subtract( a, b ) {
return a - b;
}
export function multiply( a, b ) {
return a * b;
}
export function divide( a, b ) {
if ( b === 0 ) throw new RangeError( '0で割ることはできません' );
return a / b;
}
// 変数をexportする
export const PI = 3.14159265358979;
export const E = 2.71828182845904;
export const VERSION = '1.0.0';
// 定義してからまとめてexportすることもできる
function clamp( value, min, max ) {
return Math.min( Math.max( value, min ), max );
}
function lerp( start, end, t ) {
return start + ( end - start ) * t;
}
export { clamp, lerp };
// exportしながら名前を変えることもできる(as)
export { clamp as clampValue, lerp as linearInterpolate };
2.2 default export(デフォルトエクスポート)
// utils/formatter.js
// default export:ファイルにつき1つだけ
export default class Formatter {
constructor( locale = 'ja-JP' ) {
this.locale = locale;
}
currency( value, currency = 'JPY' ) {
return new Intl.NumberFormat( this.locale, {
style: 'currency',
currency,
} ).format( value );
}
date( date, options = {} ) {
return new Intl.DateTimeFormat( this.locale, options )
.format( new Date( date ) );
}
number( value ) {
return new Intl.NumberFormat( this.locale ).format( value );
}
}
// ============================================
// 関数をdefault exportする
// components/Button.js
export default function createButton( { label, onClick, variant = 'primary' } ) {
const btn = document.createElement( 'button' );
btn.className = `btn btn--${ variant }`;
btn.textContent = label;
btn.addEventListener( 'click', onClick );
return btn;
}
// ============================================
// オブジェクトをdefault exportする
// config/api.js
export default {
baseUrl: 'https://api.example.com',
version: 'v1',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
};
2.3 named exportとdefault exportの使い分け
named export を使う場面:
→ 複数の関数・変数をエクスポートするとき
→ ユーティリティ関数集
→ 定数の集まり
→ 複数のクラスをまとめたファイル
例:
export function formatDate() { ... }
export function formatCurrency() { ... }
export const DATE_FORMAT = 'YYYY/MM/DD';
→ importするときに {} が必要
→ importする側が名前を指定する
default export を使う場面:
→ ファイルの主役が1つのとき
→ コンポーネント・クラス・設定オブジェクト
例:
export default class Modal { ... }
export default function App() { ... }
export default { baseUrl, apiKey };
→ importするときに {} が不要
→ importする側が名前を自由につけられる
3. importの使い方
3.1 named importの基本
// named export したものをimportする
// { } の中にエクスポート名を書く
import { add, subtract, PI } from './utils/math.js';
console.log( add( 3, 4 ) ); // 7
console.log( PI ); // 3.14159265358979
// ============================================
// 名前を変えてimportする(as)
import { add as addition, multiply as times } from './utils/math.js';
console.log( addition( 2, 3 ) ); // 5
console.log( times( 4, 5 ) ); // 20
// ============================================
// すべてをまとめてimportする(* as)
import * as MathUtils from './utils/math.js';
console.log( MathUtils.add( 1, 2 ) ); // 3
console.log( MathUtils.PI ); // 3.14159265358979
console.log( MathUtils.clamp( 15, 0, 10 ) ); // 10
3.2 default importの基本
// default export したものをimportする
// { } は不要・名前は自由につけられる
import Formatter from './utils/formatter.js';
const formatter = new Formatter( 'ja-JP' );
console.log( formatter.currency( 1980 ) ); // ¥1,980
console.log( formatter.number( 123456 ) ); // 123,456
// ============================================
// 名前は自由につけられる(同じファイルでもOK)
import MyFormatter from './utils/formatter.js'; // どんな名前でもOK
import createButton from './components/Button.js';
import apiConfig from './config/api.js';
// ============================================
// named と default を同時にimportする
import Carousel, { DIRECTION, DEFAULT_OPTIONS } from './components/Carousel.js';
const carousel = new Carousel( '#hero', { ...DEFAULT_OPTIONS, autoPlay: true } );
3.3 副作用のみのimport
// 何もexportしていないファイルをimportする
// → ファイルを実行するだけ(副作用のみ)
// スタイルシートの読み込み(バンドラー使用時)
import './styles/reset.css';
import './styles/main.scss';
// ポリフィルの適用
import './polyfills/intersection-observer.js';
// グローバル設定の実行
import './config/setup.js';
4. モジュールのファイル構成
4.1 実践的なファイル構成
src/
├── main.js # エントリーポイント
│
├── config/
│ ├── api.js # API設定
│ └── constants.js # 定数
│
├── utils/
│ ├── index.js # utils のまとめexport
│ ├── math.js # 数値計算ユーティリティ
│ ├── date.js # 日付ユーティリティ
│ ├── string.js # 文字列ユーティリティ
│ ├── dom.js # DOM操作ユーティリティ
│ └── validator.js # バリデーションユーティリティ
│
├── api/
│ ├── index.js # APIクライアントのまとめ
│ ├── client.js # fetchのラッパー
│ ├── posts.js # 投稿APIの処理
│ └── users.js # ユーザーAPIの処理
│
├── components/
│ ├── Modal.js # モーダル
│ ├── Toast.js # トースト通知
│ ├── Carousel.js # カルーセル
│ └── FormValidator.js # フォームバリデーター
│
└── pages/
├── home.js # トップページの処理
└── contact.js # お問い合わせページの処理
4.2 index.jsでまとめてexportする
// utils/index.js
// → ここからまとめてimportできるようにする
export { add, subtract, multiply, divide, clamp, lerp } from './math.js';
export { formatDate, formatRelative } from './date.js';
export { capitalize, truncate, slugify } from './string.js';
export { createElement, removeElement, toggleClass } from './dom.js';
export { isEmail, isPhone, isRequired } from './validator.js';
// ============================================
// 使う側ではutils/index.jsからまとめてimportできる
import { add, formatDate, capitalize, isEmail } from './utils/index.js';
// または utils/ だけでもindex.jsを探してくれる(バンドラー使用時)
import { add, formatDate } from './utils';
5. 実践的なモジュール設計パターン
5.1 APIクライアントの設計
// api/client.js
const BASE_URL = 'https://api.example.com/v1';
const DEFAULT_TIMEOUT = 10000;
class ApiError extends Error {
constructor( status, message, data = null ) {
super( message );
this.name = 'ApiError';
this.status = status;
this.data = data;
}
}
async function request( endpoint, options = {} ) {
const {
method = 'GET',
body = null,
headers = {},
timeout = DEFAULT_TIMEOUT,
} = options;
const controller = new AbortController();
const timerId = setTimeout( () => controller.abort(), timeout );
try {
const response = await fetch( `${ BASE_URL }${ endpoint }`, {
method,
headers: {
'Content-Type': 'application/json',
...headers,
},
body: body ? JSON.stringify( body ) : null,
signal: controller.signal,
} );
clearTimeout( timerId );
if ( !response.ok ) {
const errorData = await response.json().catch( () => null );
throw new ApiError(
response.status,
`HTTP ${ response.status }`,
errorData
);
}
return response.json();
} catch ( error ) {
clearTimeout( timerId );
if ( error.name === 'AbortError' ) {
throw new ApiError( 408, 'リクエストがタイムアウトしました' );
}
throw error;
}
}
export { ApiError };
export const get = ( endpoint, options ) => request( endpoint, { ...options, method: 'GET' } );
export const post = ( endpoint, body, options ) => request( endpoint, { ...options, method: 'POST', body } );
export const put = ( endpoint, body, options ) => request( endpoint, { ...options, method: 'PUT', body } );
export const del = ( endpoint, options ) => request( endpoint, { ...options, method: 'DELETE' } );
export default { get, post, put, del };
// api/posts.js
import { get, post, put, del } from './client.js';
export async function getPosts( { page = 1, limit = 10, category = '' } = {} ) {
const params = new URLSearchParams( { page, limit, category } );
return get( `/posts?${ params }` );
}
export async function getPost( id ) {
return get( `/posts/${ id }` );
}
export async function createPost( data ) {
return post( '/posts', data );
}
export async function updatePost( id, data ) {
return put( `/posts/${ id }`, data );
}
export async function deletePost( id ) {
return del( `/posts/${ id }` );
}
5.2 ユーティリティのモジュール化
// utils/dom.js
/**
* CSSセレクターで要素を取得する(型安全版)
*/
export function $( selector, parent = document ) {
return parent.querySelector( selector );
}
export function $$( selector, parent = document ) {
return [ ...parent.querySelectorAll( selector ) ];
}
/**
* 要素を作成する
*/
export function createElement( tag, { className, text, html, attrs, children } = {} ) {
const el = document.createElement( tag );
if ( className ) el.className = className;
if ( text ) el.textContent = text;
if ( html ) el.innerHTML = html;
if ( attrs ) {
Object.entries( attrs ).forEach( ( [ key, val ] ) => {
el.setAttribute( key, val );
} );
}
if ( children ) {
children.forEach( ( child ) => el.appendChild( child ) );
}
return el;
}
/**
* イベントリスナーをまとめて設定する
*/
export function on( el, events, handler, options ) {
events.split( ' ' ).forEach( ( event ) => {
el.addEventListener( event.trim(), handler, options );
} );
// クリーンアップ関数を返す
return () => {
events.split( ' ' ).forEach( ( event ) => {
el.removeEventListener( event.trim(), handler );
} );
};
}
/**
* 要素が画面内に入ったら処理する
*/
export function onVisible( el, callback, options = { threshold: 0.1 } ) {
const observer = new IntersectionObserver( ( entries ) => {
entries.forEach( ( entry ) => {
if ( entry.isIntersecting ) {
callback( entry.target );
observer.unobserve( entry.target );
}
} );
}, options );
observer.observe( el );
return () => observer.disconnect();
}
// utils/validator.js
export const isRequired = ( value ) =>
value !== null && value !== undefined && String( value ).trim() !== '';
export const isEmail = ( value ) =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test( value );
export const isPhone = ( value ) =>
/^0\d{9,10}$/.test( value.replace( /-/g, '' ) );
export const isUrl = ( value ) => {
try {
new URL( value );
return true;
} catch {
return false;
}
};
export const minLength = ( min ) => ( value ) => String( value ).length >= min;
export const maxLength = ( max ) => ( value ) => String( value ).length <= max;
export const min = ( minimum ) => ( value ) => Number( value ) >= minimum;
export const max = ( maximum ) => ( value ) => Number( value ) <= maximum;
/**
* バリデーションルールをまとめて実行する
*/
export function validate( value, rules ) {
for ( const [ ruleFn, message ] of rules ) {
if ( !ruleFn( value ) ) return { valid: false, message };
}
return { valid: true, message: null };
}
// 使用例
// const result = validate( email, [
// [ isRequired, '必須項目です' ],
// [ isEmail, '正しいメールアドレスを入力してください' ],
// ] );
5.3 メインファイルでまとめる
// main.js(エントリーポイント)
// スタイルの読み込み
import './styles/main.scss';
// ユーティリティ
import { $$, onVisible } from './utils/dom.js';
import { isEmail } from './utils/validator.js';
// コンポーネント
import Modal from './components/Modal.js';
import Toast from './components/Toast.js';
import Carousel from './components/Carousel.js';
import FormValidator from './components/FormValidator.js';
// APIクライアント
import { getPosts } from './api/posts.js';
// ページごとの処理を初期化する
async function init() {
// トースト通知
const toast = new Toast();
window.toast = toast; // グローバルに公開(任意)
// カルーセルの初期化
$$( '.carousel' ).forEach( ( el ) => {
new Carousel( `#${ el.id }`, { autoPlay: true } );
} );
// モーダルの初期化
$$( '[data-modal]' ).forEach( ( btn ) => {
const modal = new Modal( `#${ btn.dataset.modal }` );
btn.addEventListener( 'click', () => modal.show() );
} );
// フェードインアニメーション
$$( '.fade-in' ).forEach( ( el ) => {
onVisible( el, ( target ) => {
target.classList.add( 'is-visible' );
} );
} );
// 投稿一覧の取得
const postList = document.querySelector( '#post-list' );
if ( postList ) {
try {
const posts = await getPosts( { limit: 6 } );
postList.innerHTML = posts
.map( ( post ) => `
<article class="card" data-id="${ post.id }">
<h2>${ post.title }</h2>
<p>${ post.body }</p>
</article>
` )
.join( '' );
} catch ( error ) {
console.error( '投稿の取得に失敗:', error );
toast.error( '記事の読み込みに失敗しました' );
}
}
}
// DOMが読み込まれたら実行する
document.addEventListener( 'DOMContentLoaded', init );
6. 動的インポート
6.1 import() で遅延読み込みする
/*
動的インポート:
import() 関数を使って
必要なタイミングでモジュールを読み込む。
メリット:
→ 初期読み込みを軽くできる(コード分割)
→ 条件によって読み込むものを変えられる
→ Promise を返すので async/await で使える
*/
// ボタンをクリックしたときだけモジュールを読み込む
const btn = document.querySelector( '#load-chart' );
btn.addEventListener( 'click', async () => {
try {
// クリックされたときに初めてChart.jsを読み込む
const { default: Chart } = await import( './components/Chart.js' );
const chart = new Chart( '#chart-canvas', {
type: 'line',
data: { /* ... */ },
} );
} catch ( error ) {
console.error( 'チャートの読み込みに失敗:', error );
}
} );
// ============================================
// 言語に応じてモジュールを切り替える
async function loadTranslations( lang ) {
try {
const { default: translations } = await import( `./locales/${ lang }.js` );
return translations;
} catch {
// フォールバック:日本語を読み込む
const { default: ja } = await import( './locales/ja.js' );
return ja;
}
}
const lang = navigator.language.startsWith( 'en' ) ? 'en' : 'ja';
const t = await loadTranslations( lang );
console.log( t.greeting ); // 'こんにちは' or 'Hello'
7. CommonJS との比較
7.1 ES Modules vs CommonJS
// ============================================
// CommonJS(Node.js・古い書き方)
// ============================================
// エクスポート
// math.js
function add( a, b ) { return a + b; }
module.exports = { add };
// または
module.exports.add = function( a, b ) { return a + b; };
// デフォルトエクスポート
module.exports = class Formatter { ... };
// インポート
const { add } = require( './math.js' );
const Formatter = require( './utils/formatter.js' );
const path = require( 'path' ); // Node.js組み込みモジュール
// ============================================
// ES Modules(モダンな書き方)
// ============================================
// エクスポート
export function add( a, b ) { return a + b; }
export default class Formatter { ... }
// インポート
import { add } from './math.js';
import Formatter from './utils/formatter.js';
// ============================================
// WordPressのJavaScriptでよく見る CommonJS
// ============================================
jQuery( function( $ ) {
// jQueryのコード
const utils = require( './utils.js' ); // webpack使用時
} );
8. ブラウザでのES Modules
8.1 HTMLでtype=”module”を使う
<!-- type="module" を指定するとES Modulesが使える -->
<script type="module" src="./src/main.js"></script>
<!--
type="module" の特徴:
→ import / export が使える
→ 自動的にdeferされる(DOMContentLoadedを待つ)
→ 厳格モード('use strict')が自動で適用される
→ モジュールスコープになる(グローバルに漏れない)
→ 同じURLのモジュールは1回しか読み込まれない
-->
<!-- 古いブラウザ向けのフォールバック -->
<script nomodule src="./dist/bundle.js"></script>
9. 実践チェックリスト
✓ export と import を使ってコードをファイルに分割できているか
✓ named export と default export の使い分けができているか
✓ utils/index.js でまとめてエクスポートして import 元を一本化しているか
✓ APIクライアントを独立したモジュールとして設計できているか
✓ コンポーネント・ユーティリティ・API・設定などをフォルダで整理しているか
✓ 動的インポート import() で必要なタイミングだけモジュールを読み込む工夫をしているか
✓ <script type="module"> でブラウザでもES Modulesを使えているか
✓ Viteなどのバンドラーを使う場合に .js 拡張子の省略やエイリアスを活用しているか
まとめ
今回はJavaScriptのモジュールシステムとして import と export の使い方・ファイル分割の方法を解説しました。ポイントをまとめると:
- モジュールを使うとコードをファイル単位に分割でき・変数のスコープがファイル内に閉じて名前の衝突を防げる
- named export は
export function/export constの形で複数エクスポートできて・import側は{ }を使う - default export はファイルにつき1つで
export defaultの形でエクスポートして・import側は{ }なしで自由な名前をつけられる utils/index.jsでまとめてエクスポートすると複数のユーティリティを1行でimportできて便利- APIクライアント・コンポーネント・ユーティリティ・定数をフォルダで整理するとコードの見通しがよくなる
- 動的インポート
import()を使うと必要なタイミングだけモジュールを読み込めて初期ロードを軽くできる - ブラウザでは
<script type="module">を指定するだけでES Modulesが使える・自動でdeferされDOMContentLoadedを待つ - Viteなどのバンドラーを使う場合は拡張子の省略・パスエイリアス・ツリーシェイキングなども活用できる
次の記事では、Web制作の効率を大きく上げるVSCodeの便利な設定と拡張機能を紹介します。毎日使うエディタを整えることでコーディングのスピードと快適さが格段に上がる方法をお届けします。お楽しみに!
コメント