【オウル先生の基礎講座】Vol.8「モジュールとパッケージ管理」

モジュールとパッケージ管理 – コードを整理して効率的に開発しよう!
こんにちは!オウル先生🦉とフォックン🦊です。
今日はプログラミングを学ぶ上で重要な「モジュールとパッケージ管理」について学んでいきましょう。プロジェクトが大きくなってくると、コードの整理や再利用がとても大切になってきます!
🦊フォックンの疑問タイム


モジュールとは何か?

モジュールの定義
モジュールとは、関連する機能をまとめて再利用可能にしたコードの単位のことです。


モジュール化のメリット
1. コードの再利用性
- 一度書いたコードを他の場所でも使える
- 開発効率が大幅に向上
2. 保守性の向上
- 機能ごとに分かれているので、修正が楽
- バグの特定が容易
3. 名前空間の管理
- 変数名の衝突を防げる
- より安全なコードが書ける
4. チーム開発の促進
- 役割分担がしやすい
- 複数人での開発が効率的
JavaScriptのモジュールシステム

ES6モジュール(ESM)
エクスポート(export)
// math.js - 数学関数をまとめたモジュール
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export const PI = 3.14159;
// デフォルトエクスポート
export default function multiply(a, b) {
return a * b;
}
インポート(import)
// main.js - メインファイル
import multiply, { add, subtract, PI } from './math.js';
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(PI); // 3.14159
console.log(multiply(3, 4)); // 12

様々なインポート方法
// 1. 特定の関数のみインポート
import { add, subtract } from './math.js';
// 2. 全てをまとめてインポート
import * as Math from './math.js';
// 3. デフォルトエクスポートのインポート
import multiply from './math.js';
// 4. 名前を変更してインポート
import { add as addition } from './math.js';
CommonJS(Node.jsで使用)
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = {
add,
subtract
};
// main.js
const { add, subtract } = require('./math');
パッケージマネージャーの基礎

パッケージマネージャーとは?
パッケージマネージャーは、外部のライブラリ(パッケージ)を:
- インストール
- アップデート
- 削除
- 依存関係の管理
を自動で行ってくれるツールです。


主要なパッケージマネージャー
1. npm(Node Package Manager)
- Node.jsに標準搭載
- 最も普及している
2. yarn
- Facebookが開発
- 高速で安全
3. pnpm
- 省ディスク容量
- 高速インストール
npmの使い方
npmの基本コマンド

1. プロジェクトの初期化
npm init
# または
npm init -y # デフォルト設定で作成
これでpackage.json
ファイルが作成されます。
2. パッケージのインストール
# ローカルインストール(プロジェクト内のみ)
npm install パッケージ名
# グローバルインストール(システム全体)
npm install -g パッケージ名
# 開発依存関係としてインストール
npm install --save-dev パッケージ名
3. よく使うパッケージの例
# 人気のライブラリたち
npm install lodash # ユーティリティ関数集
npm install axios # HTTP通信
npm install moment # 日付操作
npm install express # Webサーバー
package.jsonの理解
{
"name": "my-project",
"version": "1.0.0",
"description": "フォックンの練習プロジェト",
"main": "index.js",
"scripts": {
"start": "node index.js",
"test": "jest",
"build": "webpack"
},
"dependencies": {
"lodash": "^4.17.21",
"axios": "^0.24.0"
},
"devDependencies": {
"jest": "^27.0.0",
"webpack": "^5.0.0"
}
}

