Skip to content

VJSP Vue3 Frame - 開発規範とベストプラクティス

概要

このドキュメントでは、VJSP Vue3フレームワークの開発規範、ベストプラクティス、およびチームコラボレーションプロセスについて詳しく説明します。これらの規範に従うことで、コードの一貫性、保守性、およびチーム開発効率を確保できます。

技術スタックアーキテクチャ

VJSP Vue3フレームワークは、以下の現代的な技術スタックを採用しています:

  • フロントエンドフレームワーク: Vue 3 + TypeScript + Vite
  • UIコンポーネントライブラリ: Element Plus
  • 状態管理: Pinia
  • ルーティング: Vue Router 4
  • 国際化: Vue I18n
  • ビルドツール: Vite 6.0
  • 開発ツール: ESLint + Prettier + TypeScript

ディレクトリ構造設計

プロジェクトは明確なディレクトリ構造を採用し、コードの組織化と保守性を確保しています:

src/
├── api/                    # API インターフェース管理
│   ├── modules/           # モジュール別API
│   ├── request.ts         # リクエストインターセプター
│   └── types.ts           # API 型定義
├── assets/                # 静的リソース
│   ├── images/            # 画像リソース
│   ├── styles/            # グローバルスタイル
│   └── icons/             # アイコンファイル
├── components/            # グローバルコンポーネント
│   ├── common/            # 共通コンポーネント
│   ├── business/          # ビジネスコンポーネント
│   └── layout/            # レイアウトコンポーネント
├── composables/           # コンポジション関数
│   ├── usePermission.ts   # 権限管理
│   ├── useRequest.ts      # リクエスト管理
│   └── useTable.ts        # テーブル操作
├── directives/            # カスタムディレクティブ
│   ├── permission.ts      # 権限ディレクティブ
│   ├── loading.ts         # ローディングディレクティブ
│   └── copy.ts            # コピーディレクティブ
├── enums/                 # 列挙型定義
│   ├── http.ts            # HTTP ステータスコード
│   ├── route.ts           # ルートタイプ
│   └── user.ts            # ユーザータイプ
├── hooks/                 # React スタイルフック
│   ├── useLocalStorage.ts # ローカルストレージ
│   └── useSessionStorage.ts # セッションストレージ
├── layouts/               # レイアウトコンポーネント
│   ├── default/           # デフォルトレイアウト
│   └── blank/             # 空白レイアウト
├── locales/               # 国際化リソース
│   ├── en-US/             # 英語リソース
│   └── ja-JP/             # 日本語リソース
├── plugins/               # Vue プラグイン
│   ├── element-plus.ts    # Element Plus プラグイン
│   └── i18n.ts            # 国際化プラグイン
├── router/                # ルーティング設定
│   ├── index.ts           # ルーターインスタンス
│   ├── modules/           # モジュールルート
│   └── guards.ts          # ルートガード
├── stores/                # 状態管理ストア
│   ├── app.ts             # アプリケーション状態
│   ├── user.ts            # ユーザー状態
│   └── permission.ts      # 権限状態
├── types/                 # グローバル型定義
│   ├── api.ts             # API 型定義
│   ├── global.ts          # グローバル型
│   └── vue-shim.d.ts      # Vue 型拡張
├── utils/                 # ユーティリティ関数
│   ├── auth.ts            # 認証ユーティリティ
│   ├── cache.ts           # キャッシュユーティリティ
│   └── validate.ts        # バリデーションユーティリティ
├── views/                 # ページコンポーネント
│   ├── dashboard/         # ダッシュボードページ
│   ├── system/            # システム管理ページ
│   └── error/             # エラーページ
├── App.vue                # ルートコンポーネント
└── main.ts                # アプリケーションエントリ

モジュール化設計原則

1. 単一責任の原則

各モジュールは単一の責任を持ち、明確な境界を持つべきです:

