Vue.js完全ガイド


Contents

Vue.jsとは

Vue.js(ビュージェーエス)は、ユーザーインターフェイス構築のためのプログレッシブなJavaScriptフレームワークです。Evan You氏によって開発され、2014年に初回リリースされました。

Vue.jsは「プログレッシブフレームワーク」として設計されており、プロジェクトの規模や要件に応じて段階的に導入できることが特徴です。既存のプロジェクトに部分的に導入することも、大規模なSPA(Single Page Application)を一から構築することも可能です。

特徴

1. 学習コストの低さ

Vue.jsはHTML、CSS、JavaScriptの基本的な知識があれば習得しやすく設計されています。テンプレート記法はHTMLに近く、直感的に理解できます。

2. リアクティブデータバインディング

データの変更が自動的にビューに反映される双方向データバインディングを提供します。DOM操作を意識することなく、データの変更だけでUIを更新できます。

3. コンポーネントベース

再利用可能なコンポーネントを作成し、大規模なアプリケーションを効率的に構築できます。各コンポーネントは独自の状態とロジックを持ちます。

4. 優れたパフォーマンス

Virtual DOMを使用し、効率的なDOM更新を実現します。また、コンパイル時の最適化により、実行時のオーバーヘッドを最小限に抑えます。

5. 豊富なエコシステム

Vue Router(ルーティング)、Vuex/Pinia(状態管理)、Vue CLI(開発ツール)など、充実した公式ツールを提供します。

6. TypeScriptサポート

Vue 3では、TypeScriptのサポートが大幅に強化され、型安全な開発が可能です。

歴史

2013年:開発開始

  • Evan You氏がGoogleでAngularJSを使用していた際の経験から、より軽量で学習しやすいフレームワークの必要性を感じて開発開始

2014年2月:Vue.js 0.6リリース

  • 初回公開リリース
  • 基本的なデータバインディングとディレクティブ機能

2014年12月:Vue.js 0.11

  • コンポーネントシステムの導入
  • より構造化されたアプリケーション開発が可能に

2015年10月:Vue.js 1.0

  • 最初の安定版リリース
  • APIの安定化とパフォーマンス改善

2016年9月:Vue.js 2.0

  • Virtual DOMの導入
  • レンダリングパフォーマンスの大幅向上
  • Server Side Rendering(SSR)サポート

2018年:Vue CLIとVue.js 2.6

  • 公式開発ツールの充実
  • フック関数の改善

2020年9月:Vue.js 3.0

  • Composition APIの導入
  • TypeScriptサポートの強化
  • パフォーマンスとTree-shakingの改善
  • 複数のルート要素サポート

2021年2月:Vue.js 3.2

  • <script setup>記法の導入
  • Composition APIの改善

2022年:Vue.js 3.3と3.4

  • TypeScriptサポートの更なる強化
  • パフォーマンス最適化

現在

  • Vue 3が主流となり、活発な開発が継続中
  • 大企業での採用も増加

環境構築と使用方法

1. CDNから直接読み込み

最も簡単な方法です。HTMLファイルに直接スクリプトタグを追加します。

<!DOCTYPE html>
<html>
<head>
  <title>Vue.js App</title>
</head>
<body>
  <div id="app">
    <h1>{{ message }}</h1>
    <button @click="count++">カウント: {{ count }}</button>
  </div>

  <script src="https://unpkg.com/vue@next"></script>
  <script>
    const { createApp } = Vue;
    
    createApp({
      data() {
        return {
          message: 'Hello Vue.js!',
          count: 0
        }
      }
    }).mount('#app');
  </script>
</body>
</html>

2. npm/yarnでのインストール

# npm使用時
npm create vue@latest my-vue-app

# yarn使用時
yarn create vue my-vue-app

3. Viteを使用した新規プロジェクト

npm create vue@latest my-project
cd my-project
npm install
npm run dev

4. Vue CLIを使用(Vue 2系向け)

npm install -g @vue/cli
vue create my-project
cd my-project
npm run serve

基本概念

1. Vue インスタンス

Vue.jsアプリケーションは、Vueインスタンスを作成することから始まります。

// Vue 3
import { createApp } from 'vue'