実践例:lodashを使ってみよう
# lodashをインストール
npm install lodash
// main.js
import _ from 'lodash';
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 配列から偶数のみを取得
const evenNumbers = _.filter(numbers, n => n % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]
// 配列の要素を2倍に
const doubled = _.map(numbers, n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// 配列をグループ化
const words = ['apple', 'banana', 'apricot', 'blueberry'];
const grouped = _.groupBy(words, word => word[0]);
console.log(grouped);
// { a: ['apple', 'apricot'], b: ['banana', 'blueberry'] }
yarnについて

yarnの特徴
1. 高速インストール
- 並列処理でパッケージを取得
- キャッシュ機能でさらに高速化
2. セキュリティ
- yarn.lockファイルで依存関係を固定
- パッケージの整合性チェック
3. オフライン対応
- 一度ダウンロードしたパッケージはオフラインでも利用可能
yarnの基本コマンド
# プロジェクト初期化
yarn init
# パッケージインストール
yarn add パッケージ名
# 開発依存関係として追加
yarn add --dev パッケージ名
# 全パッケージインストール
yarn install
# パッケージ削除
yarn remove パッケージ名
# スクリプト実行
yarn start
yarn test


実践的なプロジェクト構成

プロジェクト構成例
my-web-app/
├── package.json
├── package-lock.json
├── src/
│ ├── index.js # メインファイル
│ ├── components/ # UIコンポーネント
│ │ ├── Header.js
│ │ ├── Footer.js
│ │ └── Button.js
│ ├── utils/ # ユーティリティ関数
│ │ ├── api.js
│ │ ├── helpers.js
│ │ └── constants.js
│ ├── services/ # ビジネスロジック
│ │ ├── userService.js
│ │ └── dataService.js
│ └── styles/ # スタイルファイル
│ ├── main.css
│ └── components.css
├── tests/ # テストファイル
│ └── utils.test.js
└── dist/ # ビルド結果
└── bundle.js
実践例:Todoアプリの構成
1. ユーティリティモジュール(utils/helpers.js)
// src/utils/helpers.js
export function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
export function formatDate(date) {
return new Intl.DateTimeFormat('ja-JP').format(date);
}
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
2. データサービス(services/todoService.js)
// src/services/todoService.js
import { generateId, formatDate } from '../utils/helpers.js';
class TodoService {
constructor() {
this.todos = this.loadTodos();
}
addTodo(text) {
const todo = {
id: generateId(),
text,
completed: false,
createdAt: formatDate(new Date())
};
this.todos.push(todo);
this.saveTodos();
return todo;
}
toggleTodo(id) {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.saveTodos();
}
return todo;
}
deleteTodo(id) {
this.todos = this.todos.filter(t => t.id !== id);
this.saveTodos();
}
getAllTodos() {
return this.todos;
}
loadTodos() {
const saved = localStorage.getItem('todos');
return saved ? JSON.parse(saved) : [];
}
saveTodos() {
localStorage.setItem('todos', JSON.stringify(this.todos));
}
}
export default new TodoService();
3. UIコンポーネント(components/TodoApp.js)
// src/components/TodoApp.js
import todoService from '../services/todoService.js';
export class TodoApp {
constructor(containerElement) {
this.container = containerElement;
this.render();
this.attachEventListeners();
}
render() {
const todos = todoService.getAllTodos();
this.container.innerHTML = `
<div class="todo-app">
<h1>📝 Todoアプリ</h1>
<div class="todo-input">
<input type="text" id="todoInput" placeholder="新しいタスクを入力...">
<button id="addBtn">追加</button>
</div>
<ul class="todo-list">
${todos.map(todo => this.renderTodoItem(todo)).join('')}
</ul>
</div>
`;
}
renderTodoItem(todo) {
return `
<li class="todo-item ${todo.completed ? 'completed' : ''}">
<input type="checkbox" ${todo.completed ? 'checked' : ''}
data-id="${todo.id}" class="todo-checkbox">
<span class="todo-text">${todo.text}</span>
<span class="todo-date">${todo.createdAt}</span>
<button class="delete-btn" data-id="${todo.id}">削除</button>
</li>
`;
}
attachEventListeners() {
// 追加ボタンのイベント
this.container.querySelector('#addBtn').addEventListener('click', () => {
this.addTodo();
});
// Enterキーでの追加
this.container.querySelector('#todoInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.addTodo();
}
});
// チェックボックスのイベント
this.container.addEventListener('change', (e) => {
if (e.target.classList.contains('todo-checkbox')) {
const id = e.target.dataset.id;
todoService.toggleTodo(id);
this.render();
}
});
// 削除ボタンのイベント
this.container.addEventListener('click', (e) => {
if (e.target.classList.contains('delete-btn')) {
const id = e.target.dataset.id;
todoService.deleteTodo(id);
this.render();
}
});
}
addTodo() {
const input = this.container.querySelector('#todoInput');
const text = input.value.trim();
if (text) {
todoService.addTodo(text);
input.value = '';
this.render();
}
}
}
4. メインファイル(index.js)
// src/index.js
import { TodoApp } from './components/TodoApp.js';
import './styles/main.css';
// アプリケーションの初期化
document.addEventListener('DOMContentLoaded', () => {
const appContainer = document.getElementById('app');
new TodoApp(appContainer);
});


