Skip to content
Code Blue プログラミング学習ノート
Javascript/Vue.js

Vue.jsの単一ファイルコンポーネント(SFC)徹底解説

はじめに

Vue.jsを用いたモダンなフロントエンド開発において、外せない中核機能が「単一ファイルコンポーネント(SFC)」です。HTML、CSS、JavaScriptを1つの「.vue」ファイルに集約するこの仕組みは、単なるコードの整理術にとどまりません。本記事では、SFCがなぜ現代のWeb開発でこれほど重要視され、開発の効率性や保守性にどのような劇的変化をもたらすのか、その核心となるメリットを分かりやすく解説します。

単一ファイルコンポーネントとは

単一ファイルコンポーネント(Single File Component、以下SFC)は、Vue.jsのコンポーネントを.vue拡張子の1つのファイルにまとめる仕組みです。1つのファイルにテンプレート(HTML)、ロジック(JavaScript)、スタイル(CSS)をカプセル化できます。

<template>
  <div class="example">{{ msg }}</div>
</template>

<script>
export default {
  data() {
    return {
      msg: 'Hello world!'
    }
  }
}
</script>

<style>
.example {
  color: red;
}
</style>

SFCの基本構造

SFCは3つの主要なセクションで構成されます。

Templateセクション

<template>は、コンポーネントの画面表示を定義する部分です。HTMLをベースに記述し、データの表示やイベントの設定を行います。このコードでは、タイトルと本文を表示し、ボタンをクリックするとメソッドが実行されます。

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
    <button @click="handleClick">クリック</button>
  </div>
</template>

Scriptセクション

<script>は、コンポーネントの処理を記述する部分です。dataで表示するデータを定義し、methodsでボタンクリック時の処理などを実装します。このコードでは、ボタンを押すとアラートを表示します。

<script>
export default {
  name: 'MyComponent',
  data() {
    return {
      title: 'コンポーネントのタイトル',
      content: 'これはコンポーネントの内容です'
    }
  },
  methods: {
    handleClick() {
      alert('ボタンがクリックされました');
    }
  }
}
</script>

Styleセクション

<style>は、コンポーネントの見た目を定義する部分です。scopedを付けることで、CSSの適用範囲をこのコンポーネントだけに限定できます。このコードでは、見出しやボタンの色や余白などを設定しています。

<style scoped>
h1 {
  color: #42b983;
}
button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
}
</style>

SFCの利点

  1. 関心の分離:関連するコードを1つのファイルにまとめながら、論理的なセクションに分離
  2. スコープ付きCSSscoped属性でコンポーネント専用のスタイルを定義可能
  3. プリプロセッササポート:TypeScript、SCSS、Pugなど様々なプリプロセッサを使用可能
  4. モジュール化:コンポーネントを簡単にインポート/エクスポート可能
  5. 開発体験の向上:IDEやツールのサポートが充実

コンポーネントの登録と使用

グローバル登録

グローバル登録は、コンポーネントをアプリケーション全体で利用できるようにする方法です。一度Vue.component()で登録すれば、どのコンポーネントからでも読み込み不要で使用できます。

// main.js
import Vue from 'vue';
import MyComponent from './MyComponent.vue';

Vue.component('MyComponent', MyComponent);

new Vue({
  el: '#app'
});

ローカル登録

ローカル登録は、特定のコンポーネント内だけで利用する方法です。componentsオプションに登録することで、そのコンポーネント内でのみ使用でき、必要な場所だけで利用したい場合に適しています。

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

export default {
  components: {
    MyComponent
  }
}
</script>

プロップスの使用

親コンポーネントから子コンポーネントへデータを渡す例です。propsは、親コンポーネントから子コンポーネントへデータを渡す仕組みです。このコードでは、親が持つメッセージを子へ渡し、子コンポーネントで表示しています。また、データ型や必須項目、デフォルト値も設定できます。

<!-- ParentComponent.vue -->
<template>
  <ChildComponent :message="parentMessage" />
</template>

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

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentMessage: '親からのメッセージ'
    }
  }
}
</script>
<!-- ChildComponent.vue -->
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  props: {
    message: {
      type: String,
      required: true,
      default: 'デフォルトメッセージ'
    }
  }
}
</script>

カスタムイベントの使用

子コンポーネントから親コンポーネントへイベントを発行する例です。カスタムイベントは、子コンポーネントから親コンポーネントへ通知を送るための仕組みです。子コンポーネントが$emit()でイベントを発行し、親コンポーネントがそのイベントを受け取って処理を実行します。親子間で双方向に情報をやり取りする際によく利用されます。

<!-- ChildComponent.vue -->
<template>
  <button @click="notifyParent">親に通知</button>
</template>

<script>
export default {
  methods: {
    notifyParent() {
      this.$emit('child-event', '子からのデータ');
    }
  }
}
</script>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent @child-event="handleChildEvent" />
</template>

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

export default {
  components: {
    ChildComponent
  },
  methods: {
    handleChildEvent(data) {
      console.log('子コンポーネントから:', data);
    }
  }
}
</script>

スロットの使用

スロットは、親コンポーネントから子コンポーネントへ表示内容を渡す仕組みです。子コンポーネント側でを定義し、親コンポーネントで囲んだHTMLがその位置に表示されます。内容が指定されていない場合は、デフォルトコンテンツが表示されます。