typescript
// 良い例: 単一責任
// product/services/productApi.ts
export class ProductApiService {
  // 製品関連のAPI操作のみを担当
  async getProducts(): Promise<Product[]> {
    /* ... */
  }
  async createProduct(product: Product): Promise<void> {
    /* ... */
  }
}

// 悪い例: 複数の責任
// product/services/mixedService.ts
export class MixedService {
  // 製品、ユーザー、注文など複数の責任を混在
  async getProducts(): Promise<Product[]> {
    /* ... */
  }
  async getUsers(): Promise<User[]> {
    /* ... */
  }
  async getOrders(): Promise<Order[]> {
    /* ... */
  }
}

2. 依存関係逆転の原則

高レベルモジュールは低レベルモジュールに依存すべきではなく、抽象化に依存すべきです:

typescript
// 抽象化インターフェース
export interface IProductRepository {
  getProducts(): Promise<Product[]>
  createProduct(product: Product): Promise<void>
}

// 具象実装
export class ProductApiRepository implements IProductRepository {
  async getProducts(): Promise<Product[]> {
    // API実装
  }
}

// 高レベルモジュール
export class ProductService {
  constructor(private repository: IProductRepository) {}

  async getProducts(): Promise<Product[]> {
    return this.repository.getProducts()
  }
}

3. オープン・クローズドの原則

モジュールは拡張に対して開いており、修正に対して閉じているべきです:

typescript
// 拡張可能なベースクラス
export abstract class BaseService<T> {
  abstract getItems(): Promise<T[]>

  // 共通機能
  protected validateItem(item: T): boolean {
    // バリデーションロジック
    return true
  }
}

// 具体的な実装
export class ProductService extends BaseService<Product> {
  async getItems(): Promise<Product[]> {
    // 製品固有の実装
  }
}

コードコミット規範

Conventional Commits フォーマット

すべてのコミットメッセージは以下のフォーマットに従う必要があります:

<type>[optional scope]: <description>

[optional body]

[optional footer(s)]

コミットタイプ:

  • feat: 新機能の追加
  • fix: バグ修正
  • docs: ドキュメントのみの変更
  • style: コードの意味に影響しない変更(空白、フォーマット、セミコロンの欠落など)
  • refactor: バグ修正や機能追加ではないコード変更
  • perf: パフォーマンスを向上させるコード変更
  • test: 不足しているテストの追加または既存のテストの修正
  • chore: ビルドプロセスや補助ツール、ライブラリの変更

:

bash
# 新機能追加
feat(product): 製品検索機能を追加

# バグ修正
fix(auth): ログイン時のトークン検証問題を修正

# リファクタリング
refactor(utils): キャッシュユーティリティをモジュール化

# ドキュメント更新
docs: READMEにセットアップ手順を追加

コミットメッセージのベストプラクティス

  1. 簡潔で明確な説明: 50文字以内で変更内容を明確に説明
  2. 本文の詳細説明: 複雑な変更には本文で詳細を説明
  3. 関連するIssue番号: 関連するIssue番号をフッターに記載
  4. 英語での記述: 国際的なチームでのコラボレーションを考慮

ブランチ管理戦略

Git Flow ワークフロー

プロジェクトはGit Flowワークフローを採用しています:

main                    # 本番環境ブランチ
├── develop             # 開発ブランチ
│   ├── feature/*       # 機能開発ブランチ
│   ├── bugfix/*        # バグ修正ブランチ
│   └── hotfix/*        # 緊急修正ブランチ
└── release/*          # リリース準備ブランチ

ブランチ命名規則

  • 機能ブランチ: feature/機能名-番号 (例: feature/user-authentication-123)
  • バグ修正ブランチ: bugfix/問題説明-番号 (例: bugfix/login-error-456)
  • 緊急修正ブランチ: hotfix/緊急問題-番号 (例: hotfix/security-fix-789)
  • リリースブランチ: release/バージョン (例: release/v1.2.0)

ブランチ作成とマージプロセス

  1. 機能開発開始: developブランチからfeatureブランチを作成
  2. 開発完了: featureブランチをdevelopブランチにマージ
  3. リリース準備: developブランチからreleaseブランチを作成
  4. 本番リリース: releaseブランチをmainブランチにマージ
  5. 緊急修正: mainブランチからhotfixブランチを作成し、mainとdevelopにマージ

コードレビュープロセス

レビュー基準

すべてのプルリクエストは以下の基準を満たす必要があります:

  1. コード品質: ESLintとPrettierのチェックを通過
  2. テストカバレッジ: 関連する単体テストが含まれている
  3. ドキュメント: APIドキュメントとコードコメントが更新されている
  4. パフォーマンス: パフォーマンスへの影響が評価されている
  5. セキュリティ: セキュリティ上の問題がない

レビュープロセス

  1. プルリクエスト作成: 機能開発完了後にプルリクエストを作成
  2. 自動チェック: CI/CDパイプラインが自動的に実行
  3. コードレビュー: チームメンバーがコードをレビュー
  4. 修正と反復: フィードバックに基づいて修正
  5. マージ承認: レビュー完了後にマージ

レビューコメントのベストプラクティス

  • 建設的なフィードバック: 問題点だけでなく改善案も提案
  • コード例の提供: 具体的なコード例で改善案を示す
  • 一貫性の維持: プロジェクトのコーディング規約に従う
  • タイムリーな対応: レビューリクエストに迅速に対応

TypeScript 規範

インターフェース定義

すべてのデータ構造はインターフェースで定義する必要があります:

typescript
// ユーザーインターフェース
export interface User {
  id: number
  username: string
  email: string
  role: UserRole
  createdAt: Date
  updatedAt: Date
}

// ユーザー作成リクエスト
export interface CreateUserRequest {
  username: string
  email: string
  password: string
  role: UserRole
}

// APIレスポンス
export interface ApiResponse<T> {
  code: number
  message: string
  data: T
  timestamp: number
}

型エイリアス

複雑な型は型エイリアスで定義:

typescript
// 複合型
export type UserStatus = 'active' | 'inactive' | 'suspended'

export type PaginationParams = {
  page: number
  pageSize: number
  total: number
}

// 条件付き型
export type Nullable<T> = T | null
export type Optional<T> = T | undefined

// ユーティリティ型
export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

列挙型定義

関連する定数は列挙型で定義:

typescript
// 数値列挙型
export enum HttpStatus {
  OK = 200,
  CREATED = 201,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  FORBIDDEN = 403,
  NOT_FOUND = 404,
  INTERNAL_SERVER_ERROR = 500,
}

// 文字列列挙型
export enum UserRole {
  ADMIN = 'admin',
  EDITOR = 'editor',
  VIEWER = 'viewer',
}

// 定数列挙型
export const enum ApiEndpoints {
  USERS = '/api/users',
  PRODUCTS = '/api/products',
  ORDERS = '/api/orders',
}

Vue 3 コンポジションAPI規範

コンポーネント構造

単一ファイルコンポーネントは以下の構造に従う:

vue
<template>
  <!-- テンプレートセクション -->
  <div class="product-list">
    <ProductCard
      v-for="product in products"
      :key="product.id"
      :product="product"
      @click="handleProductClick"
    />
  </div>
</template>

<script setup lang="ts">
// インポートセクション
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import ProductCard from './ProductCard.vue'
import type { Product } from '@/types/product'

// リアクティブデータ
const products = ref<Product[]>([])
const loading = ref(false)

// コンポジション関数
const { fetchProducts } = useProductApi()

// ライフサイクルフック
onMounted(async () => {
  loading.value = true
  try {
    products.value = await fetchProducts()
  } catch (error) {
    ElMessage.error('製品リストの取得に失敗しました')
  } finally {
    loading.value = false
  }
})

// イベントハンドラー
const handleProductClick = (product: Product) => {
  console.log('製品をクリック:', product)
}

// テンプレートで使用する変数と関数を明示的に公開
defineExpose({
  products,
  loading,
})
</script>

<style scoped lang="less">
/* スタイルセクション */
.product-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 16px;
  padding: 16px;
}
</style>