const app = createApp({
  data() {
    return {
      message: 'Hello World!'
    }
  },
  methods: {
    greet() {
      alert(this.message)
    }
  }
})

app.mount('#app')

2. テンプレート記法

Vue.jsのテンプレートは、拡張されたHTMLです。

<template>
  <div id="app">
    <!-- テキスト補間 -->
    <h1>{{ title }}</h1>
    
    <!-- 属性バインディング -->
    <img :src="imageUrl" :alt="imageAlt">
    
    <!-- 条件レンダリング -->
    <p v-if="isVisible">表示される文章</p>
    <p v-else>代替の文章</p>
    
    <!-- リストレンダリング -->
    <ul>
      <li v-for="item in items" :key="item.id">
        {{ item.name }}
      </li>
    </ul>
    
    <!-- イベントハンドリング -->
    <button @click="handleClick">クリック</button>
    
    <!-- 双方向データバインディング -->
    <input v-model="inputValue" placeholder="入力してください">
  </div>
</template>

3. ディレクティブ

Vue.jsのディレクティブは、DOM要素に特別な動作を与えます。

<template>
  <div>
    <!-- v-if: 条件レンダリング -->
    <p v-if="showMessage">メッセージを表示</p>
    
    <!-- v-show: 要素の表示/非表示 -->
    <p v-show="isVisible">CSS displayで制御</p>
    
    <!-- v-for: リストレンダリング -->
    <div v-for="user in users" :key="user.id">
      {{ user.name }}
    </div>
    
    <!-- v-model: 双方向バインディング -->
    <input v-model="searchQuery" type="text">
    
    <!-- v-bind: 属性バインディング -->
    <a v-bind:href="url">リンク</a>
    <!-- 省略記法 -->
    <a :href="url">リンク</a>
    
    <!-- v-on: イベントリスナー -->
    <button v-on:click="handleClick">クリック</button>
    <!-- 省略記法 -->
    <button @click="handleClick">クリック</button>
  </div>
</template>

4. コンポーネント

Vue.jsでは、UIを独立したコンポーネントに分割できます。

// 子コンポーネント (UserCard.vue)
<template>
  <div class="user-card">
    <img :src="user.avatar" :alt="user.name">
    <h3>{{ user.name }}</h3>
    <p>{{ user.email }}</p>
    <button @click="$emit('follow', user.id)">
      フォロー
    </button>
  </div>
</template>

<script>
export default {
  name: 'UserCard',
  props: {
    user: {
      type: Object,
      required: true
    }
  },
  emits: ['follow']
}
</script>

<style scoped>
.user-card {
  border: 1px solid #ccc;
  padding: 16px;
  border-radius: 8px;
}
</style>

// 親コンポーネント
<template>
  <div>
    <UserCard 
      v-for="user in users" 
      :key="user.id"
      :user="user"
      @follow="handleFollow"
    />
  </div>
</template>

<script>
import UserCard from './components/UserCard.vue'

export default {
  components: {
    UserCard
  },
  data() {
    return {
      users: [
        { id: 1, name: '田中太郎', email: 'tanaka@example.com', avatar: '/avatar1.jpg' },
        { id: 2, name: '佐藤花子', email: 'sato@example.com', avatar: '/avatar2.jpg' }
      ]
    }
  },
  methods: {
    handleFollow(userId) {
      console.log(`ユーザー ${userId} をフォローしました`)
    }
  }
}
</script>

Vue 3の新機能

1. Composition API

Vue 3の最大の新機能です。より柔軟で再利用可能なコードが書けます。

<template>
  <div>
    <h2>{{ title }}</h2>
    <p>カウント: {{ count }}</p>
    <button @click="increment">増加</button>
    <button @click="decrement">減少</button>
    
    <h3>TODO リスト</h3>
    <input v-model="newTodo" @keyup.enter="addTodo" placeholder="新しいTODO">
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        <span :class="{ completed: todo.completed }" @click="toggleTodo(todo.id)">
          {{ todo.text }}
        </span>
        <button @click="removeTodo(todo.id)">削除</button>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

// リアクティブなデータ
const title = ref('Vue 3 Composition API デモ')
const count = ref(0)
const newTodo = ref('')
const todos = ref([])

