変更履歴
2021/10/31 ... Templates に関する記載を変更。Pages に関する記述を変更
はじめに
フロント開発における UI デザイン手法として、Atomic Design が一番耳に入るのではと思うが、その Atomic Design を自分なりにまとめたものをメモとして記事にしようと思います。
Atomic Design については、調べるとたくさんの記事が出てくるため、Atomic Design とはなんなのかについては割愛させていただきます。
また、今回の記事では Vue / Nuxt ならではの Atomic Design を自分なりにまとめた記事となるので、 React / Angular などではあまり参考にならないかもですが、設計指針的な箇所では参考になるかもなので、是非読んでみてください〜。
また、「こうした方がもっとスッキリして簡潔だよ〜」みたいなコメントなどもどんどん募集しています!
Atomic Design に触れてみて
現在の職場では Atomic Design を採用し開発を進めているが、自分はその Atomic Design に業務で触れるのは初でした。
ただ、以下のようなレイヤー・役割があるので、それだけでもわかりやすく開発しやすそうだな〜とイメージしていました。
- Atoms(原子)
- これ以上分割できない最小のコンポーネント
- Molecules(分子)
- 複数の Atoms を組み合わせたコンポーネント
- Organisms(有機体)
- 1つの機能を表現するコンポーネント
- Templates(テンプレート)
- ワイヤーフレーム
- Pages(ページ)
- ワイヤーフレームに値を流し込むためのコンポーネント
この認識を元にいざコーディングとなると、細かい箇所で「ここどうするの?」みたいなのがたくさん出てきて、調べたり、聞いたりしてようやくスッキリした形となってきたので、その内容をこれから記載していく。
Atoms(原子)
Atoms は最小のコンポーネント(ボタン・ラベル・各入力項目など)となるがその他の細かい決まり事を、以下のように定義する。
- ビジネス UI とビジネスロジックを持たない
- 異なるプロジェクトや様々な箇所で利用できるようなコンポーネントを作成する
- 基本的な UI としての機能しか持たない
ここの「ビジネス」とは、システムならではの要件・要望のことをさしており、システムならではの UI / ロジックを持つのではなく、本当にシンプルな UI / ロジック機能のみを持つ。
その要件を満たすことにより、次の項目(異なるプロジェクトや様々な箇所で利用できるようなコンポーネントを作成する)を実現できる。
以下のコンポーネントは、簡単なボタンではあるが、上記の項目を満たしたボタンとなる
<template>
<button type="button" class="a-button" :class="[color, { disabled }]">
<slot>Button</slot>
</button>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ButtonColor } from '@/types/components/01-atoms/general/AButton';
export default defineComponent({
props: {
color: {
type: String as PropType<ButtonColor>,
default: () => 'default' as ButtonColor
},
disabled: {
type: Boolean,
default: () => false
}
}
});
</script>
<style lang="scss" scoped>
// 割愛
</style>
カラーの変更や、disabled 切り替えのロジックに関しては UI の基本的な機能となり、どのプロジェクトでも必要そうなロジックとなる。
システム要件は一切入っておらず、とてもシンプルなコンポーネントとなる。
Molecules(分子)
Molecules は複数の Atoms を組み合わせたコンポーネントとなるがその他の細かいルールを以下のように定義する。
- ビジネスUIは持つが、ビジネスロジックは持たない
- UIとしての機能を持つ
今回でいうビジネス UI とは、ラベル・入力ボックスの配置などを指し、システム上の要件をのんでいいと考えている。
ただ、ビジネスロジックは持たず、シンプルな UI 機能を実装する。
こちらも上記要件を満たしたコンポーネントを記載する。
<template>
<fieldset class="m-checkbox-group">
<template v-for="option in options" :key="option.value">
<a-checkbox :option="option" v-model="model" @change="onChange" />
</template>
</fieldset>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, SetupContext } from 'vue';
import ACheckbox from '@/components/01-atoms/data-entry/ACheckbox.vue';
import { ACheckboxOption, ACheckboxValues } from '@/types/components/01-atoms/data-entry/ACheckbox';
export default defineComponent({
components: {
ACheckbox
},
props: {
options: {
type: Array as PropType<ACheckboxOption[]>,
default: () => []
},
modelValue: {
type: Array as PropType<ACheckboxValues>,
default: () => []
}
},
setup(props, { emit }: SetupContext) {
const methods = {
onChange: (option: ACheckboxOption, checked: boolean) => {
emit('change', option, checked, props.options);
}
};
const computedMethods = {
model: computed({
get: () => props.modelValue,
set: (newModelValue) => emit('update:modelValue', newModelValue)
})
};
return {
...methods,
...computedMethods
};
}
});
</script>
こちらはチェックボックスグループのコンポーネントとなり、チェックボックスの配置などはこちらのコンポーネントで考慮するが、ビジネスロジックは一切持っていない作りとなっている。
Organisms(有機体)
こちらは1つの機能(ブロック)を表現するコンポーネントであるが、その他の細かいルールを下記のように定義した。
- ビジネスUIとビジネスロジックを持つ
- UIとしての機能はMolecules以降のレイヤーに任せる(例外もあると思う)
- 一画面のビジネスロジックを担う
- 表示するデータは fetch しない(Templates から受け取る)
- その他のビジネスロジックに関わる API 連携を担う(emit しない)
ここのレイヤーはすごく悩んだ記憶があり、詳細に残せたらと思うw
まず、システムの要件・要望は全てコンポーネントに落とし込む。
ただ、ビジネスロジックでソースが膨れ上がる場合はうまく共通化を実施し、スクリプト部分を外出しする。
エレメント部分で膨れ上がる場合は、 Molecules に出すか検討し、 Molecules の要件を満たせる場合はコンポーネントとして切り出す。
スタイル部分は丸ごと import するようにし、スタイルのソース( <style scoped>
以外は)は vue ファイルには記述しないようにする。(シンプルに実装できた場合は、外出ししなくてもいいと思っている)
Templates レイヤーでワイヤーフレームを表現するため、 data fetch は Organisms では実施せずビジネスロジックに関わるもののみ API 連携を実施する(データ登録・更新・削除など)
ただ、そのビジネスロジックの API 連携は上のレイヤーに emit しない(1つの機能を表現するというルールを壊すことになるため)
機能(ブロック)の単位に関しては、例えばヘッダ・フッタなどで1つのブロックとして定義する。
このようなルールをもとにコンポーネント作成を実施すると空データの場合(ワイヤーフレーム)を考慮できたり、ビジネスロジックがまとまったりと直感的にコンポーネント作成が実現できると思っている。(また新たな問題・疑問にぶつかるかもだがw)
Templates(テンプレート)
こちらはワイヤーフレームを表現するレイヤーとなるが、個人的に Vue / Nuxt ではいらないと思っている。
こちらはここまで作成してきた Organisms のコンポーネントを組み合わせるように作成する。
配置など、レイアウトを意識してコンポーネントを組み合わせる。
ルールとしては下記のように定義した。
- API 連携はしない
- Pages から流れてくるデータを Organisms に分配する
- 似たような画面にも適応できるようデータは汎用的にされたデータを Organisms に流す(どちらかというと Organisms に規則な気がするがw)
Templates はシンプルにする必要があり、script など(ロジック)は記述しない。
スタイルに対し、layout に関する記述のみとする。
Pages(ページ)
Pages はそのページを表現するためのコンポーネントとなるが主に Templates にデータを流し込むだけのシンプルな定義となる。
本レイヤーのルールとしては下記のように定義した。
- Templates に fetch data を props で渡す
- ページに関わるものはこのレイヤーで扱う(query param など)
- Templates コンポーネントを1つ定義する
- ページの大枠で表現するために必要な UI ロジックを持つ
ここでの fetch data とはコンテンツを意味すると個人的には定義している。
そのコンテンツを Templates に流し込むことで、 Atomic Design のデザイン手法を維持している。
Nuxt などでは、 asyncData などを用いて fetch し、算出プロパティ(computed)でデータを props で渡すような記述になるかと思われる。
以下のサンプルは vue での Pages コンポーネントとなる。store に関しては、ちょっと適当に書いているので、そこはご了承くださいw
<template>
<Suspense>
<template #default>
<o-data-list :data="data" />
</template>
<template #fallback>
loading...
</template>
</Suspense>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import ODataList from '@/components/03-organisms/ODataList.vue';
import store from './store';
export default defineComponent({
components: {
ODataList
},
async setup() {
await store.dispatch();
const data = computed(() => store.getters());
return {
data
}
}
});
</script>
まとめ
ここまでざっくりまとめてみたが、なんか書き足りない感じもするw
今後開発していく上で Atomic Design はまだ課題が出てくると思うが、その時はまた調査したりしていこうと思います。
新たに出た課題に対する調査結果や、コメントいただけたりした場合は、随時更新していくつもりです〜。
こちらの記事で何か参考になれば個人的にすごい嬉しく思います!