コンポジション関数

再利用可能なロジックはコンポジション関数でカプセル化:

typescript
// composables/useProductApi.ts
import { ref } from 'vue'
import { productApi } from '@/api/modules/product'
import type { Product, CreateProductRequest } from '@/types/product'

export function useProductApi() {
  const loading = ref(false)
  const error = ref<string | null>(null)

  const fetchProducts = async (): Promise<Product[]> => {
    loading.value = true
    error.value = null

    try {
      const response = await productApi.getProducts()
      return response.data
    } catch (err) {
      error.value = '製品の取得に失敗しました'
      throw err
    } finally {
      loading.value = false
    }
  }

  const createProduct = async (request: CreateProductRequest): Promise<void> => {
    loading.value = true
    error.value = null

    try {
      await productApi.createProduct(request)
    } catch (err) {
      error.value = '製品の作成に失敗しました'
      throw err
    } finally {
      loading.value = false
    }
  }

  return {
    loading,
    error,
    fetchProducts,
    createProduct,
  }
}

リアクティビティのベストプラクティス

  1. refの適切な使用: プリミティブ値とオブジェクトのリアクティブ化
  2. reactiveの使用: 関連するデータのグループ化
  3. computedの活用: 派生データの効率的な計算
  4. watchの適切な使用: 副作用の管理
typescript
import { ref, reactive, computed, watch } from 'vue'

// プリミティブ値はrefで
const count = ref(0)

// 関連するデータはreactiveでグループ化
const state = reactive({
  user: null as User | null,
  permissions: [] as string[],
})

// 派生データはcomputedで
const hasAdminPermission = computed(() => {
  return state.permissions.includes('admin')
})

// 副作用はwatchで管理
watch(
  () => state.user,
  newUser => {
    if (newUser) {
      // ユーザー変更時の処理
    }
  },
  { immediate: true }
)

状態管理規範

Pinia Store 構造

各Storeは明確な責任を持ち、以下の構造に従う:

typescript
// stores/product.ts
import { defineStore } from 'pinia'
import { productApi } from '@/api/modules/product'
import type { Product, ProductFilters } from '@/types/product'

export const useProductStore = defineStore('product', () => {
  // 状態
  const products = ref<Product[]>([])
  const currentProduct = ref<Product | null>(null)
  const loading = ref(false)
  const filters = ref<ProductFilters>({})

  // ゲッター
  const filteredProducts = computed(() => {
    return products.value.filter(product => {
      // フィルタリングロジック
      return true
    })
  })

  const productCount = computed(() => filteredProducts.value.length)

  // アクション
  const fetchProducts = async () => {
    loading.value = true
    try {
      const response = await productApi.getProducts()
      products.value = response.data
    } catch (error) {
      console.error('製品の取得に失敗しました:', error)
      throw error
    } finally {
      loading.value = false
    }
  }

  const createProduct = async (productData: Omit<Product, 'id'>) => {
    try {
      await productApi.createProduct(productData)
      await fetchProducts() // リストを更新
    } catch (error) {
      console.error('製品の作成に失敗しました:', error)
      throw error
    }
  }

  const updateFilters = (newFilters: ProductFilters) => {
    filters.value = { ...filters.value, ...newFilters }
  }

  return {
    // 状態
    products,
    currentProduct,
    loading,
    filters,

    // ゲッター
    filteredProducts,
    productCount,

    // アクション
    fetchProducts,
    createProduct,
    updateFilters,
  }
})

