Vue.jsを使ったフロントエンド開発において、コンポーネント設計は常に悩ましい課題です。単一責任の原則やAtomicデザイン、細かい分割の是非など、多くのアプローチがありますが、正解はありません。
しかし、ルールがないとプロダクトの品質が低下するリスクがあるため、「指針」が求められます。
この記事では、私自身が現場経験を通して磨いてきた「Page > Pane > Section > Parts単位」のコンポーネント設計手法をご紹介します。
私の考え方
フロントエンドのコンポーネント設計は、
どのような考え方/単位で設計すればいいのでしょうか?
私の場合、
「どうなっていれば楽か」
を基準にコンポーネント設計をしていきます。
ここでいう「楽」とは
- 実装/管理が楽である
- デバックが楽である
- テストの作成/運用が楽である
- 開発チーム運営が楽である
ことを意味しています。
その手法が、
Page > Pane > Section > Parts単位の設計です。
Page > Pane > Section > Parts単位の設計とは?
ここでは、私のコンポーネント設計方針である「Page > Pane > Section > Parts単位」について解説します。
それぞれの単位に対し、次のような役割を持たせています。
単位の役割
- Page: ルーティング
- Pane: APIアクセス、Storeへのデータ保持・参照
- Section: Storeの参照、Partsの集合体
- Parts: 表示や入出力を管理するAtom(原子)単位のバーツ
単位 | 依存関係 | APIアクセス | Storeアクセス |
---|---|---|---|
Page | Pane | ⭕️ | ❌ |
Pane | Section | ⭕️ | ⭕️ |
Section | Parts | ❌ | ⭕️ |
Parts | Parts | ❌ | ❌ |
フロントエンド開発をやっていると、開発期間に比例してコンポーネント数が増えていきます。
それぞれのコンポーネントが何をやっているのかを、正確に把握することは困難です。
明確な設計方針がないと、そうなります。
しかし、「各単位がどのような役割を持つべきか」をルールとして定めておけば、コンポーネントの管理やバグ発生時の原因の切り分けがしやすいです。
一番避けたいのは、色々なコンポーネントでAPIアクセスをやっていたり、Storeに接続していたりするスパゲッティな状態です。
このルールに基づくと、
例えば「Section」や「Parts」は、
- 親コンポーネントやストアから供給されるデータが正常に表示されているか
- 入出力のイベント処理やデータの受け渡しが正常に動作しているか
に責任を持たせることができます。
一方、「Pane」は、APIアクセスやデータ加工、ストアへの保存処理など、機能のメインロジックに集中できます。
「Page」は、ルーティングを制御して「Pane」を配信するだけなので、その他のロジックには関与しません。
各単位で責任が明確に分離しているということは、テストもしやすく、アプリケーションの品質と開発体験を向上できます。
【実践】どのように単位を分けるか
ここでは、具体例を用いて「Page > Pane > Section > Parts」に分割していきます。
以下の画像は、
私が開発しているプログラミングスクール検索サービスの管理画面です。
【コースの一覧画面】
【コースの詳細画面】
シングルページアプリケーションで構築しているので、以下のような構成になっています。
- ルーティング
/course
- 画面(機能)
- 一覧
- 新規作成編集
1つのルートに対して、2つの画面(機能)があります。
なので、以下のように分割していきます。
Page | CoursePage.vue: ルート/course を管理する 一覧/編集モードに合わせて Paneの表示を切り替える。 |
Pane | CourseListPane.vue: コースの一覧画面を管理するPane CourseEditPane.vue コースの作成/編集画面を管理するPane |
Section(一覧画面のみ解説) | SchoolSelect.vue スクールのセレクトボックスを管理 CourseListHeader.vue ページングや新規作成のボタンを管理 CourseListBody.vue コース一覧のテーブルを管理 |
Parts(CourseListBody.vueのみ解説) | CourseRecord.vue 一覧のレコードを管理 CourseRecordButton.vue 複製・削除ボタンを管理 |
【分割イメージ】
ディレクトリ構成にすると以下のようなイメージになります。
src/
components/
course/
page/
CoursePage.vue
pane/
CourseListPane.vue
CourseEditPane.vue
section/
list/
SchoolSelect.vue
CourseListHeader.vue
CourseListBody.vue
edit/
parts/
list/
CourseRecord.vue
CourseRecordButton.vue
edit/
もしくは、「Nuxt.js」のようにcomponentsとpagesで分けてもいいです。
src/
components/
course/
pane/
section/
parts/
pages/
course/
index.vue // ← これでルーティングを管理する
この設計をすれば、ディレクトリ構成にも意味を持たせることができるので、管理とデバックが楽になります。
また、開発チーム運営の面でも、このように方針をまとめておくと認識を合わせることができます。
- →設計方針を明確にする
- →共通認識をもてる
- →誰が実装しても同じ構成になる = 属人化しない
- →管理/デバックがしやすい
- →品質に一貫性が生まれる
のフローを実現できると思います。
設計通り実装してみる
実際に以下のディレクトリ構成でコンポーネントを配置してみます。
src/
components/
course/
page/
CoursePage.vue
pane/
CourseListPane.vue
CourseEditPane.vue
section/
list/
SchoolSelect.vue
CourseListHeader.vue
CourseListBody.vue
edit/
parts/
list/
CourseRecord.vue
CourseRecordButton.vue
edit/
詳細のロジックは記載しないので、
Page > Pane > Section > Partsの構造を理解することを意識してください。
Page
@/src/components/course/page/CoursePage.vue
/course
のルートを管理し、一覧 or 編集画面を切り替える責任を持ちます。
<template>
<div>
<CourseListPane v-if="isListMode" />
<CourseEditPane v-else />
</div>
</template>
<script setup>
import { ref } from 'vue';
import CourseListPane from '../pane/CourseListPane.vue';
import CourseEditPane from '../pane/CourseEditPane.vue';
// ルーティングで一覧か編集を決定
const isListMode = ref(true); // ルーティングや条件で切り替え
</script>
Pane
@/src/components/course/pane/CourseListPane.vue
コースの一覧画面を管理し、Section
に責任を分離します。データの取得や更新など、APIに関連する処理などはこちらで実施します。
<template>
<div>
<SchoolSelect />
<CourseListHeader />
<CourseListBody />
</div>
</template>
<script setup>
import SchoolSelect from '../section/list/SchoolSelect.vue';
import CourseListHeader from '../section/list/CourseListHeader.vue';
import CourseListBody from '../section/list/CourseListBody.vue';
</script>
@/src/components/course/pane/CourseEditPane.vue
コースの編集画面を管理するPaneです。
<template>
<div>
<!-- コース編集画面用のSectionやPartsをここに配置する -->
<p>Course Edit Screen</p>
</div>
</template>
<script setup>
</script>
Section
@/src/components/course/section/list/SchoolSelect.vue
コースの一覧画面におけるスクール選択ボックスを管理します。
<template>
<div>
<label for="school">School:</label>
<select id="school">
<option value="1">School A</option>
<option value="2">School B</option>
</select>
</div>
<div>
<button>外部リンク</button>
</div>
</template>
<script setup>
</script>
@/src/components/course/section/list/CourseListHeader.vue
コース一覧のページングや新規作成ボタンを管理します。
<template>
<div>
<button @click="onCreateCourse">新規作成</button>
<p>Page 1 of 5</p> <!-- ページング要素 -->
</div>
</template>
<script setup>
const onCreateCourse = () => {
console.log('New course creation initiated');
};
</script>
@/src/components/course/section/list/CourseListBody.vue
コース一覧のテーブルを表示し、個別のコースレコードを管理するParts
に分割します。
<template>
<table>
<tbody>
<CourseRecord
v-for="course in courses"
:key="course.id"
:course="course"
/>
</tbody>
</table>
</template>
<script setup>
import { ref } from 'vue';
import CourseRecord from '../../parts/list/CourseRecord.vue';
const courses = ref([
{ id: 1, name: 'Vue.js Basics' },
{ id: 2, name: 'Advanced Vue.js' }
]);
</script>
Parts
@/src/components/course/parts/list/CourseRecord.vue
コース一覧の1つのレコード(行)を管理します。
<template>
<tr>
<td>{{ course.id }}</td>
<td>{{ course.name }}</td>
<td>
<CourseRecordButton label="Edit" />
<CourseRecordButton label="Delete" />
</td>
</tr>
</template>
<script setup>
import CourseRecordButton from './CourseRecordButton.vue';
defineProps({
course: Object
});
</script>
@/src/components/course/parts/list/CourseRecordButton.vue
レコードごとのボタンを管理します。
<template>
<button @click="onClick">{{ label }}</button>
</template>
<script setup>
defineProps({
label: String
});
const onClick = () => {
console.log(`Button clicked: ${label}`);
// ここでemitを使ってカスタムイベント発火する
};
</script>
以上の構成になります。
テスト設計のしやすさについて
私が「Page > Pane > Section > Parts」単位の設計をしている理由として、テストのしやすさがあります。最後に、この観点に触れておきましょう。
私の設計は、コンポーネントテストを容易にします。
E2Eテストやユニットテストはやっているけど、コンポーネントテストはやっていない。
このような現場が多いです。
なぜ、コンポーネントテストをやらないのか。
それは、明確なコンポーネント設計方針がないので、コンポーネントの構成も粒度もバラバラになってしまっているからです。その結果、同じ見た目なのに微妙に動きが違ったり、コードが肥大化したりします。
私の設計手法を採用すると、Storybookでのコンポーネント設計が容易に実装でき、デザイナーとの連携も深めることができます。
Storybookとの連携
ここでは、Section 、Partsにフォーカスしてください。
これまで説明してきた通り、SectionとPartsは、コンポーネント単体でAPIと通信したり、ストアのデータを更新することもありません。
Partsは最小単位のコンポーネントとして表示の役割をもち、
SectionはPartsの集合体としての役割を持ちます。
ということは、
Storybookを活用したコンポーネントテストを
ロジックの開発と独立した形で実施できます。
Parts単位でストーリを作成し、
input部品などのUI検証をします。
そして、
Section単位で複合的なストーリーを作成し、
各Partsが連携した状態で、正常に動作しているかを確認できるのです。
Storybook上でコンポーネントを開発するため、
デザイナーも早期にUIの検証に参加できます。
このように、
ロジックに影響を受けずに
コンポーネントテストができる点でも
「Page > Pane > Section > Parts」単位で設計する理由です。
まとめ
コンポーネント設計において、明確な正解はありません。しかし、効果的な基準を持つことで、開発の効率や品質を大きく向上させることができます。私の提案する「Page > Pane > Section > Parts単位」の設計手法は、各コンポーネントの責任を明確にし、開発・管理のしやすさを追求したものです。このアプローチを導入することで、ディレクトリ構成にも一貫性が生まれ、チーム全体で共通の認識を持ち、誰が開発しても同じような構造に落とし込むことができます。
コメント