こんにちは。アイレットデザイン事業部のマークアップ/フロントエンドエンジニアの工藤です。アイレットデザイン事業部ではINSIDE UI/UXと題して、所属デザイナーとエンジニアがデザイン・SEO・アクセシビリティ・UI/UXなどそれぞれスペシャリティのある領域に対する知見を幅広く発信しています。
今回はVue.js再入門シリーズ第2回となります。

コンポーネント

Vue.jsにおける コンポーネント は、Webアプリケーションのビューを部品化して再利用可能な単位に分割する仕組みです。

より簡単にいうと、Webページを開発する際にヘッダー、フッター、ナビゲーション、メインコンテンツ…など、それぞれのUI部品を作り、再利用しやすくしようというわけです。

たとえば、ヘッダーコンポーネントは以下のようになります。

<template>
<header>
<h1>{{ title }}</h1>
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
</header>
</template>

<script>
export default {
name: 'Header',
props: {
title: {
type: String,
required: true
}
}
}
</script>

<style scoped>
header {
background-color: #eee;
padding: 1rem;
}

nav {
display: flex;
justify-content: space-between;
align-items: center;
}

ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
}

li {
margin: 0 1rem;
}
</style>

上記のコードはHTML、CSS、JavaScriptのコードが1つのファイルにまとまっているのがわかりますね。このファイル形式は 単一ファイルコンポーネント(Single File Components) と呼ばれています。
Vue.jsはなぜこの形式を採用しているのでしょうか?

関心の分離

これは 関心の分離(かんしんのぶんり、英語: separation of concerns、SoC) というソフトウェア工学の思想で、プログラムを関心(何をしたいのか)毎に分離された構成要素で構築する方が良いとする考え方が基になっています。

1つの大きなHTMLファイル(5000行)、1つの大きなCSSファイル(2万行)…のようにするよりも、単一ファイルコンポーネントにする(関心の分離を行う)ことでメンテナンス性や再利用性を向上させることを目指すわけです。

さて、単にHTMLファイルを分割しコンポーネント化するということならpugやNunjucksといった先行技術がありました。Vue.jsのコンポーネント機能はそれらとどう異なるのでしょうか?

Vue.jsのコンポーネント機能は メソッド算出プロパティライフサイクルフック を定義したり、 親コンポーネントからpropsと呼ばれるプロパティを受け取る ことができ、より多機能で応用の幅が広いのです。実際の開発では、これら機能や複数コンポーネントを再利用することで効率化を図ることが開発者の腕の見せ所となります。

コンポーネントの定義と呼び出し

Vue.jsにおいて、コンポーネントは再利用可能な機能単位であり、それぞれ独立して設計されます。コンポーネントの定義は先ほど紹介した単一ファイルコンポーネントの他に、グローバルコンポーネントとローカルコンポーネントの3種類が存在します。

単一ファイルコンポーネントの定義

<template>
<!-- テンプレートの定義を書く -->
</template>

<script>
export default {
// コンポーネントのオプションを書く
}
</script>

<style>
/* スタイルの定義を書く */
</style>

グローバルコンポーネントの定義

グローバルコンポーネント(グローバル登録)は、Vueインスタンスが作成された時点ですべてのコンポーネント内で利用できるものです。グローバルコンポーネントの定義は、Vue.jsの Vue.component() メソッドを使用して行います。

Vue.component('component-name', {
// ここにコンポーネントのオプションを書く
});

ローカルコンポーネントの定義

ローカルコンポーネント(ローカル登録)は、Vueインスタンス内でのみ利用できるものであり、そのコンポーネントが宣言されたコンポーネント内でのみ利用できます。ローカルコンポーネントの定義は、コンポーネントオプションの components プロパティを使用して行います。
ローカル登録は、必要なときにのみ登録するため、アプリケーション全体のパフォーマンスの向上にもつながりますし、コンポーネントの名前空間が衝突しないというメリットがあります。

export default {
components: {
'component-name': {
// ここにコンポーネントのオプションを書く
}
}
}

コンポーネントのスコープ

コンポーネントのスコープとはデータの有効範囲のことです。コンポーネントで定義された情報にアクセスできるのは原則そのコンポーネントのみということです。コンポーネント外からはアクセスできません。

<template>
<div>
<child-component></child-component>
<p>{{ message }}</p>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent
},
data() {
return {
message: 'Hello World'
}
}
}
</script>

この場合、親コンポーネントで定義されたmessageは、親コンポーネント内で使用されていますが、子コンポーネント内では使用することができません。

プロパティ

親コンポーネントで定義したデータを子コンポーネントで使用するためには、プロパティを使用する必要があります。子コンポーネント内でpropsオプションを使用してプロパティを受け取ります。
propsオプションは、オブジェクトまたは配列を返す関数として定義されます。propsに指定できるデータ型は、StringNumberBooleanObjectArrayFunctionSymbolのいずれかです。

props down, event up

コンポーネント間でデータを受渡する際に、時として親のデータを子に、あるいは子のデータを親にという通信が発生します。このときpropsは親=>子にしか伝達されず、逆にeventは子=>親にしか伝達されない。これは props down, event up と呼ばれています。

以下のコードは基本的なprops down, event upの例です。

親コンポーネント(ParentComponent.vue)

<template>
<div>
<child-component :message="message" @update-message="updateMessage"></child-component>
</div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
components: {
ChildComponent
},
data() {
return {
message: 'Hello from parent component'
}
},
methods: {
updateMessage(newMessage) {
this.message = newMessage;
}
}
}
</script>

子コンポーネント(ChildComponent.vue)

<template>
<div>
<p>{{ message }}</p>
<button @click="updateParentMessage">Update parent message</button>
</div>
</template>

<script>
export default {
props: {
message: {
type: String,
required: true
}
},
methods: {
updateParentMessage() {
const newMessage = 'Hello from child component';
this.$emit('update-message', newMessage);
}
}
}
</script>

この例では、親コンポーネントで定義されたmessageをpropsで子コンポーネントに渡して表示しています。また、子コンポーネント内のボタンがクリックされると、updateParentMessageメソッドが呼び出され、$emitメソッドでupdate-messageイベントを発火します。このイベントは、親コンポーネントで定義されたupdateMessageメソッドを呼び出し、新しいメッセージを渡すことで、親コンポーネントのデータを更新しています。

以上がコンポーネント間通信の基本的な考え方になります。

所感

Vue.jsは関心の分離という設計思想をもとに、スコープを明確化することに重きを置いており、コンポーネント機能でそれを実現したフロントエンドの設計ということがわかりました。

Webフロントエンドは一方ではCSSのBEM記法など、1つの大きなファイルになっても破綻させない設計思想を進化させてきましたが、Vue.jsはこれとはまったく異なる考え方であるため、BEM記法に慣れてきた人ほど逆にとっつきにくい面もあるかもしれません。Vue.jsの学習では独自の記法を覚えることも大切ですが、それ以上に概念の理解が大切だと感じました。

参考資料