Vue3アプリケーションの中核を構成するルーティング・状態管理・HTTP通信の実装パターン

エントリポイント統合

import { createApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'
import ky from 'ky'

const app = createApp(App)

app.config.globalProperties.$http = ky.create({
  prefixUrl: 'https://api.example.jp',
  timeout: 10000
})

app.use(createRouter())
app.use(createStore())

app.mount('#app')

Vue Router 実装詳解

import { createRouter, createWebHistory } from 'vue-router'
import Dashboard from '@/views/Dashboard.vue'
import Detail from '@/views/Detail.vue'

export default function createRouterInstance() {
  return createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
      { path: '/', redirect: '/dashboard' },
      { 
        path: '/dashboard', 
        component: Dashboard,
        children: [
          { path: 'active', component: () => import('@/views/ActiveTasks.vue') },
          { path: 'completed', component: () => import('@/views/CompletedTasks.vue') }
        ]
      },
      { path: '/detail/:id(\\d+)', component: Detail, props: true }
    ]
  })
}

遷移制御例

// コンポーネント内ガード
beforeRouteEnter(to, from, next) {
  const isAuth = checkAuth()
  isAuth ? next() : next('/login')
}

// グローバルガード
router.beforeEach((to, from, next) => {
  if (to.meta.requiresAuth && !isLoggedIn()) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
  } else {
    next()
  }
})

Pinia による状態管理

import { defineStore } from 'pinia'

export const useTaskStore = defineStore('task', {
  state: () => ({
    items: [],
    filter: 'all'
  }),
  getters: {
    filteredItems: (state) => {
      if (state.filter === 'active') return state.items.filter(t => !t.done)
      if (state.filter === 'completed') return state.items.filter(t => t.done)
      return state.items
    },
    totalCount: (state) => state.items.length
  },
  actions: {
    add(text) {
      this.items.push({ id: Date.now(), text, done: false })
    },
    toggle(id) {
      const task = this.items.find(t => t.id === id)
      if (task) task.done = !task.done
    },
    async loadFromServer() {
      const res = await this.$http.get('tasks').json()
      this.items = res
    }
  },
  persist: {
    key: 'task-store',
    storage: localStorage,
    paths: ['items']
  }
})

コンポーネント利用例

<template>
  <ul>
    <li v-for="task in filteredItems" :key="task.id">
      <input type="checkbox" v-model="task.done" @change="toggle(task.id)">
      {{ task.text }}
    </li>
  </ul>
</template>

<script setup>
import { useTaskStore } from '@/stores/task'
const { filteredItems, toggle } = useTaskStore()
</script>

HTTP通信実装(Ky + AbortController)

import ky from 'ky'

const api = ky.create({
  prefixUrl: import.meta.env.VITE_API_BASE,
  headers: { 'Accept': 'application/json' },
  hooks: {
    beforeRequest: [
      request => {
        const token = getToken()
        if (token) request.headers.set('Authorization', `Bearer ${token}`)
      }
    ],
    afterResponse: [
      async (request, options, response) => {
        if (!response.ok) throw new Error(await response.text())
        return response
      }
    ]
  }
})

// ダウンロード処理
export async function downloadReport(format = 'xlsx') {
  const controller = new AbortController()
  const res = await api.get(`report.${format}`, {
    signal: controller.signal,
    searchParams: { format }
  })
  const blob = await res.blob()
  const url = URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `report.${format}`
  a.click()
  URL.revokeObjectURL(url)
}

// アップロード進捗
export async function uploadFile(file) {
  const body = new FormData()
  body.append('file', file)
  return api.post('upload', { body }).json()
}

リクエストキャンセル実装

import { ref } from 'vue'

export function useApi() {
  const controller = ref(new AbortController())
  
  const request = async (url, options = {}) => {
    controller.value.abort()
    controller.value = new AbortController()
    return api.get(url, { ...options, signal: controller.value.signal }).json()
  }
  
  return { request }
}

タグ: Vue3 Pinia vue-router ky abortcontroller

7月29日 07:56 投稿