Store 使用規範

  1. コンポーネント内でのStore使用:
vue
<script setup lang="ts">
import { useProductStore } from '@/stores/product'

const productStore = useProductStore()

// Storeの状態を使用
const { products, loading } = storeToRefs(productStore)

// Storeのアクションを使用
const { fetchProducts } = productStore

onMounted(() => {
  fetchProducts()
})
</script>
  1. Store間の依存関係:
typescript
// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)

  const fetchUser = async () => {
    // ユーザー情報を取得
  }

  return { user, fetchUser }
})

// stores/permission.ts
export const usePermissionStore = defineStore('permission', () => {
  const userStore = useUserStore()

  const permissions = computed(() => {
    return userStore.user?.permissions || []
  })

  return { permissions }
})

ルーティング規範

ルート定義

ルートはモジュールごとに分割して定義:

typescript
// router/modules/product.ts
import type { RouteRecordRaw } from 'vue-router'

const productRoutes: RouteRecordRaw[] = [
  {
    path: '/products',
    name: 'Products',
    component: () => import('@/views/product/ProductList.vue'),
    meta: {
      title: '製品管理',
      requiresAuth: true,
      permissions: ['product:read'],
    },
  },
  {
    path: '/products/:id',
    name: 'ProductDetail',
    component: () => import('@/views/product/ProductDetail.vue'),
    meta: {
      title: '製品詳細',
      requiresAuth: true,
      permissions: ['product:read'],
    },
  },
  {
    path: '/products/create',
    name: 'ProductCreate',
    component: () => import('@/views/product/ProductCreate.vue'),
    meta: {
      title: '製品作成',
      requiresAuth: true,
      permissions: ['product:write'],
    },
  },
]

export default productRoutes

ルートガード

認証と権限チェックのためのルートガード:

typescript
// router/guards.ts
import type { NavigationGuard } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { usePermissionStore } from '@/stores/permission'

export const authGuard: NavigationGuard = (to, from, next) => {
  const authStore = useAuthStore()

  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
    return
  }

  next()
}

export const permissionGuard: NavigationGuard = (to, from, next) => {
  const permissionStore = usePermissionStore()

  if (to.meta.permissions) {
    const hasPermission = permissionStore.hasPermission(to.meta.permissions)

    if (!hasPermission) {
      next({ name: 'Forbidden' })
      return
    }
  }

  next()
}

国際化規範

言語リソース構造

言語リソースは機能モジュールごとに分割:

typescript
// locales/ja-JP/product.ts
export default {
  product: {
    list: {
      title: '製品リスト',
      search: '検索',
      create: '新規作成',
      edit: '編集',
      delete: '削除',
    },
    form: {
      name: '製品名',
      price: '価格',
      description: '説明',
      category: 'カテゴリー',
    },
    messages: {
      createSuccess: '製品を作成しました',
      updateSuccess: '製品を更新しました',
      deleteSuccess: '製品を削除しました',
      deleteConfirm: 'この製品を削除してもよろしいですか?',
    },
  },
}

コンポーネント内での国際化

vue
<template>
  <div>
    <h1>{{ $t('product.list.title') }}</h1>
    <el-button @click="handleCreate">
      {{ $t('product.list.create') }}
    </el-button>
  </div>
</template>

<script setup lang="ts">
import { useI18n } from 'vue-i18n'

const { t } = useI18n()

const handleCreate = () => {
  ElMessage.success(t('product.messages.createSuccess'))
}
</script>

テスト規範

単体テスト

コンポーネントとユーティリティ関数の単体テスト:

typescript
// tests/unit/utils/auth.test.ts
import { describe, it, expect } from 'vitest'
import { hasPermission, checkAuth } from '@/utils/auth'