よく使う便利なパッケージ
開発効率を上げるパッケージ
# 1. lodash - ユーティリティ関数の宝庫
npm install lodash
# 2. axios - HTTP通信をシンプルに
npm install axios
# 3. date-fns - 日付操作
npm install date-fns
# 4. uuid - ユニークIDの生成
npm install uuid
# 5. validator - バリデーション
npm install validator
開発ツール系パッケージ
# 開発依存関係として追加
npm install --save-dev jest # テストフレームワーク
npm install --save-dev webpack # モジュールバンドラー
npm install --save-dev babel-core # ES6トランスパイラー
npm install --save-dev eslint # コード品質チェック
npm install --save-dev prettier # コードフォーマッター
使用例:axiosでAPI通信
// src/services/apiService.js
import axios from 'axios';
const API_BASE_URL = 'https://jsonplaceholder.typicode.com';
export class ApiService {
static async getUsers() {
try {
const response = await axios.get(`${API_BASE_URL}/users`);
return response.data;
} catch (error) {
console.error('ユーザー情報の取得に失敗しました:', error);
throw error;
}
}
static async getUserPosts(userId) {
try {
const response = await axios.get(`${API_BASE_URL}/users/${userId}/posts`);
return response.data;
} catch (error) {
console.error('投稿の取得に失敗しました:', error);
throw error;
}
}
static async createPost(postData) {
try {
const response = await axios.post(`${API_BASE_URL}/posts`, postData);
return response.data;
} catch (error) {
console.error('投稿の作成に失敗しました:', error);
throw error;
}
}
}
ベストプラクティス

1. 単一責任の原則
// ❌ 悪い例:一つのファイルに複数の責任
// userUtils.js
export function validateUser(user) { /* バリデーション */ }
export function saveUser(user) { /* データベース保存 */ }
export function sendWelcomeEmail(user) { /* メール送信 */ }
// ✅ 良い例:責任を分離
// userValidator.js
export function validateUser(user) { /* バリデーション */ }
// userRepository.js
export function saveUser(user) { /* データベース保存 */ }
// emailService.js
export function sendWelcomeEmail(user) { /* メール送信 */ }
2. 明確な命名規則
// ✅ 分かりやすいファイル名とエクスポート名
// components/UserProfile.js
export class UserProfile { }
// services/AuthenticationService.js
export class AuthenticationService { }
// utils/dateHelpers.js
export function formatDate() { }
export function parseDate() { }
3. 依存関係の最小化
// ❌ 不要な依存関係
import _ from 'lodash'; // 全体をインポート
// ✅ 必要な部分のみインポート
import { map, filter } from 'lodash';
// または
import map from 'lodash/map';
import filter from 'lodash/filter';
4. 循環参照の回避
// ❌ 循環参照が発生
// fileA.js
import { functionB } from './fileB.js';
// fileB.js
import { functionA } from './fileA.js'; // 循環参照!
// ✅ 共通の依存関係を別ファイルに
// shared.js
export function sharedFunction() { }
// fileA.js
import { sharedFunction } from './shared.js';
// fileB.js
import { sharedFunction } from './shared.js';
セキュリティの考慮事項

