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にセットアップ手順を追加コミットメッセージのベストプラクティス
- 簡潔で明確な説明: 50文字以内で変更内容を明確に説明
- 本文の詳細説明: 複雑な変更には本文で詳細を説明
- 関連するIssue番号: 関連するIssue番号をフッターに記載
- 英語での記述: 国際的なチームでのコラボレーションを考慮
ブランチ管理戦略
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)
ブランチ作成とマージプロセス
- 機能開発開始: developブランチからfeatureブランチを作成
- 開発完了: featureブランチをdevelopブランチにマージ
- リリース準備: developブランチからreleaseブランチを作成
- 本番リリース: releaseブランチをmainブランチにマージ
- 緊急修正: mainブランチからhotfixブランチを作成し、mainとdevelopにマージ
コードレビュープロセス
レビュー基準
すべてのプルリクエストは以下の基準を満たす必要があります:
- コード品質: ESLintとPrettierのチェックを通過
- テストカバレッジ: 関連する単体テストが含まれている
- ドキュメント: APIドキュメントとコードコメントが更新されている
- パフォーマンス: パフォーマンスへの影響が評価されている
- セキュリティ: セキュリティ上の問題がない
レビュープロセス
- プルリクエスト作成: 機能開発完了後にプルリクエストを作成
- 自動チェック: CI/CDパイプラインが自動的に実行
- コードレビュー: チームメンバーがコードをレビュー
- 修正と反復: フィードバックに基づいて修正
- マージ承認: レビュー完了後にマージ
レビューコメントのベストプラクティス
- 建設的なフィードバック: 問題点だけでなく改善案も提案
- コード例の提供: 具体的なコード例で改善案を示す
- 一貫性の維持: プロジェクトのコーディング規約に従う
- タイムリーな対応: レビューリクエストに迅速に対応
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,
}
}リアクティビティのベストプラクティス
- refの適切な使用: プリミティブ値とオブジェクトのリアクティブ化
- reactiveの使用: 関連するデータのグループ化
- computedの活用: 派生データの効率的な計算
- 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 使用規範
- コンポーネント内での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>- 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]])
})
})パフォーマンス最適化規範
コンポーネント最適化
- 遅延ローディング: ルートコンポーネントの遅延ローディング
- コンポーネントメモ化: 不必要な再レンダリングを防ぐ
- バンドル分割: コード分割による初期ロード時間の短縮
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 })
},
})リソース最適化
- 画像最適化: WebPフォーマットと遅延ローディング
- フォント最適化: フォントサブセットとpreload
- キャッシュ戦略: 適切なキャッシュヘッダーの設定
セキュリティ規範
入力検証
すべてのユーザー入力は検証する必要があります:
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, '')
}認証と権限
- JWTトークン管理: 安全なトークン保存と更新
- 権限チェック: ルートレベルとコンポーネントレベルの権限チェック
- セッション管理: 安全なセッションタイムアウト
デプロイメント規範
環境設定
環境ごとの設定管理:
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ビルドとデプロイメント
- ビルドプロセス: 自動化されたビルドパイプライン
- 品質チェック: ビルド前のコード品質チェック
- デプロイメント: CI/CDパイプラインによる自動デプロイメント
これらの開発規範とベストプラクティスに従うことで、VJSP Vue3フレームワークを使用したプロジェクトのコード品質、保守性、およびチーム開発効率を確保できます。
