ユニバーサル朗読プレイヤー — フックを兼任させて部品は一行も改造しない設計
今回の登場人物
Keath(キース)
AI パートナー / Blues-Men プロジェクトリーダー
Delta Blues の記録者。「既存を壊さない設計」を信条とする技術者。部品を一行も改造せずに繋げる方法を先に探す。
デジタルを通じて音楽と人をつなぐ次世代プロジェクト。現在準備中。
見た目が全然違う2つのプレイヤーが、同じ JS で動いている。
キースが Blues-Men プロジェクトで実装した朗読プレイヤーは、2種類のUIが存在する。
1つ目は「S1」——シンプルなバー型。テキストの下に横長の再生ボタンが並ぶ、よくある形だ。
2つ目は「S2」——大きなレコード盤型。中央にアルバムアートが入り、周囲にグルーブが刻まれる重厚なデザイン。見た目はまるで別物だ。
だが、この2つは同じ1本のJSファイルで動いている。コードに手を入れたのは0行だ。
課題:S2 プレイヤーに既存の narration-player.js を繋げたい
Blues-Men には universal narration-player.js という朗読再生のコア部品がある。S1 プレイヤーのために書かれたものだ。音源の読み込み、再生・停止、シーク、タイムライン表示——これらをまとめて担っている。
S2 の大レコード盤 UI を実装したとき、キースは自然にこう考えた。
「この JS、S2 でも使えないか。S2 のためにわざわざ書き直すのは違う気がする。」
しかし S2 の HTML 構造は S1 とまったく違う。クラス名も、要素の配置も。普通に考えれば、新しい HTML 構造に合わせた別の JS を書くか、既存 JS を大幅に書き換えるかのどちらかだ。
解決策:フックを「兼任」させる
キースが取った方法は、どちらでもなかった。
S2 の要素に data-np-* 属性を付けるだけ。
universal narration-player.js は HTML の構造(クラス名や入れ子)を直接見ていない。代わりに data-np-src、data-np-title など、専用の属性(フック)を持つ要素を探して動作する。だから、S2 の HTML がどんな見た目であっても、その要素に data-np-* の属性を付ければ——JS 側にとっては「普通の朗読プレイヤーの要素」に見える。
<!-- S1: シンプルバー型 -->
<div class="player-s1"
data-np-src="/audio/ep01.mp3"
data-np-title="Episode 01">
<button class="play-btn">▶</button>
<div class="timeline"></div>
</div>
<!-- S2: 大レコード盤型 — クラスもHTMLも全く違う -->
<div class="player-s2"
data-np-src="/audio/ep01.mp3"
data-np-title="Episode 01">
<div class="record-disc">
<div class="album-art"></div>
<div class="groove-ring"></div>
</div>
<div class="record-controls"></div>
</div>
<!-- universal narration-player.js: 改造 0 行 -->
universal narration-player.js は data-np-src を見つけたら動く。S1 か S2 かは関係ない。見た目がどれだけ違っても、属性が接着剤になっているから、どちらにも同じ部品がくっつく。
結果:2 UI / 1 JS / 改造 0 行
S2 に data-np-* 属性を追加した後、キースは universal narration-player.js を開かなかった。開く必要がなかったからだ。
レコード盤 UI で再生ボタンを押すと、S1 と同じ JS が動き、音が鳴った。
見た目が全く違う2つのUIが、同じ部品で動いている。これが「フックを兼任させる」設計の力だ。
【技術コラム】Universal 部品は属性駆動で作れ
キースが実証したことを設計原則として書き直すと、こうなる。
「Universal 部品は HTML 構造に依存させるな。属性(data-*)だけを見て動かせ。」
なぜこれが重要か。フロントエンド開発では、デザインが先行して変わる。S2 のような新しいUIが後から来ることは珍しくない。そのたびに JS を書き直していては、コードベースが肥大化し、バグの温床になる。
属性駆動にしておけば、JS は「この属性がついている要素なら動く」という単純なルールだけで動作する。HTML の見た目がどう変わろうと、JS は変わらない。
属性駆動設計の実装パターン
// 悪い例: HTML構造(クラス名・入れ子)に依存
const playBtn = document.querySelector('.player-s1 .play-btn')
// 良い例: 属性フックにだけ依存
const players = document.querySelectorAll('[data-np-src]')
players.forEach(el => {
const src = el.dataset.npSrc
const title = el.dataset.npTitle
// 以後の処理はどのUIでも同じ
})
変わりやすいものと変わりにくいものを分離する。CSS のクラス名やHTMLの入れ子(変わりやすい)に依存せず、意味を持つ属性(変わりにくい)だけを接着点にする。
チェックリスト(Universal 部品を設計するとき)
- ✅
document.querySelector('.specific-class')を使っていないか(クラスへの依存) - ✅ 動作のトリガーを
data-*属性に分離できているか - ✅ 新しいUIが来たとき、既存JSを開かずに対応できるか
- ✅ 属性名がJSの内部ロジックを反映していないか(例:
data-play-buttonよりdata-np-src)
「部品を一行も改造しない」は目標ではなく、設計が正しかったことの証拠だ。
キースが残した一文
「UI は変わる。機能は変わらない。その2つを繋ぐのが、属性だ。」
Blues-Men プロジェクトは音楽と人をつなぐ場所になる。その前に、コードと UI をつなぐ設計を磨いている。
株式会社ツクルンでは、仲間が積み上げた設計知見を記録し、次の実装者に届け続ける。tsukurun.co.jp →