// 算出プロパティ
const completedTodos = computed(() => 
  todos.value.filter(todo => todo.completed)
)

// メソッド
const increment = () => {
  count.value++
}

const decrement = () => {
  count.value--
}

const addTodo = () => {
  if (newTodo.value.trim()) {
    todos.value.push({
      id: Date.now(),
      text: newTodo.value.trim(),
      completed: false
    })
    newTodo.value = ''
  }
}

const removeTodo = (id) => {
  const index = todos.value.findIndex(todo => todo.id === id)
  if (index > -1) {
    todos.value.splice(index, 1)
  }
}

const toggleTodo = (id) => {
  const todo = todos.value.find(todo => todo.id === id)
  if (todo) {
    todo.completed = !todo.completed
  }
}

// ライフサイクルフック
onMounted(() => {
  console.log('コンポーネントがマウントされました')
  // 初期データの読み込みなど
})
</script>

<style scoped>
.completed {
  text-decoration: line-through;
  color: #888;
}
</style>

2. 複数のルート要素

Vue 3では、コンポーネントのテンプレートに複数のルート要素を持てます。

<template>
  <header>
    <h1>ヘッダー</h1>
  </header>
  <main>
    <p>メインコンテンツ</p>
  </main>
  <footer>
    <p>フッター</p>
  </footer>
</template>

3. Teleport

コンポーネントの一部を別の場所にレンダリングできます。

<template>
  <div>
    <button @click="showModal = true">モーダルを開く</button>
    
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h2>モーダルダイアログ</h2>
          <p>これはモーダルの内容です</p>
          <button @click="showModal = false">閉じる</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const showModal = ref(false)
</script>

エコシステムとフレームワーク

1. Vue Router

SPAのルーティングを管理します。

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import User from '../views/User.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/about', component: About },
  { path: '/user/:id', component: User, props: true }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

<!-- App.vue -->
<template>
  <div>
    <nav>
      <router-link to="/">ホーム</router-link>
      <router-link to="/about">について</router-link>
      <router-link to="/user/123">ユーザー</router-link>
    </nav>
    <router-view />
  </div>
</template>

2. Pinia(状態管理)

Vue 3で推奨される状態管理ライブラリです。

// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Eduardo'
  }),
  
  getters: {
    doubleCount: (state) => state.count * 2
  },
  
  actions: {
    increment() {
      this.count++
    },
    
    async fetchUserData(userId) {
      const response = await fetch(`/api/users/${userId}`)
      const userData = await response.json()
      this.name = userData.name
    }
  }
})

// コンポーネント内での使用
<template>
  <div>
    <p>{{ counter.name }}</p>
    <p>カウント: {{ counter.count }}</p>
    <p>ダブルカウント: {{ counter.doubleCount }}</p>
    <button @click="counter.increment">増加</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()
</script>

3. Nuxt.js

Vue.jsベースのフルスタックフレームワークです。

// nuxt.config.ts
export default defineNuxtConfig({
  devtools: { enabled: true },
  css: ['~/assets/css/main.css'],
  modules: [
    '@nuxtjs/tailwindcss',
    '@pinia/nuxt'
  ]
})

<!-- pages/index.vue -->
<template>
  <div>
    <h1>{{ data.title }}</h1>
    <p>{{ data.description }}</p>
    <NuxtLink to="/about">Aboutページへ</NuxtLink>
  </div>
</template>

<script setup>
// サーバーサイドでデータを取得
const { data } = await $fetch('/api/home')

// SEO設定
useHead({
  title: 'ホームページ',
  meta: [
    { name: 'description', content: 'Vue.js製のウェブサイト' }
  ]
})
</script>

4. Quasar Framework

Vue.jsベースのUIフレームワークです。

// quasar.config.js
module.exports = {
  framework: {
    config: {},
    components: [
      'QLayout',
      'QHeader',
      'QDrawer',
      'QPageContainer',
      'QPage',
      'QBtn',
      'QCard'
    ],
    directives: ['Ripple']
  }
}

実践的なコーディング例

1. 完全なTODOアプリケーション

