11. 保守しやすいJavaScript設計

この章では、保守しやすいJavaScriptの書き方を学びます。

ここまでの章で、ハンバーガーメニュー、アコーディオン、タブ、フォーム、スクロール演出、スライダーなどを扱ってきました。

実務では、それらのコードを1つのファイルにただ並べるだけだと、すぐに読みづらくなります。後から修正しづらくなり、別ページでエラーが出たり、同じ処理を何度もコピペしたりする原因になります。

この章では、案件で使い回しやすく、壊れにくいJavaScriptを書くための考え方を整理します。

汚いJavaScriptになりやすい理由

Web制作のJavaScriptは、最初は小さく始まります。

最初は小さい
const button = document.querySelector(".js-button");
const menu = document.querySelector(".js-menu");

button.addEventListener("click", () => {
    menu.classList.toggle("is-active");
});

しかし、案件が進むと処理が増えていきます。

  • ハンバーガーメニュー
  • アコーディオン
  • タブ
  • モーダル
  • フォーム
  • スライダー
  • スクロール演出

これらを整理せずに1つのファイルへ書き続けると、どこに何があるのかわからなくなります。

保守しやすくするには、処理の置き場所と責務を決めることが大切です。

ファイル設計の基本

小さなサイトなら、main.jsだけでも問題ありません。

ただし、処理が増えてきたら、UIごとにファイルを分けると読みやすくなります。

ファイル構成例
assets/js/
  main.js
  modules/
    menu.js
    accordion.js
    tabs.js
    modal.js
    slider.js
    form.js

main.jsは、各モジュールを呼び出す入口にします。

main.js
import { initMenu } from "./modules/menu.js";
import { initAccordion } from "./modules/accordion.js";
import { initTabs } from "./modules/tabs.js";
import { initSlider } from "./modules/slider.js";

initMenu();
initAccordion();
initTabs();
initSlider();

各ファイルには、そのUIの処理だけを書きます。

modules/accordion.js
export function initAccordion() {
    const accordions = document.querySelectorAll(".js-accordion");

    accordions.forEach((accordion) => {
        const button = accordion.querySelector(".js-accordion-button");
        const panel = accordion.querySelector(".js-accordion-panel");

        if (!button || !panel) {
            return;
        }

        button.addEventListener("click", () => {
            const isOpen = button.getAttribute("aria-expanded") === "true";

            button.setAttribute("aria-expanded", String(!isOpen));
            panel.hidden = isOpen;
        });
    });
}

初期化関数

UIごとに、初期化関数を作ると管理しやすくなります。

初期化関数
function initModal() {
    const openButtons = document.querySelectorAll(".js-modal-open");

    if (openButtons.length === 0) {
        return;
    }

    // モーダルの処理
}

initModal();

関数名は、initMenuinitAccordioninitModalのように、何を初期化するかがわかる名前にします。

対象要素がない時のガード

複数ページで同じJavaScriptを読み込む場合、すべてのページに対象要素があるとは限りません。

対象要素がない時は、何もしないようにします。

ガード処理
export function initTabs() {
    const tabGroups = document.querySelectorAll(".js-tabs");

    if (tabGroups.length === 0) {
        return;
    }

    tabGroups.forEach((tabGroup) => {
        // タブ処理
    });
}

querySelectorAllは、対象がなくても空のNodeListを返します。

length === 0で対象がないことを確認できます。

複数設置に対応する

UIは、最初は1つだけでも後から増えることがあります。

アコーディオンやタブ、モーダル、スライダーは複数設置を想定しておくと安全です。

複数設置に対応する
const accordions = document.querySelectorAll(".js-accordion");

accordions.forEach((accordion) => {
    const button = accordion.querySelector(".js-accordion-button");
    const panel = accordion.querySelector(".js-accordion-panel");

    if (!button || !panel) {
        return;
    }

    button.addEventListener("click", () => {
        panel.hidden = !panel.hidden;
    });
});

ポイントは、各コンポーネントの中から要素を探すことです。

コンポーネント内から探す
const button = accordion.querySelector(".js-accordion-button");
const panel = accordion.querySelector(".js-accordion-panel");

ページ全体から探すと、別のアコーディオンの要素を操作してしまうことがあります。

CSS用クラスとJS用クラスを分ける

CSS用クラスとJavaScript用クラスは、分けておくと壊れにくくなります。

HTML
<button class="button button-primary js-modal-open" type="button">
    モーダルを開く
</button>

buttonbutton-primaryはCSS用です。

js-modal-openはJavaScriptで取得するためのクラスです。

デザイン修正でCSSクラスが変わっても、js-クラスが残っていればJavaScriptは動き続けます。

状態クラス

状態を表すクラスには、is-has-を使うことが多いです。

状態クラス
<nav class="global-menu is-active"></nav>

よく使う状態クラスの例です。

クラス意味
is-activeアクティブ
is-open開いている
is-visible表示されている
is-hidden隠れている
is-current現在位置
has-errorエラーがある

プロジェクト内でルールを決めておくと、CSSとJavaScriptの連携が読みやすくなります。

data属性で設定を渡す

同じUIでも、場所によって設定を変えたいことがあります。

たとえば、モーダルを開くボタンがどのモーダルを開くかを、data-*属性で持たせます。

