Vue TypeScript Element Template 実践活用ガイド

一、HTTPリクエスト処理

1. APIインタフェースの定義

import request from '@/utils/request'

export const fetchUserList = (query: UserQuery, payload: UserPayload) =>
  request({
    url: '/api/v1/users',
    method: 'post',
    query,
    payload
  })

2. リクエストインターセプタ

import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'

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

// リクエストインターセプタ
httpClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const authToken = localStorage.getItem('access_token')
    if (authToken) {
      config.headers?.set('Authorization', `Bearer ${authToken}`)
    }
    return config
  },
  (error: Error) => {
    return Promise.reject(error)
  }
)

// レスポンスインターセプタ
httpClient.interceptors.response.use(
  (response: AxiosResponse) => {
    return response.data
  },
  (error: unknown) => {
    return Promise.reject(error)
  }
)

interface ApiResponse<T> {
  status: number
  result: T
  message: string
}

export function apiClient<T>(config: AxiosRequestConfig): Promise<ApiResponse<T>> {
  return httpClient(config) as Promise<ApiResponse<T>>
}

二、コンポーネント実装

1. コンポーネント基本形

import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'UserProfile'
})
export default class UserProfile extends Vue {
  private userName: string = ''
  private isLoading: boolean = false

  private async loadUserData(userId: string): Promise<void> {
    this.isLoading = true
    try {
      const response = await fetchUserData(userId)
      this.userName = response.result.name
    } finally {
      this.isLoading = false
    }
  }

  protected mounted(): void {
    this.loadUserData('123')
  }
}

2. ウォッチャ機能

import { Component, Vue, Watch } from 'vue-property-decorator'

@Component({
  name: 'DataWatcher'
})
export default class DataWatcher extends Vue {
  private searchKeyword: string = ''

  @Watch('searchKeyword')
  private onKeywordChanged(newValue: string, oldValue: string): void {
    this.fetchSearchResults()
  }

  private fetchSearchResults(): void {
    console.log('検索実行:', this.searchKeyword)
  }
}

3. プロパティ定義

import { Component, Prop, Vue } from 'vue-property-decorator'

@Component({
  name: 'ConfirmDialog'
})
export default class ConfirmDialog extends Vue {
  @Prop({
    type: Boolean,
    required: true,
    default: false,
    validator: (value: unknown): boolean => typeof value === 'boolean'
  })
  private isVisible!: boolean

  @Prop({
    type: String,
    default: '確認'
  })
  private dialogTitle!: string
}

4. イベントエミット

import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'ActionButton'
})
export default class ActionButton extends Vue {
  private handleClick(): void {
    this.$emit('button-clicked', { timestamp: Date.now() })
  }
}

5. ルーティング情報へのアクセス

import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'PageNavigator'
})
export default class PageNavigator extends Vue {
  private navigateToDetail(): void {
    const currentRoute = this.$route
    const currentParams = this.$route.params

    this.$router.push({
      path: `/details/${currentParams.id}`,
      query: { source: 'list' }
    })
  }
}

6. 算出プロパティとストア連携

import { Component, Vue } from 'vue-property-decorator'
import { UserStore } from '@/store/modules/user'

@Component({
  name: 'UserInfo'
})
export default class UserInfo extends Vue {
  get userDisplayName(): string {
    return UserStore.fullName
  }
}

7. フィルター機能

import { Component, Vue } from 'vue-property-decorator'

@Component({
  name: 'FormatHelpers',
  filters: {
    currencyJPY(value: number): string {
      return new Intl.NumberFormat('ja-JP', {
        style: 'currency',
        currency: 'JPY'
      }).format(value)
    },
    truncateText(text: string, length: number): string {
      return text.length > length ? text.slice(0, length) + '...' : text
    }
  }
})
export default class FormatHelpers extends Vue {}
}

三、ルーター設定

1. ルーター初期設定

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Layout from '@/components/Layout/index.vue'