<template>
  <div class="todo-app">
    <header class="header">
      <h1>TODOアプリ</h1>
      <input
        v-model="newTodo"
        @keyup.enter="addTodo"
        class="new-todo"
        placeholder="新しいタスクを入力..."
        autofocus
      >
    </header>

    <section class="main" v-if="todos.length">
      <ul class="todo-list">
        <li
          v-for="todo in filteredTodos"
          :key="todo.id"
          :class="{ completed: todo.completed, editing: todo === editingTodo }"
          class="todo-item"
        >
          <div class="view">
            <input
              type="checkbox"
              v-model="todo.completed"
              class="toggle"
            >
            <label @dblclick="editTodo(todo)">{{ todo.title }}</label>
            <button @click="removeTodo(todo)" class="destroy">×</button>
          </div>
          <input
            v-if="todo === editingTodo"
            v-model="todo.title"
            @blur="doneEdit(todo)"
            @keyup.enter="doneEdit(todo)"
            @keyup.escape="cancelEdit(todo)"
            class="edit"
            ref="editInput"
          >
        </li>
      </ul>
    </section>

    <footer class="footer" v-if="todos.length">
      <span class="todo-count">
        残り {{ remaining }} 件
      </span>
      <ul class="filters">
        <li>
          <a
            href="#/"
            :class="{ selected: visibility === 'all' }"
            @click="visibility = 'all'"
          >すべて</a>
        </li>
        <li>
          <a
            href="#/active"
            :class="{ selected: visibility === 'active' }"
            @click="visibility = 'active'"
          >未完了</a>
        </li>
        <li>
          <a
            href="#/completed"
            :class="{ selected: visibility === 'completed' }"
            @click="visibility = 'completed'"
          >完了済み</a>
        </li>
      </ul>
      <button
        v-if="todos.length > remaining"
        @click="clearCompleted"
        class="clear-completed"
      >完了済みを削除</button>
    </footer>
  </div>
</template>

<script setup>
import { ref, computed, nextTick, watchEffect } from 'vue'

// リアクティブデータ
const newTodo = ref('')
const todos = ref([])
const editingTodo = ref(null)
const beforeEditCache = ref('')
const visibility = ref('all')

// 算出プロパティ
const filteredTodos = computed(() => {
  switch (visibility.value) {
    case 'active':
      return todos.value.filter(todo => !todo.completed)
    case 'completed':
      return todos.value.filter(todo => todo.completed)
    default:
      return todos.value
  }
})

const remaining = computed(() => 
  todos.value.filter(todo => !todo.completed).length
)

// メソッド
const addTodo = () => {
  const value = newTodo.value.trim()
  if (!value) return

  todos.value.push({
    id: Date.now(),
    title: value,
    completed: false
  })
  newTodo.value = ''
}

const removeTodo = (todo) => {
  const index = todos.value.indexOf(todo)
  todos.value.splice(index, 1)
}

const editTodo = (todo) => {
  beforeEditCache.value = todo.title
  editingTodo.value = todo
  nextTick(() => {
    // DOM更新後にフォーカス
    document.querySelector('.edit').focus()
  })
}

const doneEdit = (todo) => {
  if (!editingTodo.value) return
  
  editingTodo.value = null
  todo.title = todo.title.trim()
  
  if (!todo.title) {
    removeTodo(todo)
  }
}

const cancelEdit = (todo) => {
  editingTodo.value = null
  todo.title = beforeEditCache.value
}

const clearCompleted = () => {
  todos.value = todos.value.filter(todo => !todo.completed)
}

// ローカルストレージとの同期
const STORAGE_KEY = 'vue-todos'

watchEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(todos.value))
})

// 初期データの読み込み
const saved = localStorage.getItem(STORAGE_KEY)
if (saved) {
  todos.value = JSON.parse(saved)
}
</script>

<style scoped>
.todo-app {
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
}

.header h1 {
  text-align: center;
  color: #333;
  margin-bottom: 20px;
}

