跳到主要内容
稻草人
プロフィール

EP.02

💻Technology

Vue 3 + Viteで軽い管理画面を作る

Vue 3とViteで小さく速い管理画面を作るときの構成メモ。

BY 稻, 草人

社内向け管理画面では、最初から大規模な構成が必要とは限りません。利用者が限定され、必要な機能が一覧、検索、登録、編集程度なら、Vue 3とViteで軽く始められます。

ただし「小さい画面だから一つのファイルでよい」と進めると、API、フォーム、権限、エラー処理が増えたところで急に保守しにくくなります。

目次

今回作る機能

  • 商品一覧
  • キーワードと公開状態による検索
  • 新規登録と編集
  • ローディング、0件、エラー表示
  • 削除前の確認

プロジェクト作成

npm create vite@latest light-admin -- --template vue-ts
cd light-admin
npm install
npm run dev

初期状態でnpm run buildも実行し、環境問題がないことを確認します。

最小構成

src/
├─ api/products.ts
├─ components/ProductFilter.vue
├─ components/ProductForm.vue
├─ components/ProductTable.vue
├─ composables/useProducts.ts
├─ types/product.ts
└─ views/ProductListView.vue

最初から細かく分けすぎる必要はありませんが、API通信、型、表示部品は分離しておくと変更しやすくなります。

型を先に決める

export type ProductStatus = 'draft' | 'published'

export type Product = {
  id: number
  name: string
  price: number
  status: ProductStatus
  updatedAt: string
}

export type ProductInput = {
  name: string
  price: number
  status: ProductStatus
}

一覧用と保存用を分けると、idや更新日時をフォームから誤送信しにくくなります。

APIを画面から分離する

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL

export async function getProducts(): Promise<Product[]> {
  const response = await fetch(`${API_BASE_URL}/products`, {
    credentials: 'include'
  })
  if (!response.ok) throw new Error(`HTTP ${response.status}`)
  return response.json()
}

Viteのクライアント用環境変数はブラウザへ含まれます。秘密鍵やパスワードは置きません。

一覧状態をまとめる

export function useProducts() {
  const products = ref<Product[]>([])
  const keyword = ref('')
  const status = ref<ProductStatus | 'all'>('all')
  const loading = ref(false)
  const errorMessage = ref('')

  const filteredProducts = computed(() => {
    const word = keyword.value.trim().toLowerCase()
    return products.value.filter(product => {
      const keywordOk =
        !word || product.name.toLowerCase().includes(word)
      const statusOk =
        status.value === 'all' || product.status === status.value
      return keywordOk && statusOk
    })
  })

  async function fetchProducts() {
    loading.value = true
    errorMessage.value = ''
    try {
      products.value = await getProducts()
    } catch (error) {
      console.error(error)
      errorMessage.value = '商品一覧を取得できませんでした。'
    } finally {
      loading.value = false
    }
  }

  return {
    keyword, status, loading, errorMessage,
    filteredProducts, fetchProducts
  }
}

データが数万件ある場合は、全件取得してブラウザで絞り込まず、検索条件とページ番号をAPIへ送ります。

画面は表示判断に集中する

<template>
  <main>
    <h1>商品管理</h1>
    <ProductFilter v-model:keyword="keyword" v-model:status="status" />

    <p v-if="loading">読み込み中です...</p>

    <div v-else-if="errorMessage" role="alert">
      <p>{{ errorMessage }}</p>
      <button type="button" @click="fetchProducts">再読み込み</button>
    </div>

    <p v-else-if="filteredProducts.length === 0">
      条件に一致する商品はありません。
    </p>

    <ProductTable v-else :products="filteredProducts" />
  </main>
</template>

ローディング、失敗、0件は別の状態です。一つにすると利用者が待つべきか、条件を変えるべきか分かりません。

フォームと権限

保存中は二重送信を防ぎます。

<button type="submit" :disabled="submitting">
  {{ submitting ? '保存中...' : '保存' }}
</button>

フロント側の入力チェックは使いやすさのために必要ですが、サーバー側の検証を置き換えるものではありません。

また、ボタンを隠すだけでは権限制御になりません。削除や更新が可能かどうかはAPI側で認証情報を検証します。Cookie認証ならCSRF、SameSite、CORS、credentialsも一緒に確認します。

小規模でも必要なテスト

  • 一覧取得成功、0件、500エラー
  • 前後空白や長い検索語
  • 登録・編集の入力エラー
  • 保存ボタンの連打
  • セッション切れ
  • 権限のない操作
  • 長い商品名と大きな数値
  • スマートフォン幅とキーボード操作

本番前

npm run build
npm run preview

開発サーバーで動いていても、本番のベースパスやAPI URLで問題が出ることがあります。サブディレクトリ配置ならViteのbaseも確認します。

まとめ

Vue 3とViteなら小規模管理画面を早く始められます。ただし、軽く作ることと、すべてを一つのコンポーネントへ入れることは同じではありません。

型、API通信、状態、表示部品の責任を最低限分け、エラーや権限も最初から通常の状態として扱うと、後から機能が増えても修正しやすくなります。

参考資料