nuxt.js に置けるコンポーネント設計の(俺的)ベストプラクティスを考える

2021/09/20

nuxt.js

vue.js

はじめに

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.jscomponents: 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.vuelogic.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 は必ず親からのみ呼び出すなど)

また今回のソースコードは こちら にある。