.new-todo {
  width: 100%;
  padding: 16px;
  font-size: 18px;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-item {
  display: flex;
  align-items: center;
  padding: 12px;
  border-bottom: 1px solid #eee;
}

.todo-item.completed label {
  text-decoration: line-through;
  color: #999;
}

.toggle {
  margin-right: 12px;
}

.view {
  display: flex;
  align-items: center;
  flex: 1;
}

.view label {
  flex: 1;
  cursor: pointer;
}

.destroy {
  background: none;
  border: none;
  color: #cc9a9a;
  font-size: 20px;
  cursor: pointer;
}

.edit {
  width: 100%;
  padding: 8px;
  font-size: 16px;
  border: 1px solid #ddd;
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-top: 20px;
  padding: 10px 0;
  border-top: 1px solid #eee;
}

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

.filters li {
  margin: 0 5px;
}

.filters a {
  text-decoration: none;
  color: #777;
  padding: 3px 7px;
  border: 1px solid transparent;
  border-radius: 3px;
}

.filters a.selected {
  border-color: #ce4646;
}

.clear-completed {
  background: none;
  border: none;
  color: #777;
  cursor: pointer;
}
</style>

2. APIと連携するユーザー管理アプリ

<template>
  <div class="user-management">
    <header>
      <h1>ユーザー管理システム</h1>
      <button @click="showCreateModal = true" class="btn-primary">
        新規ユーザー作成
      </button>
    </header>

    <!-- ローディング -->
    <div v-if="loading" class="loading">
      読み込み中...
    </div>

    <!-- エラー表示 -->
    <div v-else-if="error" class="error">
      {{ error }}
    </div>

    <!-- ユーザーリスト -->
    <div v-else class="user-grid">
      <UserCard
        v-for="user in users"
        :key="user.id"
        :user="user"
        @edit="editUser"
        @delete="deleteUser"
      />
    </div>

    <!-- ユーザー作成/編集モーダル -->
    <UserModal
      v-if="showCreateModal || showEditModal"
      :user="selectedUser"
      :is-editing="showEditModal"
      @save="saveUser"
      @cancel="closeModal"
    />
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import UserCard from './components/UserCard.vue'
import UserModal from './components/UserModal.vue'

// リアクティブデータ
const users = ref([])
const loading = ref(false)
const error = ref(null)
const showCreateModal = ref(false)
const showEditModal = ref(false)
const selectedUser = ref(null)

// API呼び出し関数
const fetchUsers = async () => {
  loading.value = true
  error.value = null
  
  try {
    const response = await fetch('/api/users')
    if (!response.ok) {
      throw new Error('ユーザーの取得に失敗しました')
    }
    users.value = await response.json()
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false
  }
}

const saveUser = async (userData) => {
  try {
    let response
    
    if (showEditModal.value) {
      // 更新
      response = await fetch(`/api/users/${userData.id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
      })
    } else {
      // 新規作成
      response = await fetch('/api/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(userData)
      })
    }

    if (!response.ok) {
      throw new Error('保存に失敗しました')
    }

    await fetchUsers() // リストを再取得
    closeModal()
  } catch (err) {
    alert(err.message)
  }
}

const deleteUser = async (user) => {
  if (!confirm(`${user.name}を削除しますか?`)) return

  try {
    const response = await fetch(`/api/users/${user.id}`, {
      method: 'DELETE'
    })

    if (!response.ok) {
      throw new Error('削除に失敗しました')
    }

    await fetchUsers() // リストを再取得
  } catch (err) {
    alert(err.message)
  }
}

const editUser = (user) => {
  selectedUser.value = { ...user }
  showEditModal.value = true
}

const closeModal = () => {
  showCreateModal.value = false
  showEditModal.value = false
  selectedUser.value = null
}

// ライフサイクル
onMounted(() => {
  fetchUsers()
})
</script>

3. リアルタイムチャットアプリ

<template>
  <div class="chat-app">
    <div class="chat-header">
      <h2>リアルタイムチャット</h2>
      <div class="user-info">
        {{ currentUser.name }} としてログイン中
      </div>
    </div>

    <div class="chat-messages" ref="messagesContainer">
      <div
        v-for="message in messages"
        :key="message.id"
        :class="['message', { 'own-message': message.userId === currentUser.id }]"
      >
        <div class="message-header">
          <span class="username">{{ message.username }}</span>
          <span class="timestamp">{{ formatTime(message.timestamp) }}</span>
        </div>
        <div class="message-content">{{ message.content }}</div>
      </div>
    </div>

    <div class="chat-input">
      <input
        v-model="newMessage"
        @keyup.enter="sendMessage"
        placeholder="メッセージを入力..."
        class="message-input"
      >
      <button @click="sendMessage" :disabled="!newMessage.trim()" class="send-button">
        送信
      </button>
    </div>

    <div class="online-users">
      <h3>オンラインユーザー ({{ onlineUsers.length }})</h3>
      <ul>
        <li v-for="user in onlineUsers" :key="user.id">
          <span class="status-indicator online"></span>
          {{ user.name }}
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
import { io } from 'socket.io-client'

// リアクティブデータ
const messages = ref([])
const newMessage = ref('')
const onlineUsers = ref([])
const messagesContainer = ref(null)
const socket = ref(null)

const currentUser = ref({
  id: 1,
  name: '田中太郎'
})

// WebSocket接続
const connectSocket = () => {
  socket.value = io('ws://localhost:3000')
  
  // 接続時
  socket.value.on('connect', () => {
    console.log('サーバーに接続しました')
    socket.value.emit('user-join', currentUser.value)
  })
  
  // メッセージ受信
  socket.value.on('message', (message) => {
    messages.value.push(message)
    scrollToBottom()
  })
  
  // ユーザーリスト更新
  socket.value.on('users-update', (users) => {
    onlineUsers.value = users
  })
  
  // 切断時
  socket.value.on('disconnect', () => {
    console.log('サーバーから切断されました')
  })
}

// メッセージ送信
const sendMessage = () => {
  const content = newMessage.value.trim()
  if (!content) return

  const message = {
    id: Date.now(),
    userId: currentUser.value.id,
    username: currentUser.value.name,
    content,
    timestamp: new Date()
  }

  socket.value.emit('send-message', message)
  newMessage.value = ''
}

// 時間フォーマット
const formatTime = (timestamp) => {
  return new Date(timestamp).toLocaleTimeString('ja-JP', {
    hour: '2-digit',
    minute: '2-digit'
  })
}

// 自動スクロール
const scrollToBottom = () => {
  nextTick(() => {
    if (messagesContainer.value) {
      messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
    }
  })
}

// 新しいメッセージが追加されたときに自動スクロール
watch(messages, () => {
  scrollToBottom()
}, { deep: true })

// ライフサイクル
onMounted(() => {
  connectSocket()
})

onUnmounted(() => {
  if (socket.value) {
    socket.value.disconnect()
  }
})
</script>

<style scoped>
.chat-app {
  display: grid;
  grid-template-areas: 
    "header header"
    "messages users"
    "input users";
  grid-template-columns: 1fr 200px;
  grid-template-rows: auto 1fr auto;
  height: 100vh;
  max-width: 800px;
  margin: 0 auto;
  border: 1px solid #ddd;
}

.chat-header {
  grid-area: header;
  background: #f5f5f5;
  padding: 16px;
  border-bottom: 1px solid #ddd;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.chat-messages {
  grid-area: messages;
  padding: 16px;
  overflow-y: auto;
  background: white;
}

.message {
  margin-bottom: 16px;
  padding: 8px 12px;
  border-radius: 8px;
  background: #f0f0f0;
}

.own-message {
  background: #007bff;
  color: white;
  margin-left: 20%;
}

.message-header {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  margin-bottom: 4px;
  opacity: 0.7;
}

.message-content {
  font-size: 14px;
}

.chat-input {
  grid-area: input;
  display: flex;
  padding: 16px;
  border-top: 1px solid #ddd;
  background: #f9f9f9;
}

.message-input {
  flex: 1;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-right: 8px;
}

.send-button {
  padding: 8px 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.send-button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.online-users {
  grid-area: users;
  padding: 16px;
  background: #f8f9fa;
  border-left: 1px solid #ddd;
}

.online-users h3 {
  margin: 0 0 12px 0;
  font-size: 14px;
}

.online-users ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.online-users li {
  padding: 4px 0;
  font-size: 12px;
  display: flex;
  align-items: center;
}

.status-indicator {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 8px;
}

.status-indicator.online {
  background: #28a745;
}
</style>

ベストプラクティス

1. コンポーネント設計

Vue.jsでは、適切なコンポーネント設計が重要です。

// ❌ 悪い例:大きすぎるコンポーネント
<template>
  <div>
    <!-- 100行以上のテンプレート -->
  </div>
</template>

<script>
export default {
  data() {
    return {
      // 20個以上のデータプロパティ
    }
  },
  methods: {
    // 30個以上のメソッド
  }
}
</script>

// ✅ 良い例:適切に分割されたコンポーネント
// BlogPost.vue
<template>
  <article class="blog-post">
    <PostHeader :title="post.title" :author="post.author" :date="post.date" />
    <PostContent :content="post.content" />
    <PostFooter :tags="post.tags" :likes="post.likes" @like="handleLike" />
  </article>
</template>

<script setup>
import PostHeader from './PostHeader.vue'
import PostContent from './PostContent.vue'
import PostFooter from './PostFooter.vue'

const props = defineProps({
  post: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['like'])

const handleLike = () => {
  emit('like', props.post.id)
}
</script>

2. Composition APIの活用

ロジックを再利用可能な関数として分離します。

// composables/useApi.js
import { ref } from 'vue'

export function useApi() {
  const loading = ref(false)
  const error = ref(null)

  const request = async (url, options = {}) => {
    loading.value = true
    error.value = null

    try {
      const response = await fetch(url, options)
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      
      const data = await response.json()
      return data
    } catch (err) {
      error.value = err.message
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    loading,
    error,
    request
  }
}

// composables/useUsers.js
import { ref } from 'vue'
import { useApi } from './useApi'

export function useUsers() {
  const users = ref([])
  const { loading, error, request } = useApi()

  const fetchUsers = async () => {
    const data = await request('/api/users')
    users.value = data
  }

  const createUser = async (userData) => {
    const newUser = await request('/api/users', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    })
    users.value.push(newUser)
    return newUser
  }

  const updateUser = async (id, userData) => {
    const updatedUser = await request(`/api/users/${id}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(userData)
    })
    
    const index = users.value.findIndex(user => user.id === id)
    if (index !== -1) {
      users.value[index] = updatedUser
    }
    
    return updatedUser
  }

  const deleteUser = async (id) => {
    await request(`/api/users/${id}`, { method: 'DELETE' })
    users.value = users.value.filter(user => user.id !== id)
  }

  return {
    users,
    loading,
    error,
    fetchUsers,
    createUser,
    updateUser,
    deleteUser
  }
}

// コンポーネントでの使用
<script setup>
import { onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'

const { users, loading, error, fetchUsers, createUser, deleteUser } = useUsers()

onMounted(() => {
  fetchUsers()
})
</script>

3. 型安全性の確保(TypeScript)

Vue 3とTypeScriptを組み合わせることで、型安全な開発が可能です。

// types/user.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: string
}

export interface CreateUserRequest {
  name: string
  email: string
}

// コンポーネントでの使用
<script setup lang="ts">
import { ref, computed } from 'vue'
import type { User, CreateUserRequest } from '@/types/user'

interface Props {
  initialUsers?: User[]
}

interface Emits {
  userCreated: [user: User]
  userDeleted: [userId: number]
}

const props = withDefaults(defineProps<Props>(), {
  initialUsers: () => []
})

const emit = defineEmits<Emits>()

const users = ref<User[]>(props.initialUsers)
const searchQuery = ref<string>('')

const filteredUsers = computed<User[]>(() => {
  return users.value.filter(user =>
    user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
    user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
  )
})

const createUser = (userData: CreateUserRequest): void => {
  const newUser: User = {
    id: Date.now(),
    ...userData,
    createdAt: new Date().toISOString()
  }
  
  users.value.push(newUser)
  emit('userCreated', newUser)
}
</script>

4. パフォーマンス最適化

大規模なリストや複雑なコンポーネントでのパフォーマンス最適化手法です。

<template>
  <div>
    <!-- 仮想スクロール -->
    <VirtualList
      :items="largeDataSet"
      :item-height="50"
      :container-height="400"
      v-slot="{ item, index }"
    >
      <ListItem :item="item" :key="item.id" />
    </VirtualList>

    <!-- 遅延ローディング -->
    <LazyComponent v-if="shouldShowExpensiveComponent">
      <ExpensiveComponent :data="complexData" />
    </LazyComponent>

    <!-- KeepAlive でコンポーネントをキャッシュ -->
    <KeepAlive :include="['UserProfile', 'Dashboard']">
      <component :is="currentComponent" />
    </KeepAlive>
  </div>
</template>

<script setup>
import { ref, computed, defineAsyncComponent } from 'vue'

// 非同期コンポーネント
const LazyComponent = defineAsyncComponent({
  loader: () => import('./ExpensiveComponent.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: () => import('./ErrorComponent.vue'),
  loadingComponent: () => import('./LoadingComponent.vue')
})

const largeDataSet = ref([])
const shouldShowExpensiveComponent = ref(false)

// 計算量の多い処理をメモ化
const expensiveComputation = computed(() => {
  // 重い計算処理
  return largeDataSet.value
    .filter(item => item.active)
    .map(item => ({
      ...item,
      processedValue: item.value * 2
    }))
    .sort((a, b) => b.processedValue - a.processedValue)
})
</script>

5. エラーハンドリング

適切なエラーハンドリングの実装例です。

// plugins/errorHandler.js
export default {
  install(app) {
    // グローバルエラーハンドラー
    app.config.errorHandler = (err, instance, info) => {
      console.error('Vue Error:', err)
      console.error('Component:', instance)
      console.error('Info:', info)
      
      // エラー報告サービスに送信
      if (process.env.NODE_ENV === 'production') {
        sendErrorReport(err, instance, info)
      }
    }
    
    // 警告ハンドラー
    app.config.warnHandler = (msg, instance, trace) => {
      console.warn('Vue Warning:', msg)
      console.warn('Trace:', trace)
    }
  }
}

// composables/useErrorHandler.js
import { ref } from 'vue'

export function useErrorHandler() {
  const error = ref(null)
  const isError = computed(() => error.value !== null)

  const handleError = (err) => {
    error.value = err
    console.error('Error:', err)
  }

  const clearError = () => {
    error.value = null
  }

  const withErrorHandling = async (asyncFn) => {
    try {
      clearError()
      return await asyncFn()
    } catch (err) {
      handleError(err)
      throw err
    }
  }

  return {
    error,
    isError,
    handleError,
    clearError,
    withErrorHandling
  }
}

まとめ

Vue.jsは、学習しやすさと強力な機能を両立したモダンなJavaScriptフレームワークです。プログレッシブフレームワークとして設計されているため、小さなプロジェクトから大規模なエンタープライズアプリケーションまで幅広く対応できます。

Vue.jsの主な利点

学習コストの低さ: HTML、CSS、JavaScriptの基本知識があれば比較的容易に習得できます。テンプレート記法が直感的で、既存の知識を活かしながら段階的に学習を進められます。

豊富なエコシステム: Vue Router、Pinia、Nuxt.jsなど、充実した公式ツールと活発なコミュニティにより、開発効率を大幅に向上させることができます。

優れたパフォーマンス: Virtual DOMと最適化されたリアクティブシステムにより、高いパフォーマンスを実現します。Vue 3では、さらなる最適化が施されています。

柔軟な開発スタイル: Options APIとComposition APIの両方をサポートし、プロジェクトの要件や開発者の好みに応じて選択できます。

TypeScript対応: Vue 3では、TypeScriptのサポートが大幅に強化され、大規模開発での型安全性を確保できます。

学習のロードマップ

  1. 基礎学習: Vue.jsの基本概念、テンプレート記法、ディレクティブ
  2. コンポーネント開発: 再利用可能なコンポーネントの作成と管理
  3. 状態管理: Piniaを使用したアプリケーション状態の管理
  4. ルーティング: Vue Routerを使用したSPA開発
  5. 高度な機能: Composition API、TypeScript、パフォーマンス最適化
  6. 実践開発: 実際のプロジェクトでの応用

Vue.jsは継続的に進化しており、最新の機能や改善を取り入れながら、現代的なWebアプリケーション開発に必要な機能を提供し続けています。この記事で紹介した概念と実践例を参考に、Vue.jsを活用した効率的な開発を進めてください。

この記事は役に立ちましたか?

もし参考になりましたら、下記のボタンで教えてください。

コメント

この記事へのコメントはありません。