エントリポイント統合
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 }
}