Edited at

Vue.jsでAtomic Designを実践する

DevicebookというNuxt.jsとLaravelを使ったWebサービスを個人開発しています。

最近、「うわっ、わたしのコンポーネント設計、見づらすぎ・・・?」と思いはじめたため、重い腰をあげてAtomic Designを導入しはじめました。


Atomic Deisngのメリット

Atomic Designを導入すれば、コンポーネントの設計に統一感をもたらすことができて、再利用性が高まるという恩恵を受けられます。

今まではVue.jsのスタイルガイドに準拠しながら設計をしていましたが、個人開発サービスの規模感でも、そろそろ限界が見えはじめました。


  • コンポーネントの設計が、機能や開発時期によってバラつきがあるため、可読性が低い。

  • コンポーネントが他のコンポーネントやVuexと密結合になっているため、再利用しにくい。

つまり中規模段階に入るとVue.jsのスタイルガイドに限界が見え始める、ということです。こういった時期に導入したのがAtomic Designです。Atomic Designとは、全ての要素を5つに分解して組み合わせていくデザイン手法のことです。Atomic Designの詳細については、他の記事やスライドが参考になります。

ちなみにnote.muやDMMでも、Vue.jsの設計にAtomic Designが取り入れられている模様です。


早速Atomic Designを実践してみる

前置きが長くなりましたが、とりあえず実践してみます。

今回は「入力されたAmazonのURLから商品情報を表示する」という機能をAtomic Designに基づいて設計してみます。

e85c2681fcd0d234f51fdd62d4c621b3.gif

コンポーネントの粒度としては、以下のようにします。


  • Atom:<input>に汎用的な処理を加える

  • Molecule:<input>にAmazonのURL処理を加える

  • Organism:Moleculeにドメイン処理を加える

それでは、AtomからOrganismまで実際のコードを見ていきます。


Atoms

まずはAtomsですが、私の場合はUIフレームワークであるVuetifyに用意されているコンポーネントを利用しています。

今回におけるAtomは、こちらのtext-fieldsコンポーネントです。

https://vuetifyjs.com/ja/components/text-fields

https://github.com/vuetifyjs/vuetify/tree/v1.3.8/packages/vuetify/src/components/VTextField

Atomsは基本的にpropsとして渡したものをレンダリングします。内部でStateなどは持ちません。このような汎用的で抽象性の高いコンポーネント(Atoms)は基本的に自分で実装せず、OSSとして公開されているライブラリを積極的に活用することが望ましいです。


Molecules

次にMoleculesです。Moleculesの主な役割としては、Atomsのイベントハンドリングです。親コンポーネントから値を受け取り、Atomsとの橋渡し的な役割を担います。

さて、実際にコードを見てみます。このコンポーネントは、入力されたAmazonのURLからASINというAmazon固有の商品IDを抜き出し、それを親コンポーネントにイベントとして発行します。


~/components/molecules/AmazonUrlInputExtractAsin.vue

<template>

<v-text-field
v-model="input"
:error-messages="errorMessage ? [errorMessage] : []"
:disabled="disabled"
:append-icon="input ? 'close' : undefined"
@click:append="$emit('resetInputUrl')"
label="AmazonのURLを入力"
prepend-icon="link"
/>
</template>

<script>
import isUrl from 'is-url'

export default {
props: {
value: {
type: String,
required: true
},

disabled: {
type: Boolean,
required: true
}
},

computed: {
input: {
get () {
return this.value
},

set (input) {
this.$emit('input', input)
}
},

isJPAmazonUrl () {
if (typeof this.input !== 'string') {
return false
}

const regex = RegExp('^https://www.amazon.co.jp')
return Boolean(this.input.match(regex))
},

extractedAsin () {
if (this.isJPAmazonUrl) {
const regex = RegExp('https://www.amazon.co.jp/([\\w-%]+/)?(dp|gp/product)/(\\w+/)?(\\w{10})')
const match = this.input.match(regex)
return match ? match[4] : null
}

return null
},

errorMessage () {
if (!this.input) {
return
}

if (!isUrl(this.input)) {
return 'URLを入力してください。'
}

if (!this.isJPAmazonUrl) {
return 'Amazon.co.jp(日本)にのみ対応しています。'
}

if (this.input.length > 15 && !this.extractedAsin) {
return 'URLが正しくありません。'
}
}
},

watch: {
extractedAsin (extractedAsin) {
this.$emit('extractedAsin', extractedAsin)
}
}
}
</script>


