今回のテーマ
コンポーネント(Component)ってなんぞや?
コンポーネントとは、いわゆる「部品」です。ちょっと難しく言うと「独立した、再利用可能な部品」のことです。 例えば、プログラミングから離れますが「ハンコ」をイメージしてください。自分の名前が入ったハンコです。署名のとき、毎回手書きでサインするのだるいですよね?4回5回となるとめんどくさくなります。 ですがハンコならポンっ!で終わります。楽できますね! 他にも「日付付きハンコ」で考えてみると、今日の日付を毎回「西暦●年▲月■日」って書くのは大変です。書類10枚にそれぞれやるのはしんどいです。 ですがハンコならポンっ!で終わります。楽できますね!しかも日付をいじれば違う日でも使えます。明日も来週も楽できますね!
と、いうようにコンポーネントは「楽するためのモノ」のイメージで大丈夫です! 具体例でいうと、以下のような「動画のサムネカード」を作るとします。
<template lang="pug">
.card-image
figure.image.is-16by9
img(src="画像" alt="Thumbnail")
.media.pt-2
.media-left
figure.image.is-32x32
img.is-rounded(src="画像" alt="Channel image")
.media-content.has-text-left
p.subtitle.is-6.m-0.mb-2 タイトル
p.subtitle.is-7.has-text-grey.m-0 チャンネル名
p.subtitle.is-7.has-text-grey.m-0 0 回視聴・1時間前
</template>
これをとあるページで一覧として3 ✖️ n(3列n行)で表示しようとすると以下のようになります。
<template lang="pug">
ul.columns.is-multiline.p-4
li.column.is-one-third
.card-image
figure.image.is-16by9
img(src="画像" alt="Thumbnail")
.media.pt-2
.media-left
figure.image.is-32x32
img.is-rounded(src="画像" alt="Channel image")
.media-content.has-text-left
p.subtitle.is-6.m-0.mb-2 タイトル1タイトル1タイトル1タイトル1タ
p.subtitle.is-7.has-text-grey.m-0 チャンネル名1チャンネル名1チャンネル名1
p.subtitle.is-7.has-text-grey.m-0 0 回視聴・7時間前
li.column.is-one-third
.card-image
figure.image.is-16by9
img(src="画像" alt="Thumbnail")
.media.pt-2
.media-left
figure.image.is-32x32
img.is-rounded(src="画像" alt="Channel image")
.media-content.has-text-left
p.subtitle.is-6.m-0.mb-2 タイトル1タイトル1タイトル1タイトル1タ
p.subtitle.is-7.has-text-grey.m-0 チャンネル名1チャンネル名1チャンネル名1
p.subtitle.is-7.has-text-grey.m-0 0 回視聴・7時間前
li.column.is-one-third
.card-image
figure.image.is-16by9
img(src="画像" alt="Thumbnail")
.media.pt-2
//- 〜〜〜
</template>
はい。大変ですね。ただでさえ3個でも30行以上も同じものを書かなければならないのは苦痛です。もしこれが3 ✖️ 5であれば150行以上も、、、 そこで「コンポーネント」です。実際にコンポーネントを使った場合はこんな感じです。
<template lang="pug">
ul.columns.is-multiline.p-4
li.column.is-one-third
MovieCard
li.column.is-one-third
MovieCard
li.column.is-one-third
MovieCard
//- 〜〜〜
</template>
<template lang="pug">
//- ※これはMovieCard.vue
.card-image
figure.image.is-16by9
img(src="画像" alt="Thumbnail")
.media.pt-2
.media-left
figure.image.is-32x32
img.is-rounded(src="画像" alt="Channel image")
.media-content.has-text-left
p.subtitle.is-6.m-0.mb-2 タイトル1タイトル1タイトル1タイトル1タ
p.subtitle.is-7.has-text-grey.m-0 チャンネル名1チャンネル名1チャンネル名1
p.subtitle.is-7.has-text-grey.m-0 0 回視聴・7時間前
</template>
MovieCard というコンポーネントを作りそれを一覧ページに反映させました。つまり、サムネイルの部分を「MovieCard」という"部品"として扱えるようにして、それを一覧ページでハンコのようにポチポチできるようにしました。 では、実際どうすればコンポーネントが作れるのか解説しましょう。 Nuxt3でコンポーネントを作るにはまず、components フォルダーを作ります。そしてその中にvueファイルを作ります。これがコンポーネントになります。(今回は MovieCard.vueを作りました。) あとはこれを使いたいところでファイル名でタブとして出す(今回はMovieCard (Html的には <MovieCard />))これだけです! ※v-forを使うともっとコンパクトになります。
でもこれだと"全部同じサムネ、タイトル"しか出ないし某動画サイトのようないろんな内容にはならなくない? そうです、今のままでは「ただのハンコ」状態です。これを「日付付きハンコ」のような"汎用的な部品"にします。 必要なものは、「props」です。これは、コンポーネントに値を渡すものです。厳密に言えば「親コンポーネントから子コンポーネントに値を渡す」ものです(←あまり深く考えなくて大丈夫です。) このpropsを利用して指定した値をコンポーネントに渡すことができます。日付付きハンコで言うところの「日付を設定しておく」とおんなじことです。 では実際に例を見ていきましょう。movieListにあるサムネ情報を全て出すとこうなります。
<template lang="pug">
ul.columns.is-multiline.p-4
li.column.is-one-third(v-for="movie in movieList" :key="movie")
MovieCard(:movie="movie")
</template>
<script setup>
let movieList = [
{
title: "タイトル1",
thumbnail: "画像",
viewCount: 0,
publishedAt: new Date(2023, 10, 10),
channel: {
name: "チャンネル名1",
thumbnail: "画像",
},
},
{
title: "タイトル2",
thumbnail: "画像",
viewCount: 0,
publishedAt: new Date(2022, 10, 10),
channel: {
name: "チャンネル名2",
thumbnail: "画像",
},
},
{
title: "タイトル3",
thumbnail: "画像",
viewCount: 0,
publishedAt: new Date(2023, 10, 20),
channel: {
name: "チャンネル名3",
thumbnail: "画像",
},
},
{
title: "タイトル4",
thumbnail: "画像",
viewCount: 0,
publishedAt: new Date(2023, 10, 20, 10, 10),
channel: {
name: "チャンネル名4",
thumbnail: "画像",
},
},
];
</script>
<template lang="pug">
//- ※これはMovieCard.vue
.card-image
figure.image.is-16by9
img(:src="movie.thumbnail" alt="Thumbnail")
.media.pt-2
.media-left
figure.image.is-32x32
img.is-rounded(:src="movie.channel.thumbnail" alt="Channel image")
.media-content.has-text-left
p.subtitle.is-6.m-0.mb-2 {{ movie.title }}
p.subtitle.is-7.has-text-grey.m-0 {{ movie.channel.name }}
p.subtitle.is-7.has-text-grey.m-0 {{ movie.viewCount }} 回視聴・{{ publishedAt }}
</template>
<script setup lang="ts">
const props = defineProps<{
movie: any;
}>();
const publishedAt = computed(() => {
const nowDate = new Date();
let diffMilliSec = nowDate.getTime() - props.movie.publishedAt.getTime();
const diffInSecs = Math.floor(diffMilliSec / 1000);
if (diffInSecs < 60) {
return `${diffInSecs}秒前`;
}
const diffInMins = Math.floor(diffMilliSec / 1000 / 60);
if (diffInMins < 60) {
return `${diffInMins}分前`;
}
const diffInHours = Math.floor(diffMilliSec / 1000 / 60 / 60);
if (diffInHours < 24) {
return `${diffInHours}時間前`;
}
const diffInDays = Math.floor(diffMilliSec / 1000 / 60 / 60 / 24);
if (diffInDays < 7) {
return `${diffInDays}日前`;
}
const diffInWeeks = Math.floor(diffMilliSec / 1000 / 60 / 60 / 24 / 7);
if (diffInWeeks < 5) {
return `${diffInWeeks}週間前`;
}
const diffInMonths =
(nowDate.getFullYear() - props.movie.publishedAt.getFullYear()) * 12 +
(nowDate.getMonth() - props.movie.publishedAt.getMonth());
if (diffInWeeks >= 4 && diffInMonths < 2) {
return `1ヶ月前`;
} else if (diffInMonths < 12) {
return `${diffInMonths}ヶ月前`;
}
const diffInYears =
nowDate.getFullYear() - props.movie.publishedAt.getFullYear();
return `${diffInYears}年前`;
});
</script>
詳しく見ていきましょう。まずは一覧ページの方から。 movieListにサムネ情報を配列で作る。 そして、v-for="movie in movieList" :key="movie")としてmovieListの繰り返しを行う。繰り返されるのはliの部分(MovieCardも含まれる) そのMovieCardについている「:movie="movie" 」が今回のpropsでいうところの「データを渡す」サインです。この場合はmovieListの要素「movie」をコンポーネントに渡した、と言う意味です。
次にコンポーネント側(MovieCard.vue)ですが、大きく変わったのは const props = 〜 の部分です。 これこそ今回の肝のpropsです。この部分の意味は、「コンポーネント(MovieCard)はmovieというデータを必要とします(型はany)」です。 それによりこのコンポーネント内にて引数movieを使右ことができます。そのためtitleや画像などは、渡されたmovieのデータをそのまま表示できます。 ※const publishedAt = computed(() => はよくある「●時間前」を算出する部分です。props自体と関連はしていません。
このようにして汎用的なコンポーネントを作ることができます。
まとめ
いかがだったでしょうか?コンポーネントは上手く使うとソースの書く量も見やすさも、改修のしやすさも段違いになります。ぜひぜひ使ってみてください!