1. パッケージの信頼性チェック
# パッケージの詳細情報を確認
npm info パッケージ名
# セキュリティ監査
npm audit
# 脆弱性の自動修正
npm audit fix
2. 定期的なアップデート
# 古いパッケージをチェック
npm outdated
# パッケージのアップデート
npm update
# 特定のパッケージをアップデート
npm install パッケージ名@latest
3. package-lock.jsonの管理


// package-lock.jsonの一部(例)
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
}
}
}
トラブルシューティング
よくあるエラーと対処法
1. Module not foundエラー
Error: Cannot find module './myModule.js'
対処法:
- ファイルパスを確認
- ファイル拡張子を確認
- 相対パスと絶対パスを確認
2. パッケージインストールエラー
npm ERR! 404 Not Found
対処法:
- パッケージ名のスペルチェック
- npmレジストリの確認
- インターネット接続の確認
3. 権限エラー
npm ERR! Error: EACCES: permission denied
対処法:
- sudoを使わない(セキュリティリスク)
- nvmを使用してNode.jsを管理
- npmの設定を変更
🎯 実践課題

課題1:基本的なモジュール作成
以下の機能を持つモジュールを作成してください:
// 課題:mathUtils.js を作成
// - 四則演算関数(add, subtract, multiply, divide)
// - 平均値計算関数(average)
// - 最大値・最小値取得関数(max, min)
課題2:NPMパッケージの活用
# 以下のパッケージを使ったプロジェクトを作成
npm install lodash moment axios
// 課題:以下の機能を実装
// 1. lodashを使った配列操作
// 2. momentを使った日付フォーマット
// 3. axiosを使ったAPI通信
課題3:プロジェクト構成の実践
weather-app/
├── src/
│ ├── components/
│ ├── services/
│ ├── utils/
│ └── index.js
└── package.json

まとめ

📝 今日のポイント
1. モジュール化の重要性
- コードの再利用性向上
- 保守性の改善
- チーム開発の効率化
2. ES6モジュールの基本
export
でモジュールを公開import
でモジュールを読み込み- 名前付きエクスポートとデフォルトエクスポート
3. パッケージマネージャー
- npmとyarnの基本的な使い方
- package.jsonによる依存関係管理
- セキュリティの考慮事項
4. 実践的なプロジェクト構成
- 機能ごとのファイル分割
- 明確な命名規則
- 単一責任の原則
🚀 次のステップ


- テスト駆動開発(TDD) – 品質の高いコードを書く技術
- TypeScript – より安全で保守しやすいコード
- Webpack・Vite – モジュールバンドラーの活用
- フレームワーク – React、Vue.js、Angularなど
💡 オウル先生からのアドバイス
「モジュール化は最初は複雑に感じるかもしれませんが、プロジェクトが大きくなるにつれてその価値を実感できるはずです。小さなプロジェクトから始めて、徐々に慣れていきましょう!」
🦊 フォックンからのメッセージ
「今日も新しいことをたくさん学べました!モジュール化のおかげで、コードがすっきり整理できそうです。みなさんも一緒に頑張りましょう!」
📚 参考リンク
次回予告: Vol.9では「非同期処理とPromise」をテーマに、JavaScriptの非同期プログラミングについて深く学んでいきます。コールバック地獄からの解放と、modern JavaScriptの書き方をマスターしましょう!
🦉オウル先生と🦊フォックンの未経験からのプログラミング上達ガイドでした!
🎁 特典コンテンツ
モジュール設計チートシート