Vue.jsのwatchで、extractedAsinが変わるたびにthis.$emit('extractedAsin', extractedAsin)でイベントを発行しています。

ちなみにASINを抜き出すロジックがコンポーネント内にべた書きされていますが、本当は.jsファイルとして切り出し、テストしやすいようにするのが理想です。

Nuxt.jsの場合は公式にJestとVueTestUtilsのexampleがあるのでそちらがテスト導入に参考になります。

https://github.com/nuxt/nuxt.js/tree/dev/examples/jest-vtu-example


Organisms

さて、OrganismsではMoleculesやAtomsのイベントハンドリングをし、必要であればVuexなども使います。基本的にAtomsやMoleculesはOSSとして公開してもいいレベルで抽象化しますが、Organismsはドメインに関する処理を入れても構いません

以下の例では、先ほど作ったMolecules(AmazonUrlInputExtractAsin.vue)のイベントを受け取ってAPIを叩き、取得した商品のタイトルを表示するようにしています。


~/components/organisms/AmazonDevice.vue

<template>

<div>
<AmazonUrlInputExtractAsin
v-model="inputUrl"
hint="URLから簡単にデバイスを取得できます"
:disabled="fetchingDevice || sendingForm"
@extractedAsin="(asin) => fetchDeviceByAsin(asin)"
@resetInputUrl="inputUrl = ''"
/>

<template v-if="fetchingDevice">
<div class="text-xs-center mt-3">
<v-progress-circular
:size="50"
indeterminate
color="primary"/>
<div class="title mt-3">URLから取得しています…</div>
</div>
</template>

<p>{{ fetchedData.device.title }}</p>
</div>
</template>

<script>
import FormHandleable from '~/mixins/form/form-handleable'
import AmazonUrlInputExtractAsin from '~/components/molecules/AmazonUrlInputExtractAsin'

export default {
components: { AmazonUrlInputExtractAsin },

data () {
return {
inputUrl: '',
lastSearchedAsin: '',
fetchingDevice: false,
fetchedData: null
}
},

computed: {},

methods: {
async fetchDeviceByAsin (asin) {
if (this.fetchingDevice || !asin || this.lastSearchedAsin === asin) {
return
}

this.fetchingDevice = true

await this.$axios.$get('/my-search-api', { params: { asin } })
.then(data => {
this.fetchedData = data
this.lastSearchedAsin = asin
})
.finally(() => {
this.fetchingDevice = false
})
},

sentRequest () {
this.fetchedData = null
this.inputUrl = ''
this.lastSearchedAsin = ''
}
}
}
</script>


例を見て分かると思いますが、Moleculesがうまく抽象化されており、非常に使いやすくなっています。もちろん、自分がどのように再利用するかで、Moleculesの債務は柔軟に変更しても構いません。

またOrganismsより上の層ですが、Nuxt.jsの場合はlayoutsがTemplates、pagesがそのままpagesに当てはまります。


おわりに

以上が実践Atomic Designです。このデザイン手法は、ある程度の規模を見越しているならば、開発初期から導入するべきです。またいずれフロントでのチーム開発を考えているならば、とても役に立つものです。

Atomic Designに慣れないうちは、悩み、そして本当にこれでいいのかといったプロセスを歩むと思います。けれどこういったコンフォートゾーン(自分が楽だと思う領域)から抜け出すための時間というのは、自分の成長においてとても有意義な時間だと私は思っています。