🫠

#6 全体の構造整理をやってみる

に公開

🏷️ 今回の位置づけ

これまでの記事(#1〜#5)で作成したインターバルタイマーをさらに進化させ、コードの可読性や保守性を高めるために、JavaScriptのモジュール分割を行いました。


✍️ この記事の内容

✅ なぜモジュール化したのか
✅ どのようにファイルを分けたのか
✅ コード全体の仕組み
✅ 次回予告


🔎 現在の構成

以下はモジュール化する前の、すべて1ファイルで管理していたコードです。
Zennの折りたたみ機能で確認できます。

📄 index.html

折りたたむ
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>タイマー</title>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <div class="container">
    <h1>インターバルタイマー</h1>
    <div class="input-group">
      <label for="work-minutes">作業時間(分、例:25.5)</label>
      <input type="number" id="work-minutes" placeholder="例: 25.5" />
    </div>
    <div class="input-group">
      <label for="break-minutes">休憩時間(分、例:5)</label>
      <input type="number" id="break-minutes" placeholder="例: 5" />
    </div>
    <div id="timer-display">00:00</div>
    <div id="phase-label">作業中</div>
    <div class="button-group">
      <button id="start-btn">Start</button>
      <button id="stop-btn">Stop</button>
      <button id="reset-btn">Reset</button>
    </div>
  </div>
  <script src="index.js" type="module"></script>
</body>
</html>

🎨 style.css

折りたたむ

css```
コードをコピーする
body {
font-family: Arial, sans-serif;
text-align: center;
background-color: #f0f0f0;
}

.container {
max-width: 400px;
margin: 30px auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}

.input-group {
margin-bottom: 10px;
}

input {
width: 100%;
padding: 8px;
margin-top: 4px;
}

.button-group {
display: flex;
justify-content: center;
gap: 16px;
margin-top: 16px;
}

#timer-display {
font-size: 2em;
font-weight: bold;
margin-top: 10px;
}

#phase-label {
font-weight: bold;
color: #666;
}

⚙️ index.js

折りたたむ

js
コードをコピーする
// 各種ボタンと表示エリアの要素を取得
const startBtn = document.getElementById("start-btn");
const stopBtn = document.getElementById("stop-btn");
const resetBtn = document.getElementById("reset-btn");
const timerDisplay = document.getElementById("timer-display");
const phaseLabel = document.getElementById("phase-label");

// タイマー状態を管理する変数
let isRunning = false; // タイマーが動作中かどうか
let isWorkPhase = true; // 現在が作業時間かどうか(falseなら休憩時間)
let workDuration = 0; // 作業時間(秒)
let breakDuration = 0; // 休憩時間(秒)
let remainingTime = 0; // 残り時間(秒)
let timerInterval = null; // setIntervalのID(後でclear用)

// タイマーの開始処理
startBtn.addEventListener("click", () => {
if (!isRunning) {
const workMinutes = parseFloat(document.getElementById("work-minutes").value) || 0;
const breakMinutes = parseFloat(document.getElementById("break-minutes").value) || 0;

workDuration = Math.floor(workMinutes * 60);
breakDuration = Math.floor(breakMinutes * 60);

remainingTime = workDuration;
isWorkPhase = true;
isRunning = true;

startPhase();

}
});

// ストップボタンの処理
stopBtn.addEventListener("click", () => {
clearInterval(timerInterval);
isRunning = false;
});

// リセットボタンの処理
resetBtn.addEventListener("click", () => {
clearInterval(timerInterval);
isRunning = false;
isWorkPhase = true;
remainingTime = 0;
timerDisplay.textContent = "00:00";
phaseLabel.textContent = "作業中";
});

// フェーズ切り替え
function startPhase() {
phaseLabel.textContent = isWorkPhase ? "作業中" : "休憩中";
remainingTime = isWorkPhase ? workDuration : breakDuration;

timerInterval = setInterval(() => {
remainingTime--;
const minutes = String(Math.floor(remainingTime / 60)).padStart(2, "0");
const seconds = String(remainingTime % 60).padStart(2, "0");
timerDisplay.textContent = ${minutes}:${seconds};

if (remainingTime <= 0) {
  clearInterval(timerInterval);
  isWorkPhase = !isWorkPhase;
  startPhase();
}

}, 1000);
}


🎯 なぜモジュール化したのか?

理由 目的やメリット
見通しの良さ 1ファイルにすべて書くと長くなり、修正しづらい。
役割ごとにファイルを分割することで読みやすくした。
保守性の向上 新機能追加やバグ修正が必要になったとき、必要なファイルだけを見れば済むので効率的になる。
拡張しやすさ 今後の機能追加(音声通知や履歴保存など)でも、新しいモジュールを足すだけで済む。

📦 モジュール分割の構成

以下のように役割ごとにファイルを分けました。

ファイル名 役割
main.js アプリ起動処理。DOMContentLoadedでイベント登録を開始する。
events.js ユーザー操作(開始・停止・リセットボタン)を処理する。
timer.js タイマーのカウントダウン・フェーズ切替を担う。
dom.js 画面(DOM)の取得・更新を担う。
config.js タイマーや状態を保持する変数群を管理する。

🧩 各モジュールの解説

⚙️ main.js の解説

このファイルは**アプリ全体の「起点」**です。
具体的には、次の役割を担います。

  • モジュールの読み込み
    events.js モジュールから registerEvents 関数をインポートしています。これにより、イベントリスナーの登録処理を外部ファイルに分割して管理できるようになっています。
  • ボタンのイベントリスナー登録
    DOMContentLoaded イベントが発火すると、registerEvents が呼び出されます。これは、HTMLの読み込みが完了したタイミング で、各種ボタンのイベント設定を行うためです。
    DOMContentLoadedは画像などのリソースの読み込みを待たずに、HTMLの構造だけが整った時点で イベントが発火するので、ユーザーの操作をいち早く受け付けられます。
  • グローバル変数管理を避ける
    main.jsは「ハブ」としての役割にとどめ、タイマー処理はtimer.jsに任せています。
main.js のコード(クリックして展開)
// main.js
import { registerEvents } from "./events.js";

document.addEventListener("DOMContentLoaded", () => {
  registerEvents();
});

🎯 events.js の解説

このモジュールは、ユーザーが操作する「イベントハンドラ」をまとめています。
ボタンを押したときの動作など、アプリの「操作ロジック」を担当しています。

  • イベントの集約管理
    すべてのボタンのクリックイベント(start, stop, reset)をここにまとめることで、見通しが良くなります。
  • スタート・ストップ・リセット機能の切り替え
    それぞれのボタンが担う役割に応じて、タイマーの開始、停止、初期化を行います。
  • ロジックの分離
    DOM要素の取得やタイマーの進行処理は別モジュール(dom.js, timer.js)に分け、ここではイベントの設定のみに専念します。
events.js のコード(クリックして展開)
//events.js
import { getElements, updateDisplay, updatePhaseLabel } from "./dom.js";
import { setState, isRunning, timerInterval } from "./config.js";
import { startPhase } from "./timer.js";

export const registerEvents = () => {
  const {
    startBtn,
    stopBtn,
    resetBtn,
    workInput,
    breakInput
  } = getElements();

  startBtn.addEventListener("click", () => {
    if (!isRunning) {
      const work = parseFloat(workInput.value) || 0;
      const rest = parseFloat(breakInput.value) || 0;
      setState({
        workDuration: Math.floor(work * 60),
        breakDuration: Math.floor(rest * 60),
        isWorkPhase: true,
        isRunning: true
      });
      startPhase();
    }
  });

  stopBtn.addEventListener("click", () => {
    clearInterval(timerInterval);
    setState({ isRunning: false });
  });

  resetBtn.addEventListener("click", () => {
    clearInterval(timerInterval);
    updateDisplay(0);
    updatePhaseLabel("作業中");
    setState({
      isRunning: false,
      isWorkPhase: true,
      remainingTime: 0
    });
  });
};


⏳ timer.js の解説

タイマーの状態管理フェーズ切替処理を行うモジュールです。
以下のような責任を担います。

  • タイマー状態の変数管理
    作業時間・休憩時間・現在のフェーズなどを1つのファイルにまとめ、分かりやすくしました。
  • タイマー制御ロジック
    Startでフェーズを始め、Stopで停止、Resetで初期化する仕組みを持っています。
  • モジュールエクスポート
    他のモジュール(main.js)から使えるように、関数をexportで公開しています。
timer.js のコード(クリックして展開)
//timer.js
import { isRunning, isWorkPhase, workDuration, breakDuration, remainingTime, timerInterval, setState } from "./config.js";
import { updateDisplay, updatePhaseLabel } from "./dom.js";

export const startPhase = () => {
  const duration = isWorkPhase ? workDuration : breakDuration;
  updatePhaseLabel(isWorkPhase ? "作業中" : "休憩中");
  setState({ remainingTime: duration });

  updateDisplay(duration);

  const interval = setInterval(() => {
    // まず現在のremainingTimeを表示
    updateDisplay(remainingTime);

    if (remainingTime <= 0) {
      clearInterval(timerInterval);
      setState({
        isWorkPhase: !isWorkPhase,
      });
      startPhase();
      return;
    }

    setState({ remainingTime: remainingTime - 1 });
  }, 1000);

  setState({ timerInterval: interval });
};


📦 dom.js の解説

このモジュールは、HTML要素の取得(DOMの参照)をまとめています。
複数のファイルで同じ要素を扱う場合でも、ここを参照すれば済むようにしています。

  • UI要素の取得
    ボタン、入力欄、表示エリアなどを一括して管理します。
  • コードの見通しを良くする
    直接getElementByIdを散乱させるのではなく、dom.jsで整理しておくことで可読性が向上。
  • 再利用性の向上
    例えば、timer.jsdisplay.jsで同じDOM要素を使いたいときに、dom.jsをインポートすればOKです。
dom.js のコード(クリックして展開)
// dom.js
export const getElements = () => ({
  startBtn: document.getElementById("start-btn"),
  stopBtn: document.getElementById("stop-btn"),
  resetBtn: document.getElementById("reset-btn"),
  timerDisplay: document.getElementById("timer-display"),
  phaseLabel: document.getElementById("phase-label"),
  workInput: document.getElementById("work-minutes"),
  breakInput: document.getElementById("break-minutes")
});

export const updateDisplay = (remainingTime) => {
  const minutes = String(Math.floor(remainingTime / 60)).padStart(2, "0");
  const seconds = String(remainingTime % 60).padStart(2, "0");
  document.getElementById("timer-display").textContent = `${minutes}:${seconds}`;
};

export const updatePhaseLabel = (label) => {
  document.getElementById("phase-label").textContent = label;
};

🛠️ config.js の解説

このモジュールは、タイマーに関連する「設定」や「初期値」を管理しています。
アプリ全体で使う設定値を1箇所にまとめることで、変更や調整を簡単にできるようにしました。

  • 初期状態のフラグ管理
    isRunning, isWorkPhase など、アプリの挙動に関わる変数をまとめています。
  • 作業・休憩時間の秒数設定
    フェーズ切り替えに使う作業時間と休憩時間の初期値を一括管理。
  • 再利用性の向上
    他のモジュールからインポートするだけで、同じ設定を共有できます。
config.js のコード(クリックして展開)
// config.js
export let isRunning = false;
export let isWorkPhase = true;
export let workDuration = 0;
export let breakDuration = 0;
export let remainingTime = 0;
export let timerInterval = null;

export const setState = (updates) => {
  if ("isRunning" in updates) isRunning = updates.isRunning;
  if ("isWorkPhase" in updates) isWorkPhase = updates.isWorkPhase;
  if ("workDuration" in updates) workDuration = updates.workDuration;
  if ("breakDuration" in updates) breakDuration = updates.breakDuration;
  if ("remainingTime" in updates) remainingTime = updates.remainingTime;
  if ("timerInterval" in updates) timerInterval = updates.timerInterval;
};

🔍 この記事でやったこと

  • コードをモジュール化
    → 機能ごとにファイルを分割し、見やすく・改造しやすい形にしました。

  • 関数をアロー関数に統一
    → 記法を統一することで読みやすさや一貫性を向上させました。

  • import/exportによる依存関係の明示化
    → 各ファイルの役割がはっきりして、後から見返すときに分かりやすくなりました。

  • 必要な修正ポイントの洗い出しと対応
    → HTMLでtype="module"を追加することで、ES Modulesとして動作するように修正しました。


📌 今回の位置づけ(シリーズ進行)

今回もご覧いただきありがとうございます。いよいよ最後にサーバーにアップするだけとなりました!
サーバーアップ後簡単にシリーズ全体のまとめもしようとおもっていますので、よければご覧ください!

Discussion