ファイル命名規則
命名規則例:
├── components/ # UIコンポーネント
│ ├── Button.js # PascalCase
│ ├── UserProfile.js # 複合語もPascalCase
│ └── index.js # エクスポート用
├── services/ # ビジネスロジック
│ ├── apiService.js # camelCase + Service
│ ├── authService.js
│ └── dataService.js
├── utils/ # ユーティリティ関数
│ ├── dateHelpers.js # camelCase + 機能名
│ ├── validators.js
│ └── constants.js
└── hooks/ # React Hooks(該当する場合)
├── useAuth.js # use + 機能名
└── useLocalStorage.js
エクスポートパターン集
// パターン1: 名前付きエクスポート(推奨)
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
export const PI = 3.14159;
// パターン2: まとめてエクスポート
function add(a, b) { return a + b; }
function subtract(a, b) { return a - b; }
const PI = 3.14159;
export { add, subtract, PI };
// パターン3: デフォルト + 名前付きエクスポート
class Calculator {
add(a, b) { return a + b; }
subtract(a, b) { return a - b; }
}
export default Calculator;
export { PI };
// パターン4: 再エクスポート(index.jsでよく使用)
export { Button } from './Button.js';
export { UserProfile } from './UserProfile.js';
export { default as Calculator } from './Calculator.js';
🔧 高度なモジュール技術

動的インポート(Dynamic Import)
// 条件付きでモジュールを読み込み
async function loadFeature(featureName) {
if (featureName === 'chart') {
const { Chart } = await import('./Chart.js');
return new Chart();
} else if (featureName === 'map') {
const { MapView } = await import('./MapView.js');
return new MapView();
}
}
// 使用例
document.getElementById('loadChart').addEventListener('click', async () => {
const chart = await loadFeature('chart');
chart.render();
});
モジュールの遅延読み込み
// LazyLoader.js - 大きなライブラリの遅延読み込み
class LazyLoader {
static async loadChartLibrary() {
if (!this.chartLib) {
// 大きなチャートライブラリを必要時のみ読み込み
this.chartLib = await import('https://cdn.jsdelivr.net/npm/chart.js');
}
return this.chartLib;
}
static async loadVideoPlayer() {
if (!this.videoLib) {
this.videoLib = await import('./VideoPlayer.js');
}
return this.videoLib;
}
}
// 使用例
async function showChart(data) {
const Chart = await LazyLoader.loadChartLibrary();
const chart = new Chart.default(document.getElementById('chart'), {
type: 'bar',
data: data
});
}
環境別設定モジュール
// config/index.js - 環境に応じた設定
const development = {
API_URL: 'http://localhost:3000/api',
DEBUG: true,
LOG_LEVEL: 'debug'
};
const production = {
API_URL: 'https://api.myapp.com',
DEBUG: false,
LOG_LEVEL: 'error'
};
const test = {
API_URL: 'http://localhost:3001/api',
DEBUG: true,
LOG_LEVEL: 'silent'
};
const env = process.env.NODE_ENV || 'development';
const configs = { development, production, test };
export default configs[env];
// 使用例
// api.js
import config from '../config/index.js';
export async function fetchData(endpoint) {
const response = await fetch(`${config.API_URL}${endpoint}`);
if (config.DEBUG) {
console.log('API Response:', response);
}
return response.json();
}
📊 モジュール性能最適化
バンドルサイズの最適化

