Vue3项目架构选型与初始化
Vue3组合式API配合TypeScript和Vite,是目前中大型前端项目的标准技术栈。用create-vue初始化项目,按需引入Router、Pinia、ESLint:
npm create vue@latest my-app
# 选择 TypeScript + Router + Pinia + ESLint
cd my-app && npm install
项目目录结构规划:
src/
api/ # 接口请求封装
assets/ # 静态资源
components/ # 公共组件
composables/ # 组合式函数(hooks)
layouts/ # 布局组件
pages/ # 页面级组件
router/ # 路由配置
stores/ # Pinia状态管理
types/ # TypeScript类型定义
utils/ # 工具函数
组合式API封装业务逻辑:Composable函数设计
把可复用的业务逻辑抽到composables目录,用ref/reactive管理响应式状态:
// composables/usePagination.ts
import { ref, computed } from 'vue'
export function usePagination(fetchFn, pageSize = 10) {
const currentPage = ref(1)
const total = ref(0)
const loading = ref(false)
const data = ref([])
const totalPages = computed(() => Math.ceil(total.value / pageSize))
async function fetchPage(page: number) {
loading.value = true
try {
const res = await fetchFn({ page, pageSize })
data.value = res.list
total.value = res.total
currentPage.value = page
} finally {
loading.value = false
}
}
function nextPage() {
if (currentPage.value < totalPages.value) fetchPage(currentPage.value + 1)
}
function prevPage() {
if (currentPage.value > 1) fetchPage(currentPage.value - 1)
}
return { data, currentPage, totalPages, total, loading, fetchPage, nextPage, prevPage }
}
使用时直接解构调用,逻辑清晰且可跨组件复用:
// pages/UserList.vue
import { usePagination } from '@/composables/usePagination'
import { getUserList } from '@/api/user'
const { data, currentPage, totalPages, loading, fetchPage } = usePagination(getUserList)
onMounted(() => fetchPage(1))
Pinia状态管理:模块化Store设计
Pinia相比Vuex更轻量,用defineStore定义模块:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)
const isLoggedIn = computed(() => !!token.value)
async function login(credentials) {
const res = await loginApi(credentials)
token.value = res.token
userInfo.value = res.userInfo
localStorage.setItem('token', res.token)
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { token, userInfo, isLoggedIn, login, logout }
})
接口请求层封装:统一错误处理与Token刷新
// utils/request.ts
import axios from 'axios'
import { useUserStore } from '@/stores/user'
import router from '@/router'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 15000
})
request.interceptors.request.use(config => {
const userStore = useUserStore()
if (userStore.token) {
config.headers.Authorization = `Bearer ${userStore.token}`
}
return config
})
request.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
useUserStore().logout()
router.push('/login')
}
return Promise.reject(error)
}
)
export default request
路由守卫与权限控制
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('@/pages/Login.vue') },
{ path: '/dashboard', component: () => import('@/pages/Dashboard.vue'), meta: { auth: true } },
{ path: '/admin', component: () => import('@/pages/Admin.vue'), meta: { auth: true, role: 'admin' } }
]
})
router.beforeEach((to) => {
const userStore = useUserStore()
if (to.meta.auth && !userStore.isLoggedIn) return '/login'
if (to.meta.role && userStore.userInfo?.role !== to.meta.role) return '/403'
})
生产构建优化配置
vite.config.ts关键优化项:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-vendor': ['element-plus'],
'utils': ['axios', 'dayjs', 'lodash-es']
}
}
},
chunkSizeWarningLimit: 1000
}
})
manualChunks把第三方库拆成独立chunk,利用浏览器缓存减少重复下载。Element Plus这样的大UI库单独分包,只在需要时加载。
常见架构问题
Q: Composable函数中的异步操作如何处理竞态?
用AbortController取消前一次请求:
let controller = null
async function search(keyword) {
controller?.abort()
controller = new AbortController()
try {
const res = await fetchApi(keyword, { signal: controller.signal })
data.value = res
} catch (e) {
if (e.name !== 'AbortError') throw e
}
}
Q: 大型项目如何管理路由配置?
按功能模块拆分路由文件,用动态import实现懒加载。路由meta字段配置权限标识,守卫统一拦截。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-xiang-mu-jia-gou-shi-zhan-cong-jiao-shou/