HTML
<button class="js-modal-open" type="button" data-modal-target="modal-contact">
    お問い合わせを開く
</button>

<div class="modal js-modal" id="modal-contact" hidden></div>
data属性を読む
const targetId = button.dataset.modalTarget;
const modal = document.getElementById(targetId);

data-*属性を使うと、JavaScriptの中にIDや設定値をベタ書きしすぎずに済みます。

設定オブジェクト

複数の設定値をまとめたい時は、オブジェクトを使います。

設定オブジェクト
const menuOptions = {
    activeClass: "is-active",
    openClass: "is-menu-open",
    breakpoint: "(max-width: 768px)",
};

小さな処理では不要ですが、同じ値が何度も出る場合はまとめると読みやすくなります。

設定を使う
menu.classList.add(menuOptions.activeClass);
document.body.classList.add(menuOptions.openClass);

コメントの書き方

コメントは、コードを読めばわかることではなく、判断理由を書くために使います。

あまり意味がないコメント
// クラスを追加する
menu.classList.add("is-active");

これはコードを見ればわかります。

意味のあるコメント
// iOS Safariでは背景スクロールが残るため、body側にも状態を持たせる
document.body.classList.add("is-menu-open");

なぜその処理が必要なのかを書くと、後から読む人に役立ちます。

小さな設計例: アコーディオン

ここまでの考え方を使って、アコーディオンを整理した形で書いてみます。

accordion.js
export function initAccordion() {
    const accordions = document.querySelectorAll(".js-accordion");

    if (accordions.length === 0) {
        return;
    }

    accordions.forEach((accordion) => {
        setupAccordion(accordion);
    });
}

function setupAccordion(accordion) {
    const button = accordion.querySelector(".js-accordion-button");
    const panel = accordion.querySelector(".js-accordion-panel");

    if (!button || !panel) {
        return;
    }

    button.addEventListener("click", () => {
        const isOpen = button.getAttribute("aria-expanded") === "true";

        setAccordionState(button, panel, !isOpen);
    });
}

function setAccordionState(button, panel, shouldOpen) {
    button.setAttribute("aria-expanded", String(shouldOpen));
    panel.hidden = !shouldOpen;
    panel.classList.toggle("is-active", shouldOpen);
}

関数を分けることで、役割が見えやすくなります。

  • initAccordion: 全体の初期化
  • setupAccordion: 1つのアコーディオンを準備
  • setAccordionState: 開閉状態を反映

ページごとのJavaScript

サイトによっては、共通JSとページ専用JSを分けることがあります。

ファイル構成例
assets/js/
  main.js
  pages/
    top.js
    contact.js
  modules/
    accordion.js
    modal.js

トップページだけのファーストビュー演出や、フォームページだけの処理は、ページ専用ファイルに分けると見通しが良くなります。

ただし、ビルド環境やCMSの仕様によって管理方法は変わります。

まずは案件のルールに合わせましょう。

AstroやNext.jsとの関係

最近のWeb制作では、AstroやNext.jsのようなフレームワークを使ってサイトを作ることがあります。

これらは、HTML、CSS、JavaScriptを直接書く制作とまったく別物ではありません。

コンポーネントとしてUIを分けたり、MarkdownやCMSの内容からページを生成したり、ビルドして公開用のHTMLやJavaScriptを作ったりするための仕組みです。

名前Web制作での位置づけ
Astro静的サイトやコンテンツ中心のサイトと相性がよいフレームワーク
Next.jsReactを使ったサイトやWebアプリでよく使われるフレームワーク
SSGビルド時にHTMLを生成する考え方
SSRアクセス時にサーバー側でHTMLを生成する考え方

この講座では、AstroやNext.jsの使い方までは深掘りしません。

ただし、どの環境でも、最終的にはブラウザ上のHTMLをJavaScriptで操作する場面があります。

そのため、DOM操作、イベント、初期化関数、対象要素がない時のガード処理といった基本は、フレームワークを使う前の土台になります。

よくある失敗

保守性の面でよくある失敗を整理します。

  • main.jsにすべてを書き続けて、どこに何があるかわからなくなる
  • 同じ開閉処理を複数箇所にコピペする
  • 対象要素がないページでエラーになる
  • 1ページに1つだけの前提で書いて、複数設置に対応できない
  • CSS用クラスをJavaScriptで取得して、デザイン修正で壊れる
  • 関数名がaaatestのように意味を持たない
  • コメントが多いのに、なぜ必要なのかが書かれていない

保守しやすさは、後から一気に足すより、最初の小さなルールでかなり変わります。

実務でのチェックリスト

JavaScriptを書いた後は、次の項目を確認しましょう。

この章のまとめ

この章では、保守しやすいJavaScript設計を学びました。

  • 処理が増えたら、UIごとにファイルや関数を分ける
  • main.jsは各初期化関数を呼び出す入口にする
  • 対象要素がないページでは何もしないガード処理を書く
  • 複数設置に対応するには、コンポーネント内から要素を探す
  • CSS用クラスとJavaScript用クラスを分ける
  • 状態クラスはis-activeなど、プロジェクト内でルールを決める
  • コメントは「何を」より「なぜ」を補足する
  • AstroやNext.jsのような環境でも、DOM、イベント、状態管理の基礎は土台になる