// ❌ 悪い例:ライブラリ全体をインポート
import _ from 'lodash'; // 全体(~70KB)
import moment from 'moment'; // 全体(~67KB)
// ✅ 良い例:必要な部分のみインポート
import debounce from 'lodash/debounce'; // 必要な部分のみ(~2KB)
import format from 'date-fns/format'; // 軽量な代替ライブラリ(~2KB)
// または、ES6のdestructuring import
import { debounce, throttle } from 'lodash';
Tree Shakingの活用
// utils/index.js - Tree Shakingに対応したエクスポート
export { formatDate } from './dateUtils.js';
export { validateEmail } from './validators.js';
export { debounce } from './performanceUtils.js';
export { generateId } from './idUtils.js';
// main.js - 使用する関数のみインポート
import { formatDate, validateEmail } from './utils/index.js';
// 使用されないdebounce, generateIdは最終バンドルから除外される
コード分割の実践
// routes/index.js - ページごとの動的インポート
const routes = {
'/': () => import('../pages/Home.js'),
'/about': () => import('../pages/About.js'),
'/contact': () => import('../pages/Contact.js'),
'/admin': () => import('../pages/Admin.js') // 管理者のみアクセス
};
class Router {
async navigate(path) {
const loadPage = routes[path];
if (loadPage) {
const module = await loadPage();
const Page = module.default;
this.renderPage(new Page());
}
}
renderPage(page) {
document.getElementById('app').innerHTML = '';
page.render(document.getElementById('app'));
}
}
🧪 テストしやすいモジュール設計

依存性注入パターン
// services/EmailService.js - 依存関係を外部から注入
export class EmailService {
constructor(httpClient, logger) {
this.httpClient = httpClient;
this.logger = logger;
}
async sendEmail(to, subject, body) {
try {
const response = await this.httpClient.post('/send-email', {
to, subject, body
});
this.logger.info(`Email sent to ${to}`);
return response.data;
} catch (error) {
this.logger.error(`Failed to send email: ${error.message}`);
throw error;
}
}
}
// 実際の使用時
import { EmailService } from './services/EmailService.js';
import { HttpClient } from './utils/HttpClient.js';
import { Logger } from './utils/Logger.js';
const emailService = new EmailService(
new HttpClient(),
new Logger()
);
// テスト時
import { EmailService } from './services/EmailService.js';
const mockHttpClient = {
post: jest.fn().mockResolvedValue({ data: { success: true } })
};
const mockLogger = {
info: jest.fn(),
error: jest.fn()
};
const emailService = new EmailService(mockHttpClient, mockLogger);
ピュア関数によるモジュール設計
// utils/calculations.js - 副作用のないピュア関数
export function calculateTax(price, taxRate = 0.1) {
if (typeof price !== 'number' || price < 0) {
throw new Error('Price must be a positive number');
}
if (typeof taxRate !== 'number' || taxRate < 0) {
throw new Error('Tax rate must be a positive number');
}
return Math.round((price * taxRate) * 100) / 100;
}
export function calculateTotal(items) {
if (!Array.isArray(items)) {
throw new Error('Items must be an array');
}
return items.reduce((total, item) => {
const tax = calculateTax(item.price, item.taxRate);
return total + item.price + tax;
}, 0);
}
// テストが簡単
import { calculateTax, calculateTotal } from './calculations.js';
describe('calculateTax', () => {
test('正しく税金を計算する', () => {
expect(calculateTax(100, 0.1)).toBe(10);
expect(calculateTax(200, 0.08)).toBe(16);
});
test('不正な入力に対してエラーを投げる', () => {
expect(() => calculateTax(-100)).toThrow();
expect(() => calculateTax('100')).toThrow();
});
});
🚀 実戦的なプロジェクト例


