nuxt.js に置けるコンポーネント設計の(俺的)ベストプラクティスを考える
2021/09/20
はじめに
nuxt.js
を書く機会が増えたので(俺的)ベストプラクティスをまとめておく。
Composition Api について
vue3.x 系と typescript との相性を考えて @nuxtjs/composition-api
を使用する。
npm install -D @nuxtjs/composition-api
nuxt.config.js
に 設定を追加
...
{
buildModules: [
...
'@nuxtjs/composition-api/module'
]
}
...
ファイル管理
nuxt.js
は コンポーネントは components/
に配置し簡単に呼び出すことが可能になっている。
例えば components/Hoge/Fuga/Forbar.vue
みたいな vue コンポーネントは
<template>
<HogeFugaForbar> こんにちわ </HogeFugaForbar>
</template>
のように呼び出せる。
なので以下のようなフォルダ構成にしたい
COMPONENTS
├─Common
│ ├─Box
│ │ Item.vue
│ └─ListChild
│ Item.vue
└─ListPost
│ index.vue
└─Item
Logic.vue
Ui.vue
ちなみに nuxt.cofig.js
で components: true,
になってる場合上記のことが可能(default ではそのはず)。
共通デザインコンポーネント
たとえばカードのデザインやリストの間の margin の管理などは compontns/Common
に定義していく。
カードのデザインは以下のように。
<template>
<div class="box">
<div class="box__inner">
<slot></slot>
</div>
<div class="box__outer">
<slot name="outer"></slot>
</div>
</div>
</template>
<style lang="scss" scoped>
.box {
border: 1px solid gray;
border-radius: 20px;
&__inner {
padding: 16px;
}
&__outer {
&:empty {
display: none;
}
border-top: 1px solid gray;
}
}
</style>
リストなどの間のマージンを管理するコンポーネントは以下のように。
<template>
<div class="one">
<slot></slot>
</div>
</template>
<style lang="scss">
.one {
&:not(:first-of-type) {
margin-top: 16px;
}
&:not(:last-of-type) {
margin-bottom: 16px;
}
}
</style>
使用するときは以下のように
<template>
<CommonListPearentItem>
<CommonListChildItem>
<CommonBoxItem>
<p>sample text.</p>
<template slot="outer">
<p>sample text.</p>
</template>
</CommonBoxItem>
</CommonListChildItem>
<CommonListChildItem>
<CommonBoxItem>
<p>no outer item</p>
</CommonBoxItem>
</CommonListChildItem>
</CommonListPearentItem>
</template>
機能を持ったコンポーネントの設計
極力デザインとロジックは分けたい。理由としては、Vue をやめるとき UI とロジックが同じコンポーネント内に存在してると移行がとても難しくなるため。
なので、1 フォルダ内に ui.vue
と logic.vue
を定義する。(別に名前は何でもいいし必ずしも 2 つじゃなくてもいいと思う)
ui.vue
では主に css やデザインで作用する js を定義する。必要なデータは props
を通して受け取るようにする。
例えば以下のようにデザインのみの vue コンポーネントを作成する。
<template>
<CommonBoxItem>
<div class="post">
<div class="post__thumbnail">
<img class="post__thumbnail__img" :src="post.img" />
</div>
<div class="post__contens">
<p class="post__contens__body">{{ post.text }}</p>
</div>
</div>
</CommonBoxItem>
</template>
<style lang="scss" scoped>
.post {
display: grid;
grid-template-columns: 80px 1fr;
grid-gap: 0 16px;
&__thumbnail {
width: 80px;
&__img {
width: 100%;
height: 100%;
}
}
&__contens {
&__body {
margin: 0;
}
}
}
</style>
<script lang="ts">
import { defineComponent, PropType } from "@vue/composition-api";
import { Post } from "@/types/post";
export default defineComponent({
props: {
post: {
type: Object as PropType<Post>,
required: true,
},
},
setup() {},
});
</script>
そしてこのコンポーネントに当て込むデータを加工したりするコンポーネントを作成する。
このコンポーネントで表示前の加工や watch
やデータ変更を VueX
などの状態管理などに流すようにする。
<template>
<ListPostItemUi :post="editedPost" />
</template>
<script lang="ts">
import { defineComponent, PropType, computed } from "@vue/composition-api";
import { Post } from "@/types/post";
export default defineComponent({
props: {
post: {
type: Object as PropType<Post>,
required: true,
},
},
setup(props) {
const editedPost = computed(() => {
const item = props.post;
return item;
});
return {
editedPost,
};
},
});
</script>
またこれらのリスト表示を管理するコンポーネントを作成する。
このコンポーネントではデータ取得などを担当する。(今回はスタブだけど)
<template>
<CommonListPearentItem>
<CommonListChildItem v-for="(post, index) in postList" :key="index">
<ListPostItemLogic :post="post" />
</CommonListChildItem>
</CommonListPearentItem>
</template>
<script lang="ts">
import { defineComponent } from "@vue/composition-api";
import { Post } from "@/types/post";
export default defineComponent({
setup() {
const postList: Post[] = [
{
img: "https://avatars.githubusercontent.com/u/7958925?v=4",
text: "test1",
},
{
img: "https://avatars.githubusercontent.com/u/7958925?v=4",
text: "test2",
},
{
img: "https://avatars.githubusercontent.com/u/7958925?v=4",
text: "test3",
},
];
return {
postList,
};
},
});
</script>
終わりに
今回はコンポーネント設計についてだけにする。vuex
などの設計次第でコンポーネントへと挿入する層を別途考える必要がある。( getters
は必ず親からのみ呼び出すなど)
また今回のソースコードは こちら にある。