const routes: RouteRecordRaw[] = [
  {
    path: '/dashboard',
    component: Layout,
    redirect: '/dashboard/overview',
    children: [
      {
        path: 'overview',
        name: 'DashboardOverview',
        component: () => import('@/views/dashboard/Overview.vue'),
        meta: {
          title: 'ダッシュボード',
          icon: 'dashboard',
          keepAlive: true
        }
      }
    ]
  }
]

const router = createRouter({
  history: createWebHistory(),
  scrollBehavior: (to, from, savedPosition) => {
    if (savedPosition) {
      return savedPosition
    }
    return { top: 0, left: 0 }
  },
  routes
})

export default router

2. ナビゲーションガード

import router from '@/router'
import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { AuthService } from '@/services/auth'

router.beforeEach(
  async (
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    next: NavigationGuardNext
  ): Promise<void> {
    const requiresAuth = to.meta.requiresAuth !== false

    if (requiresAuth && !AuthService.isAuthenticated()) {
      next({ path: '/login', query: { redirect: to.fullPath } })
      return
    }

    next()
  }
)

router.afterEach((to: RouteLocationNormalized): void => {
  document.title = to.meta.title as string || 'アプリケーション'
})

3. アプリケーション起動設定

import { createApp } from 'vue'
import App from '@/App.vue'
import router from '@/router'
import { setupStore } from '@/store'

const app = createApp(App)

setupStore(app)
app.use(router)
app.mount('#app')

4. ネストドルーターでのスクロール位置復元(Vue3)

<template>
  <div ref="containerRef" class="page-container">
    <header class="app-header">ヘッダー</header>
    <main class="app-content">
      <router-view v-slot="{ Component }">
        <transition name="fade">
          <keep-alive>
            <component :is="Component" />
          </keep-alive>
        </transition>
      </router-view>
    </main>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteUpdate } from 'vue-router'

const containerRef = ref<HTMLElement | null>(null)

onBeforeRouteUpdate((to, from) => {
  const currentRoute = from.matched[from.matched.length - 1]

  if (currentRoute) {
    currentRoute.meta.savedScrollTop = containerRef.value?.scrollTop || 0
  }

  if (to.meta.keepAlive && typeof to.meta.savedScrollTop === 'number') {
    containerRef.value!.scrollTop = to.meta.savedScrollTop
  } else {
    containerRef.value!.scrollTop = 0
  }
})
</script>

<style scoped>
.page-container {
  height: 100vh;
  overflow-y: auto;
}
</style>

四、ステート管理

1. ルートストア定義

import { createStore } from 'vuex'
import { IApplicationState } from './modules/application'

export interface RootState {
  application: IApplicationState
}

export default createStore<RootState>({
  modules: {
    application
  }
})

2. モジュール実装

import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators'
import store from '@/store'

export interface IApplicationState {
  theme: 'light' | 'dark'
  sidebarCollapsed: boolean
}

@Module({
  dynamic: true,
  store,
  name: 'application'
})
class ApplicationModule extends VuexModule implements IApplicationState {
  public theme: 'light' | 'dark' = 'light'
  public sidebarCollapsed = false

  get currentTheme(): 'light' | 'dark' {
    return this.theme
  }

  @Mutation
  SET_THEME(theme: 'light' | 'dark'): void {
    this.theme = theme
  }

  @Mutation
  TOGGLE_SIDEBAR(): void {
    this.sidebarCollapsed = !this.sidebarCollapsed
  }

  @Action
  public changeTheme(theme: 'light' | 'dark'): void {
    this.SET_THEME(theme)
    localStorage.setItem('theme', theme)
  }
}

export const ApplicationStore = getModule(ApplicationModule)

3. ストア初期化

import { createApp } from 'vue'
import App from '@/App.vue'
import rootStore from '@/store'

const app = createApp(App)

app.use(rootStore)
app.mount('#app')

タグ: Vue TypeScript vue-router Vuex Axios

7月18日 23:29 投稿