<!-- ChildComponent.vue -->
<template>
  <div class="container">
    <h2>タイトル</h2>
    <slot>デフォルトコンテンツ</slot>
  </div>
</template>
<!-- ParentComponent.vue -->
<template>
  <ChildComponent>
    <p>ここがスロットに挿入されるコンテンツです</p>
  </ChildComponent>
</template>

名前付きスロット

名前付きスロットは、複数のスロットを定義する場合に使用します。ヘッダー・メイン・フッターなど複数の表示領域を持つコンポーネントを作成するための機能です。親コンポーネントはv-slotを使用して、それぞれのスロットへ異なる内容を渡すことができます。

<!-- BaseLayout.vue -->
<template>
  <div class="container">
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
<!-- ParentComponent.vue -->
<template>
  <BaseLayout>
    <template v-slot:header>
      <h1>ヘッダーコンテンツ</h1>
    </template>

    <p>メインコンテンツ</p>

    <template v-slot:footer>
      <p>フッターコンテンツ</p>
    </template>
  </BaseLayout>
</template>

動的コンポーネント

動的コンポーネントは、<component>タグのis属性を利用して、表示するコンポーネントを切り替える機能です。このコードでは、ボタンをクリックすると表示するコンポーネントがComponentAとComponentBで切り替わります。

<template>
  <div>
    <button @click="currentComponent = 'ComponentA'">Aを表示</button>
    <button @click="currentComponent = 'ComponentB'">Bを表示</button>

    <component :is="currentComponent"></component>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

export default {
  components: {
    ComponentA,
    ComponentB
  },
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  }
}
</script>

ミックスインの使用

ミックスインは、複数のコンポーネントで共通のデータやメソッドを再利用するための仕組みです。このコードでは、myMixinで定義したデータやメソッドをコンポーネントへ取り込み、同じ処理を簡単に共有しています。これにより複数のコンポーネントでロジックを共有できます。

// myMixin.js
export default {
  data() {
    return {
      mixinData: 'ミックスインのデータ'
    }
  },
  methods: {
    mixinMethod() {
      console.log('ミックスインのメソッド');
    }
  }
}
<!-- MyComponent.vue -->
<script>
import myMixin from './myMixin.js';

export default {
  mixins: [myMixin],
  created() {
    console.log(this.mixinData); // ミックスインのデータ
    this.mixinMethod(); // ミックスインのメソッド
  }
}
</script>

カスタムディレクティブ

カスタムディレクティブは、独自のHTML属性を作成して要素の動作を拡張する機能です。このコードでは、v-highlightディレクティブを定義し、指定した色で要素の背景色を変更しています。コンポーネント内でカスタムディレクティブを定義できます。

<template>
  <div v-highlight="'yellow'">このテキストはハイライトされます</div>
</template>

<script>
export default {
  directives: {
    highlight: {
      inserted(el, binding) {
        el.style.backgroundColor = binding.value;
      }
    }
  }
}
</script>

ベストプラクティス

  1. コンポーネント命名規則:パスカルケース(例: MyComponent.vue
  2. 単一責任の原則:1コンポーネントにつき1つの責務
  3. 適切な分割:大きくなりすぎたコンポーネントは分割
  4. propsバリデーション:propsの型と必須チェックを実装
  5. scopedスタイル:スタイルの衝突を防ぐ
  6. ドキュメンテーション:props、events、slotsをコメントで説明

ベストプラクティスを取り入れた例です。

<!-- 
@component MyButton
@desc カスタムボタンコンポーネント
@prop {String} type - ボタンのタイプ (primary|secondary)
@prop {Boolean} disabled - 無効状態
@event click - クリックイベント
@slot default - ボタン内のコンテンツ
-->
<template>
  <button 
    :class="['my-button', type]"
    :disabled="disabled"
    @click="$emit('click')"
  >
    <slot></slot>
  </button>
</template>

<script>
export default {
  name: 'MyButton',
  props: {
    type: {
      type: String,
      default: 'primary',
      validator: value => ['primary', 'secondary'].includes(value)
    },
    disabled: {
      type: Boolean,
      default: false
    }
  }
}
</script>

<style scoped>
.my-button {
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}
.my-button.primary {
  background-color: #42b983;
  color: white;
}
.my-button.secondary {
  background-color: #f0f0f0;
  color: #333;
}
.my-button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
</style>

propsでボタンの種類(primaryまたはsecondary)や無効状態を受け取り、指定された内容に応じて見た目や動作を切り替えます。

また、<slot>を使用してボタン内の文字や要素を自由に設定でき、クリック時には$emit(‘click’)によって親コンポーネントへクリックイベントを通知します。scopedを指定したCSSにより、このコンポーネント専用のスタイルが適用されるため、他のコンポーネントへ影響を与えずに利用できます。

まとめ

Vue.jsの単一ファイルコンポーネント(SFC)は、テンプレート・スクリプト・スタイルを1つの.vueファイルにまとめて管理できる仕組みです。

表示・処理・デザインを論理的に整理できるため、開発や保守がしやすくなります。また、コンポーネントを簡単に再利用できるほか、scopedによってスタイルの競合を防ぐことも可能です。さらに、Vue CLIやViteなどの開発ツールとの相性が良く、コンポーネント単位でテストしやすい点も大きな特徴です。

SFCを適切に活用することで、より整理された、メンテナンス性の高いVue.jsアプリケーションを構築できます。