ECサイトのモジュール構成例
ecommerce-app/
├── package.json
├── webpack.config.js
├── src/
│ ├── index.js # エントリーポイント
│ ├── App.js # メインアプリケーション
│ ├── components/ # UIコンポーネント
│ │ ├── common/ # 共通コンポーネント
│ │ │ ├── Header.js
│ │ │ ├── Footer.js
│ │ │ ├── Loading.js
│ │ │ └── Modal.js
│ │ ├── product/ # 商品関連
│ │ │ ├── ProductCard.js
│ │ │ ├── ProductList.js
│ │ │ ├── ProductDetail.js
│ │ │ └── ProductFilter.js
│ │ └── cart/ # カート関連
│ │ ├── CartItem.js
│ │ ├── CartSummary.js
│ │ └── Checkout.js
│ ├── services/ # ビジネスロジック
│ │ ├── api/ # API通信
│ │ │ ├── productApi.js
│ │ │ ├── userApi.js
│ │ │ └── orderApi.js
│ │ ├── cart/ # カート管理
│ │ │ ├── CartService.js
│ │ │ └── CartStorage.js
│ │ └── auth/ # 認証
│ │ ├── AuthService.js
│ │ └── TokenManager.js
│ ├── utils/ # ユーティリティ
│ │ ├── formatters.js # フォーマット関数
│ │ ├── validators.js # バリデーション
│ │ ├── constants.js # 定数
│ │ └── helpers.js # ヘルパー関数
│ ├── hooks/ # カスタムフック
│ │ ├── useAuth.js
│ │ ├── useCart.js
│ │ └── useProducts.js
│ ├── store/ # 状態管理
│ │ ├── index.js
│ │ ├── userStore.js
│ │ ├── productStore.js
│ │ └── cartStore.js
│ └── styles/ # スタイル
│ ├── globals.css
│ ├── components.css
│ └── themes.css
└── tests/ # テスト
├── components/
├── services/
└── utils/
商品サービスの実装例
// services/api/productApi.js
import { HttpClient } from '../../utils/HttpClient.js';
import { API_ENDPOINTS } from '../../utils/constants.js';
class ProductApi {
constructor(httpClient) {
this.http = httpClient;
}
async getProducts(filters = {}) {
const params = new URLSearchParams(filters);
const response = await this.http.get(`${API_ENDPOINTS.PRODUCTS}?${params}`);
return response.data;
}
async getProduct(id) {
const response = await this.http.get(`${API_ENDPOINTS.PRODUCTS}/${id}`);
return response.data;
}
async searchProducts(query) {
const response = await this.http.get(`${API_ENDPOINTS.SEARCH}?q=${encodeURIComponent(query)}`);
return response.data;
}
async getRecommendations(productId) {
const response = await this.http.get(`${API_ENDPOINTS.PRODUCTS}/${productId}/recommendations`);
return response.data;
}
}
export default new ProductApi(new HttpClient());
// services/cart/CartService.js
import { EventEmitter } from '../../utils/EventEmitter.js';
import { CartStorage } from './CartStorage.js';
import { formatPrice } from '../../utils/formatters.js';
export class CartService extends EventEmitter {
constructor(storage) {
super();
this.storage = storage;
this.items = this.storage.load();
}
addItem(product, quantity = 1) {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({
id: product.id,
name: product.name,
price: product.price,
image: product.image,
quantity
});
}
this.storage.save(this.items);
this.emit('itemAdded', { product, quantity });
this.emit('cartUpdated', this.getCartSummary());
}
removeItem(productId) {
const index = this.items.findIndex(item => item.id === productId);
if (index !== -1) {
const removedItem = this.items.splice(index, 1)[0];
this.storage.save(this.items);
this.emit('itemRemoved', removedItem);
this.emit('cartUpdated', this.getCartSummary());
}
}
updateQuantity(productId, quantity) {
const item = this.items.find(item => item.id === productId);
if (item) {
if (quantity <= 0) {
this.removeItem(productId);
} else {
item.quantity = quantity;
this.storage.save(this.items);
this.emit('quantityUpdated', { productId, quantity });
this.emit('cartUpdated', this.getCartSummary());
}
}
}
getItems() {
return [...this.items];
}
getCartSummary() {
const itemCount = this.items.reduce((total, item) => total + item.quantity, 0);
const subtotal = this.items.reduce((total, item) => total + (item.price * item.quantity), 0);
const tax = subtotal * 0.1; // 10%税金
const total = subtotal + tax;
return {
itemCount,
subtotal: formatPrice(subtotal),
tax: formatPrice(tax),
total: formatPrice(total),
items: this.getItems()
};
}
clear() {
this.items = [];
this.storage.clear();
this.emit('cartCleared');
this.emit('cartUpdated', this.getCartSummary());
}
}
// components/product/ProductCard.js
import { CartService } from '../../services/cart/CartService.js';
import { formatPrice } from '../../utils/formatters.js';
export class ProductCard {
constructor(product, container) {
this.product = product;
this.container = container;
this.cartService = CartService;
this.render();
this.attachEventListeners();
}
render() {
this.container.innerHTML = `
<div class="product-card" data-product-id="${this.product.id}">
<div class="product-image">
<img src="${this.product.image}" alt="${this.product.name}" loading="lazy">
${this.product.discount ? `<span class="discount-badge">${this.product.discount}% OFF</span>` : ''}
</div>
<div class="product-info">
<h3 class="product-name">${this.product.name}</h3>
<p class="product-description">${this.product.description}</p>
<div class="product-rating">
${this.renderStars(this.product.rating)}
<span class="rating-text">(${this.product.reviewCount})</span>
</div>
<div class="product-price">
${this.product.originalPrice && this.product.originalPrice !== this.product.price ?
`<span class="original-price">${formatPrice(this.product.originalPrice)}</span>` : ''
}
<span class="current-price">${formatPrice(this.product.price)}</span>
</div>
<div class="product-actions">
<button class="btn-primary add-to-cart" data-product-id="${this.product.id}">
🛒 カートに追加
</button>
<button class="btn-secondary view-details" data-product-id="${this.product.id}">
詳細を見る
</button>
</div>
</div>
</div>
`;
}
renderStars(rating) {
const fullStars = Math.floor(rating);
const hasHalfStar = rating % 1 !== 0;
const emptyStars = 5 - Math.ceil(rating);
return [
'★'.repeat(fullStars),
hasHalfStar ? '☆' : '',
'☆'.repeat(emptyStars)
].join('');
}
attachEventListeners() {
const addToCartBtn = this.container.querySelector('.add-to-cart');
const viewDetailsBtn = this.container.querySelector('.view-details');
addToCartBtn.addEventListener('click', () => {
this.addToCart();
});
viewDetailsBtn.addEventListener('click', () => {
this.viewDetails();
});
}
addToCart() {
this.cartService.addItem(this.product);
// 視覚的フィードバック
const button = this.container.querySelector('.add-to-cart');
const originalText = button.textContent;
button.textContent = '✓ 追加されました';
button.disabled = true;
setTimeout(() => {
button.textContent = originalText;
button.disabled = false;
}, 2000);
}
viewDetails() {
// カスタムイベントを発火して、親コンポーネントに詳細表示を依頼
const event = new CustomEvent('productDetailRequested', {
detail: { productId: this.product.id }
});
document.dispatchEvent(event);
}
}


🎓 卒業課題:個人プロジェクト構築

課題:タスク管理アプリケーションの作成
要件:
- モジュール構成
- 適切なディレクトリ構造
- 責任の分離
- 再利用可能なコンポーネント
- 使用技術
- ES6モジュール
- npm/yarnによるパッケージ管理
- 最低3つの外部ライブラリを活用
- 機能要件
- タスクの追加・編集・削除
- カテゴリ分け
- 期限設定
- 検索・フィルタリング
- データの永続化
- 推奨パッケージ
npm install lodash date-fns uuid npm install --save-dev jest webpack
評価ポイント:
- モジュール設計の適切さ
- コードの再利用性
- 命名規則の一貫性
- エラーハンドリング
- パフォーマンスの考慮


🌟 おわりに
プログラミングの世界では、「車輪の再発明をしない」という言葉があります。既存の優秀なライブラリやモジュールを活用することで、より効率的に、より高品質なアプリケーションを開発できます。
今日学んだモジュールとパッケージ管理の知識は、これからのプログラミング人生において強力な武器となるでしょう。小さな一歩から始めて、徐々にスキルを積み重ねていってください。


📚 この記事が役に立ったら、ブックマークやシェアをお忘れなく! 💬 質問やコメントもお待ちしています!