describe('auth utils', () => {
  describe('hasPermission', () => {
    it('should return true when user has required permission', () => {
      const userPermissions = ['product:read', 'user:write']
      expect(hasPermission(userPermissions, 'product:read')).toBe(true)
    })

    it('should return false when user does not have required permission', () => {
      const userPermissions = ['user:write']
      expect(hasPermission(userPermissions, 'product:read')).toBe(false)
    })
  })

  describe('checkAuth', () => {
    it('should pass when token is valid', () => {
      const token = 'valid-token'
      expect(() => checkAuth(token)).not.toThrow()
    })

    it('should throw error when token is invalid', () => {
      const token = ''
      expect(() => checkAuth(token)).toThrow('Invalid token')
    })
  })
})

コンポーネントテスト

typescript
// tests/unit/components/ProductList.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import ProductList from '@/components/ProductList.vue'

describe('ProductList', () => {
  it('should render product list correctly', () => {
    const products = [
      { id: 1, name: 'Product 1', price: 100 },
      { id: 2, name: 'Product 2', price: 200 },
    ]

    const wrapper = mount(ProductList, {
      props: { products },
    })

    expect(wrapper.findAll('.product-item')).toHaveLength(2)
    expect(wrapper.text()).toContain('Product 1')
    expect(wrapper.text()).toContain('Product 2')
  })

  it('should emit product-click event when product is clicked', async () => {
    const products = [{ id: 1, name: 'Product 1', price: 100 }]

    const wrapper = mount(ProductList, {
      props: { products },
    })

    await wrapper.find('.product-item').trigger('click')

    expect(wrapper.emitted('product-click')).toBeTruthy()
    expect(wrapper.emitted('product-click')?.[0]).toEqual([products[0]])
  })
})

パフォーマンス最適化規範

コンポーネント最適化

  1. 遅延ローディング: ルートコンポーネントの遅延ローディング
  2. コンポーネントメモ化: 不必要な再レンダリングを防ぐ
  3. バンドル分割: コード分割による初期ロード時間の短縮
typescript
// 遅延ローディング
const ProductList = defineAsyncComponent(() => import('@/views/product/ProductList.vue'))

// コンポーネントメモ化
const MemoizedProductCard = defineComponent({
  name: 'MemoizedProductCard',
  props: {
    product: {
      type: Object as PropType<Product>,
      required: true,
    },
  },
  setup(props) {
    return () => h(ProductCard, { product: props.product })
  },
})

リソース最適化

  1. 画像最適化: WebPフォーマットと遅延ローディング
  2. フォント最適化: フォントサブセットとpreload
  3. キャッシュ戦略: 適切なキャッシュヘッダーの設定

セキュリティ規範

入力検証

すべてのユーザー入力は検証する必要があります:

typescript
// utils/validation.ts
export function validateEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
  return emailRegex.test(email)
}

export function validatePassword(password: string): boolean {
  // パスワード強度チェック
  return (
    password.length >= 8 && /[A-Z]/.test(password) && /[a-z]/.test(password) && /\d/.test(password)
  )
}

export function sanitizeInput(input: string): string {
  // XSS対策
  return input.replace(/[<>]/g, '')
}

認証と権限

  1. JWTトークン管理: 安全なトークン保存と更新
  2. 権限チェック: ルートレベルとコンポーネントレベルの権限チェック
  3. セッション管理: 安全なセッションタイムアウト

デプロイメント規範

環境設定

環境ごとの設定管理:

typescript
// .env.development
VITE_API_BASE_URL=http://localhost:3000/api
VITE_APP_TITLE=VJSP Vue3 App (開発)

// .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=VJSP Vue3 App

ビルドとデプロイメント

  1. ビルドプロセス: 自動化されたビルドパイプライン
  2. 品質チェック: ビルド前のコード品質チェック
  3. デプロイメント: CI/CDパイプラインによる自動デプロイメント

これらの開発規範とベストプラクティスに従うことで、VJSP Vue3フレームワークを使用したプロジェクトのコード品質、保守性、およびチーム開発効率を確保できます。