まえがき
VueでAtomicデザインの考え方を取り入れてAtomに相当する小さなコンポーネントを作っていくときに気をつけておきたいパターンです
Atomに相当する部分のコンポーネントは、コード量や複雑度は高くないですが最も多くの依存を受けるため
一度コンポーネントのインターフェイスが固まってしまうと、後から変更するのが難しい傾向にあります。
常に利用するときの意識を持ちながらコンポーネントを作るのが大事です
※ おまけでStorybookにカタログを作っていく際に気をつけることも書いています。
Vueコンポーネント編
パターン1 : emitされるeventの不足
問題
コンポーネントを利用する際にあってほしいイベントが受け取れない
例
- Buttonなのにclickイベントが発行されない
<template>
<div>
<!--このボタンのclickイベントは外からキャッチできない-->
<input type="button" value="button" />
</div>
</template>
- Inputなのにchangeイベントしかなくinputイベントでリアルタイムな検証ができない
解決策
- 標準のDOMと違和感がないようなイベントをemitする
パターン2:過剰なeventのemit
問題
mouseenter/mouseleaveなど大抵のDOMで利用できるイベントを、細かく自前でemitしなおす実装をしてしまう
デバッグツールに大量のイベントが表示されてしまいノイズになる
こういうコードを書いて
<div @mouseleave="$emit('mouseleave')" >
</div>
解決策
.native
や$listenersを使って必要なeventだけを自分でemitする
参考:コンポーネントにネイティブイベントをバインディング
パターン3:過剰なprops依存
問題
propsが多すぎて、ユーザが使いにくいコンポーネントになる
例
- 表示・非表示をコントロールするだけのpropsを作る(外部からv-ifで切り替えるのと同じ)
<template>
<div v-if="show">
</div>
</template>
<script>
export default {
name: "AppComponent",
props: {
show:Boolean
},
}
解決策
- そのpropsが本当にいるのかをよく考える
上の例で言えば↓のように書けばshowはいらないはず
<app-component v-if="show" />
- slotを使った設計に切り替えてpropsを絞る
パターン4:propsの名前が直感的でない
問題
標準のHTMLやCSSで使われる単語を別の意味のpropsとして受け取れるようになっている
利用者は普段のHTMLと意味が乖離するので困惑する(バグを生みやすい)
例
- buttonのtypeが意味が違う(コンポーネントではデザインのためのpropsだが、標準のbuttonではsubmitなどのロールを担う)
<!-- コンポーネント名を見て、利用者はtypeに'submit'などの標準的なbuttonのattributeが入ることを期待しがち -->
<app-button type="submit" />
<template>
<!-- 実際は全然関係ないところにtypeが使われている -->
<div :class="type">
<button></button>
</div>
</template>
<script>
export default {
name: "AppButton",
props: {
type:String
},
};
</script>
解決策
- 常に標準のHTMLやCSSで利用される値を考慮して、概念と命名の感覚をなるべく揃える
パターン5:レイアウトのためのCSSを内包している
問題
コンポーネントの外から与えたいレイアウトのためのCSS(margin、float、position、z-indexなど)がコンポーネント内部に内包されていて、利用が難しい
例
- z-indexが内包されていて外部からコントロールが難しい
<template>
<div><div class="modal"></div></div>
</template>
<style scoped>
.modal {
z-index: 10000;
}
</style>
解決策
デザインのためのCSSとレイアウトのためのCSSを分けて考える。
レイアウトに使うCSSは「コンポーネントを利用する側」が与えられるような設計にする
<template>
<!-- 外からスタイルを与える -->
<app-modal class="modal">
</template>
<style scoped>
.modal {
z-index: 10000;
}
パターン6:data依存
問題
親に向けてemitすればいいだけの値を、コンポーネント自身のdataに一回保存してしまう
例
↓のAppInputコンポーネントでは初期値しか渡せませんが、初期値以外の値を渡そうとすると、propsの上書きの警告にぶつかることが多く、単純なコードでは実現できなくなることが多いです。
<template>
<div><input type="text" v-model="value" /></div>
</template>
<script>
export default {
name: "AppInput",
props: {
initValue: String
},
data() {
return {
innerValue: this.$props.initValue
};
},
computed: {
value: {
get() {
return this.innerValue;
},
set(value) {
this.innerValue = value;
this.$emit("input", value);
}
}
}
};
</script>
解決策
dataの項目はなるべく減らす
上のAppInputコンポーネントの例では以下のようにすればそもそもdata自体が不要です
<template>
<!-- propsの値をそのまま子供に渡してしまう -->
<div><input type="text" :value="value" @input="onInput" /></div>
</template>
<script>
export default {
name: "AppInput",
props: {
value: String
},
methods: {
onInput(event) {
// 親へは純粋に値の橋渡しをするだけでいい
this.$emit("input", event.target.value);
}
}
};
</script>
Storybook編
storybookを作る目的はUIのカタログを作ると同時に、
利用者が実装の詳細を追わなくてもstorybookを見ればコンポーネントを利用できる状態にすることにあるので、
それが実現されているかどうかを常に考える
パターン1:propsとして渡せる値が分からない
問題
コンポーネントに渡せるプロパティとして何が用意されているのかわからない
解決策
storybook-addon-vue-infoなどを使って、コンポーネントのプロパティを出力する
パターン2:propsの値のバリエーションが分からない
問題
<button type="submit">
のsubmit
のように、プロパティとして渡す値の選択肢が決まっているときに、それがStorybook上からわからない
解決策
-
knobを使う
- 値のパターンが決まっているときはselectやradioなどを使う
- 値のON/OFFのみならcheckboxを使う
- storybook-addon-vue-infoを使ってプロパティの説明をちゃんと書く
パターン3:emitされるイベントが管理されていない
問題
コンポーネントから発行される、(利用を想定している)イベントに対してactionが貼られていない・送られてくるpayloadがわからない
解決策
actionを使って、想定される利用シーンを伝える