📚

Vue.js初心者が管理画面を実装して学んだ11のこと

に公開

はじめに

Vue.js未経験の状態から、IoTデバイス管理画面(Admin Dashboard)を実装する機会がありました。この記事では、実装過程で学んだVue.js・JavaScriptの基礎知識を、初心者目線で整理してまとめます。

前提条件:

  • Vueは完全な初心者
  • JSは一定の学習経験あり(その他、pythonなど一定はプログラミング言語経験あり)

実装した機能:

  • デバイス一覧表示(リアルタイムステータス表示)
  • Azure Functions APIとの連携
  • Piniaによる状態管理
  • エラーハンドリング・再試行機能

技術スタック:

  • Vue 3 (Composition API with <script setup>)
  • Vite
  • Pinia
  • axios

1. Viteとは - 開発サーバー+ビルドツール

Viteは、Vue.jsプロジェクトの開発を支える高速ツールです。

役割:

  • 開発サーバー: npm run dev でローカル環境を起動
  • HMR (Hot Module Replacement): コード変更を即座に反映
  • 本番ビルド: npm run build で最適化されたファイルを生成
  • 環境変数アクセス: import.meta.env で環境変数を取得
// 環境変数の利用例
const apiUrl = import.meta.env.VITE_API_BASE_URL

2. axios interceptors - リクエスト/レスポンスの前処理・後処理

interceptorsを使うと、すべてのHTTPリクエスト/レスポンスに共通処理を挿入できます。

// src/api/client.js
import axios from 'axios'

const apiClient = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000
})

// リクエストインターセプター: 開始時刻を記録
apiClient.interceptors.request.use((config) => {
  config.metadata = { startTime: Date.now() }
  console.info('[API Request]', config.method, config.url)
  return config
})

// レスポンスインターセプター: レイテンシ測定とエラー分類
apiClient.interceptors.response.use(
  (response) => {
    const duration = Date.now() - response.config.metadata.startTime
    console.info('[API Success]', { duration })
    return response
  },
  (error) => {
    // エラー種別の分類
    if (error.response?.status === 404) {
      error.domainError = 'NotFound'
    } else if (error.response?.status === 503) {
      error.domainError = 'ServiceUnavailable'
    }
    return Promise.reject(error)
  }
)

ユースケース:

  • 認証トークンの自動付与
  • レイテンシ計測
  • エラー分類
  • ログ記録

3. Promiseの仕組み - 「番号札」の例え

Promiseは「将来の結果を表す箱」です。レストランの番号札でイメージするとわかりやすいです。

レストラン例え:

// Promise作成 = 「番号札を作って渡す」(一瞬で終わる)
return Promise.reject(error)

// await = 「料理ができるまで待つ」(時間がかかる)
const result = await fetchData()

ポイント:

  • return Promise.reject(error) は箱を作って返すだけ(待たない)
  • await promise は箱の中身を取り出すまで待つ
  • async/await は Promise の糖衣構文

4. Optional chaining (?.) - 安全なプロパティアクセス

JavaScriptでは、存在しないプロパティへのアクセスでエラーが発生します。?.を使うと安全にアクセスできます。

// 従来の書き方
const method = error.config && error.config.method

// Optional chaining
const method = error.config?.method  // error.config が undefined でもエラーにならない

5. エラーオブジェクトへのプロパティ追加

JavaScriptでは、実行時にオブジェクトへプロパティを追加できます。エラー分類に便利です。

// interceptorsでエラー種別を追加
error.domainError = 'NotFound'

// 後続処理で利用
if (error.domainError === 'NotFound') {
  return 'リソースが見つかりません'
}

6. Pinia - Vue.jsの状態管理ライブラリ

Piniaは「共有倉庫」のようなものです。どのコンポーネントからもアクセス可能な状態を管理できます。

Props渡しとの違い:

  • Props: 親→子→孫と階層を経由(面倒)
  • Pinia: どこからでも直接アクセス(簡単)
// src/stores/devices.js
import { ref } from 'vue'
import { defineStore } from 'pinia'

export const useDevicesStore = defineStore('devices', () => {
  const devices = ref([])  // リアクティブ変数
  const loading = ref(false)
  const error = ref(null)

  async function loadDevices() {
    loading.value = true
    error.value = null  // 新しいリクエスト前にリセット

    try {
      devices.value = await fetchDevices()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false  // returnしても必ず実行される
    }
  }

  return { devices, loading, error, loadDevices }
})

7. リアクティビティ - 自動で画面が更新される

ref()で作成した変数は「リアクティブ」になります。値を変更すると自動で画面が更新されます。

const devices = ref([])  // リアクティブ変数

devices.value = [新データ]  // 変更すると画面が自動更新

8. モジュールレベル変数 - 関数呼び出しをまたいで値を保持

関数外の変数は、関数呼び出しをまたいで値が保持されます。AbortControllerの管理に便利です。

// 関数の外(モジュールレベル変数)
let abortController = null

function loadDevices() {
  // 前回の値が残っている
  if (abortController) {
    abortController.abort()  // 前回のリクエストをキャンセル
  }

  // 新しいコントローラーを作成
  abortController = new AbortController()

  await fetchDevices(abortController.signal)
}

ポイント:

  • 関数外の変数: 関数呼び出しをまたいで保持される
  • 関数内の変数: 毎回リセットされる

9. error.value のリセットが必要な理由

Piniaストアのerror.valueは、新しいリクエスト前に必ずリセットする必要があります。

async function loadDevices() {
  error.value = null  // ← これを忘れると前回のエラーが残る

  try {
    devices.value = await fetchDevices()
  } catch (err) {
    error.value = err.message
  }
}

理由:

  • 成功時はcatchブロックが実行されないため、エラーがリセットされない
  • 前回のエラーが残っていると画面に表示され続ける

10. finally ブロックの挙動 - 必ず実行される

finallyブロックは、try/catchでreturnしても必ず実行されます。クリーンアップ処理に最適です。

async function loadDevices() {
  loading.value = true

  try {
    devices.value = await fetchDevices()
    return  // ← returnしても...
  } catch (err) {
    error.value = err.message
  } finally {
    loading.value = false  // ← 必ず実行される
  }
}

11. AbortController - ボタン連打対策

AbortControllerを使うと、前回のリクエストをキャンセルして新しいリクエストを開始できます。

let abortController = null

async function loadDevices() {
  // 前回のリクエストをキャンセル
  if (abortController) {
    abortController.abort()
  }

  abortController = new AbortController()

  try {
    devices.value = await fetchDevices(abortController.signal)
  } catch (err) {
    // AbortError は特別扱い(エラー表示しない)
    if (err.name === 'AbortError') return
    error.value = err.message
  }
}

おまけ: v-forの:keyの役割

:keyは、v-forで各要素を識別するIDです。並び順を指定するものではなく、トラッキング用です。

:keyがない場合の問題:

# 初期状態
☑ Device1
☐ Device2

# 順番が入れ替わった後
☑ Device2  ← おかしい!(チェックが残ってる)
☐ Device1

:keyがある場合:

# 順番が入れ替わった後
☐ Device2  ← 正しい
☑ Device1  ← 正しい

まとめ

Vue.js初心者が管理画面実装を通して学んだ11のポイントを紹介しました。

特に重要だったポイント:

  1. Promiseの仕組み(「番号札」の例え)
  2. Piniaによる状態管理(共有倉庫)
  3. エラーハンドリング(リセット、finally、AbortController)

これからVue.jsを学ぶ方の参考になれば幸いです。

参考リンク

Discussion