Vue.js现代开发实践指南
Vue.js是一个渐进式JavaScript框架,以其简洁的API、优秀的性能和丰富的生态系统成为现代前端开发的热门选择。Vue 3引入的组合式API、更好的TypeScript支持和性能优化,使其成为构建现代Web应用的理想选择。
核心价值
Vue.js = 渐进式 + 响应式 + 组件化 + 生态丰富
- 🎯 渐进式框架:可以逐步采用,从简单页面到复杂应用
- ⚡ 响应式系统:基于Proxy的高性能响应式数据绑定
- 🧩 组件化开发:单文件组件,开发体验优秀
- 🔧 组合式API:更好的逻辑复用和TypeScript支持
- 📦 丰富生态:Vue Router、Pinia、Nuxt.js等完整解决方案
- 🎨 开发体验:优秀的开发工具和调试支持
1. Vue 3核心特性与架构
1.1 Vue 3架构演进
Vue 3相比Vue 2在架构上有了重大改进,引入了组合式API、更好的性能和TypeScript支持。
Vue 2 vs Vue 3 对比
| 特性对比 | Vue 2 | Vue 3 | 改进说明 |
|---|---|---|---|
| 响应式系统 | Object.defineProperty | Proxy | 更好的性能,支持数组和对象 |
| API风格 | 选项式API | 组合式API + 选项式API | 更好的逻辑复用和TypeScript支持 |
| 性能 | 基准性能 | 2x更快 | 编译优化、Tree-shaking |
| 包大小 | ~34KB | ~16KB | 更好的Tree-shaking |
| TypeScript | 部分支持 | 完全支持 | 原生TypeScript支持 |
| 多根节点 | 不支持 | 支持 | Fragment支持 |
1.2 组合式API深度解析
组合式API是Vue 3的核心特性,提供了更灵活的逻辑组织方式和更好的TypeScript支持。
- 组合式API基础
- 组合式函数
- 响应式系统
基础组合式API使用
组合式API基础示例
vue
1<template>2 <div class="user-profile">3 <!-- 用户信息展示 -->4 <div class="user-info" v-if="!loading">5 <img :src="user?.avatar" :alt="user?.name" class="avatar" />6 <div class="details">7 <h2>{{ user?.name }}</h2>8 <p>{{ user?.email }}</p>9 <span class="role">{{ user?.role }}</span>10 </div>11 </div>12 13 <!-- 加载状态 -->14 <div v-else class="loading">15 <div class="spinner"></div>16 <p>加载中...</p>17 </div>18 19 <!-- 错误状态 -->20 <div v-if="error" class="error">21 <p>{{ error }}</p>22 <button @click="retry">重试</button>23 </div>24 25 <!-- 用户操作 -->26 <div class="actions" v-if="user">27 <button @click="toggleEdit" class="btn-primary">28 {{ isEditing ? '取消编辑' : '编辑资料' }}29 </button>30 <button @click="logout" class="btn-secondary">登出</button>31 </div>32 33 <!-- 编辑表单 -->34 <div v-if="isEditing" class="edit-form">35 <form @submit.prevent="saveUser">36 <div class="form-group">37 <label for="name">姓名:</label>38 <input39 id="name"40 v-model="editForm.name"41 type="text"42 required43 class="form-input"44 />45 </div>46 47 <div class="form-group">48 <label for="email">邮箱:</label>49 <input50 id="email"51 v-model="editForm.email"52 type="email"53 required54 class="form-input"55 />56 </div>57 58 <div class="form-actions">59 <button type="submit" :disabled="saving" class="btn-primary">60 {{ saving ? '保存中...' : '保存' }}61 </button>62 <button type="button" @click="cancelEdit" class="btn-secondary">63 取消64 </button>65 </div>66 </form>67 </div>68 </div>69</template>7071<script setup lang="ts">72import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'73import { useRouter } from 'vue-router'74import { useUserStore } from '@/stores/user'7576// 类型定义77interface User {78 id: number79 name: string80 email: string81 avatar: string82 role: 'admin' | 'user' | 'guest'83}8485interface EditForm {86 name: string87 email: string88}8990// 响应式数据91const user = ref<User | null>(null)92const loading = ref(true)93const error = ref<string | null>(null)94const isEditing = ref(false)95const saving = ref(false)9697// 响应式对象98const editForm = reactive<EditForm>({99 name: '',100 email: ''101})102103// 组合式函数104const router = useRouter()105const userStore = useUserStore()106107// 计算属性108const isAdmin = computed(() => user.value?.role === 'admin')109const canEdit = computed(() => user.value && (isAdmin.value || user.value.id === userStore.currentUserId))110111// 生命周期钩子112onMounted(async () => {113 await fetchUser()114})115116// 侦听器117watch(user, (newUser) => {118 if (newUser) {119 editForm.name = newUser.name120 editForm.email = newUser.email121 }122}, { immediate: true })123124// 方法定义125const fetchUser = async () => {126 try {127 loading.value = true128 error.value = null129 130 const response = await fetch('/api/user/profile')131 if (!response.ok) {132 throw new Error('获取用户信息失败')133 }134 135 user.value = await response.json()136 } catch (err) {137 error.value = err instanceof Error ? err.message : '未知错误'138 } finally {139 loading.value = false140 }141}142143const toggleEdit = () => {144 if (!canEdit.value) return145 146 isEditing.value = !isEditing.value147 148 if (isEditing.value) {149 // 进入编辑模式时,聚焦到第一个输入框150 nextTick(() => {151 const firstInput = document.querySelector('.edit-form input') as HTMLInputElement152 firstInput?.focus()153 })154 }155}156157const saveUser = async () => {158 if (!user.value) return159 160 try {161 saving.value = true162 163 const response = await fetch(`/api/users/${user.value.id}`, {164 method: 'PATCH',165 headers: {166 'Content-Type': 'application/json'167 },168 body: JSON.stringify(editForm)169 })170 171 if (!response.ok) {172 throw new Error('保存失败')173 }174 175 const updatedUser = await response.json()176 user.value = updatedUser177 isEditing.value = false178 179 // 显示成功消息180 userStore.showMessage('保存成功', 'success')181 } catch (err) {182 error.value = err instanceof Error ? err.message : '保存失败'183 } finally {184 saving.value = false185 }186}187188const cancelEdit = () => {189 if (user.value) {190 editForm.name = user.value.name191 editForm.email = user.value.email192 }193 isEditing.value = false194}195196const retry = () => {197 fetchUser()198}199200const logout = async () => {201 await userStore.logout()202 router.push('/login')203}204</script>205206<style scoped>207.user-profile {208 max-width: 600px;209 margin: 0 auto;210 padding: 20px;211}212213.user-info {214 display: flex;215 align-items: center;216 gap: 20px;217 padding: 20px;218 border: 1px solid #e0e0e0;219 border-radius: 8px;220 background: #fff;221}222223.avatar {224 width: 80px;225 height: 80px;226 border-radius: 50%;227 object-fit: cover;228}229230.details h2 {231 margin: 0 0 8px 0;232 color: #333;233}234235.details p {236 margin: 0 0 8px 0;237 color: #666;238}239240.role {241 display: inline-block;242 padding: 4px 8px;243 background: #e3f2fd;244 color: #1976d2;245 border-radius: 4px;246 font-size: 12px;247}248249.loading, .error {250 text-align: center;251 padding: 40px;252}253254.spinner {255 width: 40px;256 height: 40px;257 border: 4px solid #f3f3f3;258 border-top: 4px solid #3498db;259 border-radius: 50%;260 animation: spin 1s linear infinite;261 margin: 0 auto 16px;262}263264@keyframes spin {265 0% { transform: rotate(0deg); }266 100% { transform: rotate(360deg); }267}268269.actions {270 display: flex;271 gap: 12px;272 margin-top: 20px;273}274275.edit-form {276 margin-top: 20px;277 padding: 20px;278 border: 1px solid #e0e0e0;279 border-radius: 8px;280 background: #f9f9f9;281}282283.form-group {284 margin-bottom: 16px;285}286287.form-group label {288 display: block;289 margin-bottom: 4px;290 font-weight: 500;291}292293.form-input {294 width: 100%;295 padding: 8px 12px;296 border: 1px solid #ddd;297 border-radius: 4px;298 font-size: 14px;299}300301.form-input:focus {302 outline: none;303 border-color: #3498db;304 box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);305}306307.form-actions {308 display: flex;309 gap: 12px;310}311312.btn-primary, .btn-secondary {313 padding: 8px 16px;314 border: none;315 border-radius: 4px;316 cursor: pointer;317 font-size: 14px;318 transition: background-color 0.2s;319}320321.btn-primary {322 background: #3498db;323 color: white;324}325326.btn-primary:hover:not(:disabled) {327 background: #2980b9;328}329330.btn-primary:disabled {331 background: #bdc3c7;332 cursor: not-allowed;333}334335.btn-secondary {336 background: #95a5a6;337 color: white;338}339340.btn-secondary:hover {341 background: #7f8c8d;342}343344.error {345 color: #e74c3c;346}347</style>自定义组合式函数
组合式函数最佳实践
typescript
1// composables/useApi.ts2import { ref, computed } from 'vue'34export interface ApiState<T> {5 data: Ref<T | null>6 loading: Ref<boolean>7 error: Ref<string | null>8 execute: (...args: any[]) => Promise<T>9 reset: () => void10}1112export function useApi<T>(13 apiFunction: (...args: any[]) => Promise<T>14): ApiState<T> {15 const data = ref<T | null>(null)16 const loading = ref(false)17 const error = ref<string | null>(null)18 19 const execute = async (...args: any[]): Promise<T> => {20 try {21 loading.value = true22 error.value = null23 24 const result = await apiFunction(...args)25 data.value = result26 return result27 } catch (err) {28 error.value = err instanceof Error ? err.message : '请求失败'29 throw err30 } finally {31 loading.value = false32 }33 }34 35 const reset = () => {36 data.value = null37 loading.value = false38 error.value = null39 }40 41 return {42 data,43 loading,44 error,45 execute,46 reset47 }48}4950// composables/useLocalStorage.ts51import { ref, watch, Ref } from 'vue'5253export function useLocalStorage<T>(54 key: string,55 defaultValue: T56): [Ref<T>, (value: T) => void] {57 const storedValue = localStorage.getItem(key)58 const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue59 60 const state = ref<T>(initialValue)61 62 const setValue = (value: T) => {63 state.value = value64 }65 66 watch(67 state,68 (newValue) => {69 localStorage.setItem(key, JSON.stringify(newValue))70 },71 { deep: true }72 )73 74 return [state, setValue]75}7677// composables/useDebounce.ts78import { ref, watch, Ref } from 'vue'7980export function useDebounce<T>(81 value: Ref<T>,82 delay: number = 30083): Ref<T> {84 const debouncedValue = ref<T>(value.value)85 86 watch(value, (newValue) => {87 const timer = setTimeout(() => {88 debouncedValue.value = newValue89 }, delay)90 91 return () => clearTimeout(timer)92 })93 94 return debouncedValue95}9697// composables/useIntersectionObserver.ts98import { ref, onMounted, onUnmounted, Ref } from 'vue'99100export function useIntersectionObserver(101 target: Ref<Element | null>,102 options: IntersectionObserverInit = {}103) {104 const isIntersecting = ref(false)105 const isSupported = typeof IntersectionObserver !== 'undefined'106 107 let observer: IntersectionObserver | null = null108 109 const cleanup = () => {110 if (observer) {111 observer.disconnect()112 observer = null113 }114 }115 116 const observe = () => {117 if (!isSupported || !target.value) return118 119 cleanup()120 121 observer = new IntersectionObserver(([entry]) => {122 isIntersecting.value = entry.isIntersecting123 }, options)124 125 observer.observe(target.value)126 }127 128 onMounted(observe)129 onUnmounted(cleanup)130 131 return {132 isIntersecting,133 isSupported,134 observe,135 cleanup136 }137}138139// 使用示例140export default defineComponent({141 setup() {142 // API调用143 const { data: users, loading, error, execute: fetchUsers } = useApi(144 () => fetch('/api/users').then(res => res.json())145 )146 147 // 本地存储148 const [preferences, setPreferences] = useLocalStorage('userPreferences', {149 theme: 'light',150 language: 'zh-CN'151 })152 153 // 防抖搜索154 const searchQuery = ref('')155 const debouncedQuery = useDebounce(searchQuery, 500)156 157 watch(debouncedQuery, (query) => {158 if (query) {159 // 执行搜索160 console.log('搜索:', query)161 }162 })163 164 // 无限滚动165 const loadMoreTrigger = ref<HTMLElement | null>(null)166 const { isIntersecting } = useIntersectionObserver(loadMoreTrigger)167 168 watch(isIntersecting, (intersecting) => {169 if (intersecting) {170 // 加载更多数据171 console.log('加载更多')172 }173 })174 175 onMounted(() => {176 fetchUsers()177 })178 179 return {180 users,181 loading,182 error,183 preferences,184 setPreferences,185 searchQuery,186 loadMoreTrigger187 }188 }189})响应式系统深入理解
Vue 3响应式系统详解
typescript
1import { 2 ref, 3 reactive, 4 computed, 5 watch, 6 watchEffect,7 readonly,8 shallowRef,9 shallowReactive,10 toRef,11 toRefs,12 unref,13 isRef,14 isReactive15} from 'vue'1617// 1. 基础响应式API18export function useReactivityDemo() {19 // ref: 基础类型响应式20 const count = ref(0)21 const message = ref('Hello Vue 3')22 23 // reactive: 对象响应式24 const state = reactive({25 user: {26 name: 'John',27 age: 3028 },29 settings: {30 theme: 'dark',31 notifications: true32 }33 })34 35 // readonly: 只读响应式36 const readonlyState = readonly(state)37 38 // 计算属性39 const doubleCount = computed(() => count.value * 2)40 const userInfo = computed(() => `${state.user.name} (${state.user.age}岁)`)41 42 // 可写计算属性43 const fullName = computed({44 get: () => `${state.user.name}`,45 set: (value: string) => {46 state.user.name = value47 }48 })49 50 return {51 count,52 message,53 state,54 readonlyState,55 doubleCount,56 userInfo,57 fullName58 }59}6061// 2. 高级响应式模式62export function useAdvancedReactivity() {63 // 浅层响应式64 const shallowState = shallowReactive({65 deep: {66 nested: {67 value: 168 }69 }70 })71 72 const shallowCount = shallowRef({ count: 0 })73 74 // toRef 和 toRefs75 const state = reactive({76 name: 'Vue',77 version: '3.0',78 features: ['Composition API', 'TypeScript']79 })80 81 const name = toRef(state, 'name')82 const { version, features } = toRefs(state)83 84 // 响应式判断85 const checkReactivity = () => {86 console.log('count is ref:', isRef(count))87 console.log('state is reactive:', isReactive(state))88 console.log('unref count:', unref(count))89 }90 91 return {92 shallowState,93 shallowCount,94 name,95 version,96 features,97 checkReactivity98 }99}100101// 3. 侦听器模式102export function useWatchers() {103 const count = ref(0)104 const state = reactive({105 name: 'Vue',106 nested: {107 value: 1108 }109 })110 111 // 基础侦听器112 watch(count, (newValue, oldValue) => {113 console.log(`count changed from ${oldValue} to ${newValue}`)114 })115 116 // 侦听多个源117 watch([count, () => state.name], ([newCount, newName], [oldCount, oldName]) => {118 console.log('Multiple sources changed:', {119 count: { old: oldCount, new: newCount },120 name: { old: oldName, new: newName }121 })122 })123 124 // 深度侦听125 watch(126 () => state.nested,127 (newValue, oldValue) => {128 console.log('Nested object changed:', newValue)129 },130 { deep: true }131 )132 133 // 立即执行侦听器134 watch(135 count,136 (value) => {137 console.log('Immediate watch:', value)138 },139 { immediate: true }140 )141 142 // watchEffect: 自动追踪依赖143 const stopWatcher = watchEffect(() => {144 console.log(`Count is ${count.value}, name is ${state.name}`)145 })146 147 // 停止侦听器148 const stopAllWatchers = () => {149 stopWatcher()150 }151 152 // 异步侦听器153 watchEffect(async (onInvalidate) => {154 const controller = new AbortController()155 156 onInvalidate(() => {157 controller.abort()158 })159 160 try {161 const response = await fetch(`/api/data/${count.value}`, {162 signal: controller.signal163 })164 const data = await response.json()165 console.log('Fetched data:', data)166 } catch (error) {167 if (error.name !== 'AbortError') {168 console.error('Fetch error:', error)169 }170 }171 })172 173 return {174 count,175 state,176 stopAllWatchers177 }178}179180// 4. 响应式工具函数181export function useReactivityUtils() {182 // 响应式转换工具183 const convertToReactive = <T extends object>(obj: T): T => {184 return reactive(obj)185 }186 187 // 深度只读转换188 const makeDeepReadonly = <T>(obj: T): Readonly<T> => {189 return readonly(obj)190 }191 192 // 响应式数据克隆193 const cloneReactive = <T extends object>(source: T): T => {194 return reactive(JSON.parse(JSON.stringify(source)))195 }196 197 // 响应式数据合并198 const mergeReactive = <T extends object, U extends object>(199 target: T,200 source: U201 ): T & U => {202 return reactive({ ...target, ...source })203 }204 205 return {206 convertToReactive,207 makeDeepReadonly,208 cloneReactive,209 mergeReactive210 }211}212213// 使用示例组件214export default defineComponent({215 setup() {216 const { count, state, doubleCount } = useReactivityDemo()217 const { name, version } = useAdvancedReactivity()218 const { stopAllWatchers } = useWatchers()219 220 // 组合多个组合式函数221 const increment = () => {222 count.value++223 }224 225 const updateUser = () => {226 state.user.name = 'Jane'227 state.user.age = 25228 }229 230 onUnmounted(() => {231 stopAllWatchers()232 })233 234 return {235 count,236 state,237 doubleCount,238 name,239 version,240 increment,241 updateUser242 }243 }244})2. Vue组件化开发
2.1 单文件组件(SFC)架构
Vue的单文件组件提供了优秀的开发体验,将模板、逻辑和样式封装在一个文件中。
组件设计模式对比
| 设计模式 | 适用场景 | 优势 | 劣势 | 示例 |
|---|---|---|---|---|
| 展示组件 | UI渲染 | 可复用、易测试 | 功能单一 | Button, Card, Modal |
| 容器组件 | 业务逻辑 | 逻辑集中 | 耦合度高 | UserList, ProductManager |
| 高阶组件 | 功能增强 | 横切关注点 | 复杂度高 | withAuth, withLoading |
| Renderless组件 | 逻辑复用 | 灵活性高 | 理解成本 | DataProvider, FormValidator |
- 组件通信
- 依赖注入
- 插槽系统
组件通信完整方案
父子组件通信示例
vue
1<!-- 父组件: UserManagement.vue -->2<template>3 <div class="user-management">4 <div class="header">5 <h1>用户管理</h1>6 <button @click="showAddModal = true" class="btn-primary">7 添加用户8 </button>9 </div>10 11 <!-- 搜索和过滤 -->12 <UserFilters13 v-model:search="searchQuery"14 v-model:role="selectedRole"15 :roles="availableRoles"16 @reset="resetFilters"17 />18 19 <!-- 用户列表 -->20 <UserList21 :users="filteredUsers"22 :loading="loading"23 @edit="handleEditUser"24 @delete="handleDeleteUser"25 @toggle-status="handleToggleStatus"26 />27 28 <!-- 添加/编辑用户模态框 -->29 <UserModal30 v-model:visible="showAddModal"31 :user="editingUser"32 :mode="modalMode"33 @save="handleSaveUser"34 @cancel="handleCancelEdit"35 />36 37 <!-- 确认删除对话框 -->38 <ConfirmDialog39 v-model:visible="showDeleteDialog"40 title="确认删除"41 :message="`确定要删除用户 ${deletingUser?.name} 吗?`"42 @confirm="confirmDelete"43 @cancel="showDeleteDialog = false"44 />45 </div>46</template>4748<script setup lang="ts">49import { ref, computed, onMounted } from 'vue'50import UserFilters from './components/UserFilters.vue'51import UserList from './components/UserList.vue'52import UserModal from './components/UserModal.vue'53import ConfirmDialog from './components/ConfirmDialog.vue'54import { useUserApi } from '@/composables/useUserApi'55import { useNotification } from '@/composables/useNotification'5657// 类型定义58interface User {59 id: number60 name: string61 email: string62 role: 'admin' | 'user' | 'guest'63 status: 'active' | 'inactive'64 avatar?: string65 createdAt: string66}6768type ModalMode = 'add' | 'edit'6970// 响应式数据71const searchQuery = ref('')72const selectedRole = ref<string>('')73const showAddModal = ref(false)74const showDeleteDialog = ref(false)75const editingUser = ref<User | null>(null)76const deletingUser = ref<User | null>(null)77const modalMode = ref<ModalMode>('add')7879// 组合式函数80const { users, loading, fetchUsers, createUser, updateUser, deleteUser } = useUserApi()81const { showSuccess, showError } = useNotification()8283// 计算属性84const availableRoles = computed(() => [85 { value: '', label: '全部角色' },86 { value: 'admin', label: '管理员' },87 { value: 'user', label: '普通用户' },88 { value: 'guest', label: '访客' }89])9091const filteredUsers = computed(() => {92 return users.value.filter(user => {93 const matchesSearch = !searchQuery.value || 94 user.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||95 user.email.toLowerCase().includes(searchQuery.value.toLowerCase())96 97 const matchesRole = !selectedRole.value || user.role === selectedRole.value98 99 return matchesSearch && matchesRole100 })101})102103// 事件处理104const handleEditUser = (user: User) => {105 editingUser.value = { ...user }106 modalMode.value = 'edit'107 showAddModal.value = true108}109110const handleDeleteUser = (user: User) => {111 deletingUser.value = user112 showDeleteDialog.value = true113}114115const handleToggleStatus = async (user: User) => {116 try {117 const newStatus = user.status === 'active' ? 'inactive' : 'active'118 await updateUser(user.id, { status: newStatus })119 showSuccess(`用户状态已更新为${newStatus === 'active' ? '激活' : '禁用'}`)120 } catch (error) {121 showError('更新用户状态失败')122 }123}124125const handleSaveUser = async (userData: Partial<User>) => {126 try {127 if (modalMode.value === 'add') {128 await createUser(userData)129 showSuccess('用户创建成功')130 } else {131 await updateUser(editingUser.value!.id, userData)132 showSuccess('用户更新成功')133 }134 135 showAddModal.value = false136 editingUser.value = null137 } catch (error) {138 showError(modalMode.value === 'add' ? '创建用户失败' : '更新用户失败')139 }140}141142const handleCancelEdit = () => {143 showAddModal.value = false144 editingUser.value = null145}146147const confirmDelete = async () => {148 if (!deletingUser.value) return149 150 try {151 await deleteUser(deletingUser.value.id)152 showSuccess('用户删除成功')153 showDeleteDialog.value = false154 deletingUser.value = null155 } catch (error) {156 showError('删除用户失败')157 }158}159160const resetFilters = () => {161 searchQuery.value = ''162 selectedRole.value = ''163}164165// 生命周期166onMounted(() => {167 fetchUsers()168})169</script>170171<!-- 子组件: UserFilters.vue -->172<template>173 <div class="user-filters">174 <div class="filter-group">175 <label for="search">搜索用户:</label>176 <input177 id="search"178 :value="search"179 @input="$emit('update:search', $event.target.value)"180 type="text"181 placeholder="输入姓名或邮箱..."182 class="search-input"183 />184 </div>185 186 <div class="filter-group">187 <label for="role">角色筛选:</label>188 <select189 id="role"190 :value="role"191 @change="$emit('update:role', $event.target.value)"192 class="role-select"193 >194 <option195 v-for="roleOption in roles"196 :key="roleOption.value"197 :value="roleOption.value"198 >199 {{ roleOption.label }}200 </option>201 </select>202 </div>203 204 <button @click="$emit('reset')" class="btn-secondary">205 重置筛选206 </button>207 </div>208</template>209210<script setup lang="ts">211// Props定义212interface Props {213 search: string214 role: string215 roles: Array<{ value: string; label: string }>216}217218defineProps<Props>()219220// 事件定义221interface Emits {222 'update:search': [value: string]223 'update:role': [value: string]224 'reset': []225}226227defineEmits<Emits>()228</script>229230<!-- 子组件: UserList.vue -->231<template>232 <div class="user-list">233 <div v-if="loading" class="loading">234 <div class="spinner"></div>235 <p>加载中...</p>236 </div>237 238 <div v-else-if="users.length === 0" class="empty-state">239 <p>暂无用户数据</p>240 </div>241 242 <div v-else class="user-grid">243 <div244 v-for="user in users"245 :key="user.id"246 class="user-card"247 :class="{ 'user-card--inactive': user.status === 'inactive' }"248 >249 <div class="user-avatar">250 <img251 :src="user.avatar || '/default-avatar.png'"252 :alt="user.name"253 class="avatar-image"254 />255 <div class="status-indicator" :class="`status--${user.status}`"></div>256 </div>257 258 <div class="user-info">259 <h3 class="user-name">{{ user.name }}</h3>260 <p class="user-email">{{ user.email }}</p>261 <span class="user-role" :class="`role--${user.role}`">262 {{ getRoleLabel(user.role) }}263 </span>264 </div>265 266 <div class="user-actions">267 <button268 @click="$emit('edit', user)"269 class="btn-icon"270 title="编辑"271 >272 ✏️273 </button>274 <button275 @click="$emit('toggle-status', user)"276 class="btn-icon"277 :title="user.status === 'active' ? '禁用' : '启用'"278 >279 {{ user.status === 'active' ? '🔒' : '🔓' }}280 </button>281 <button282 @click="$emit('delete', user)"283 class="btn-icon btn-danger"284 title="删除"285 >286 🗑️287 </button>288 </div>289 </div>290 </div>291 </div>292</template>293294<script setup lang="ts">295// Props和Emits定义296interface User {297 id: number298 name: string299 email: string300 role: 'admin' | 'user' | 'guest'301 status: 'active' | 'inactive'302 avatar?: string303}304305interface Props {306 users: User[]307 loading: boolean308}309310interface Emits {311 'edit': [user: User]312 'delete': [user: User]313 'toggle-status': [user: User]314}315316defineProps<Props>()317defineEmits<Emits>()318319// 工具函数320const getRoleLabel = (role: string) => {321 const roleLabels = {322 admin: '管理员',323 user: '普通用户',324 guest: '访客'325 }326 return roleLabels[role] || role327}328</script>Provide/Inject模式
依赖注入完整示例
vue
1<!-- 根组件: App.vue -->2<template>3 <div id="app">4 <ThemeProvider>5 <UserProvider>6 <NotificationProvider>7 <router-view />8 </NotificationProvider>9 </UserProvider>10 </ThemeProvider>11 </div>12</template>1314<script setup lang="ts">15import ThemeProvider from './providers/ThemeProvider.vue'16import UserProvider from './providers/UserProvider.vue'17import NotificationProvider from './providers/NotificationProvider.vue'18</script>1920<!-- 主题提供者: ThemeProvider.vue -->21<template>22 <div :class="`theme-${theme}`">23 <slot />24 </div>25</template>2627<script setup lang="ts">28import { ref, provide, computed } from 'vue'2930// 主题类型定义31type Theme = 'light' | 'dark' | 'auto'3233interface ThemeContext {34 theme: Ref<Theme>35 toggleTheme: () => void36 setTheme: (theme: Theme) => void37 isDark: ComputedRef<boolean>38}3940// 注入键41export const ThemeKey = Symbol('theme') as InjectionKey<ThemeContext>4243// 响应式状态44const theme = ref<Theme>('light')4546// 计算属性47const isDark = computed(() => {48 if (theme.value === 'auto') {49 return window.matchMedia('(prefers-color-scheme: dark)').matches50 }51 return theme.value === 'dark'52})5354// 方法55const toggleTheme = () => {56 theme.value = theme.value === 'light' ? 'dark' : 'light'57}5859const setTheme = (newTheme: Theme) => {60 theme.value = newTheme61}6263// 提供主题上下文64provide(ThemeKey, {65 theme,66 toggleTheme,67 setTheme,68 isDark69})7071// 监听系统主题变化72if (theme.value === 'auto') {73 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')74 mediaQuery.addEventListener('change', () => {75 // 触发重新计算76 theme.value = 'auto'77 })78}79</script>8081<!-- 用户提供者: UserProvider.vue -->82<template>83 <slot />84</template>8586<script setup lang="ts">87import { ref, provide, computed } from 'vue'88import { useRouter } from 'vue-router'8990// 用户类型定义91interface User {92 id: number93 name: string94 email: string95 role: 'admin' | 'user' | 'guest'96 avatar?: string97 permissions: string[]98}99100interface UserContext {101 user: Ref<User | null>102 loading: Ref<boolean>103 login: (credentials: LoginCredentials) => Promise<void>104 logout: () => Promise<void>105 updateProfile: (updates: Partial<User>) => Promise<void>106 hasPermission: (permission: string) => boolean107 isAdmin: ComputedRef<boolean>108}109110// 注入键111export const UserKey = Symbol('user') as InjectionKey<UserContext>112113// 响应式状态114const user = ref<User | null>(null)115const loading = ref(false)116117// 路由118const router = useRouter()119120// 计算属性121const isAdmin = computed(() => user.value?.role === 'admin')122123// 方法124const login = async (credentials: LoginCredentials) => {125 try {126 loading.value = true127 128 const response = await fetch('/api/auth/login', {129 method: 'POST',130 headers: { 'Content-Type': 'application/json' },131 body: JSON.stringify(credentials)132 })133 134 if (!response.ok) {135 throw new Error('登录失败')136 }137 138 const { user: userData, token } = await response.json()139 140 localStorage.setItem('token', token)141 user.value = userData142 143 router.push('/dashboard')144 } catch (error) {145 throw error146 } finally {147 loading.value = false148 }149}150151const logout = async () => {152 try {153 await fetch('/api/auth/logout', { method: 'POST' })154 } catch (error) {155 console.error('Logout error:', error)156 } finally {157 localStorage.removeItem('token')158 user.value = null159 router.push('/login')160 }161}162163const updateProfile = async (updates: Partial<User>) => {164 if (!user.value) return165 166 try {167 const response = await fetch(`/api/users/${user.value.id}`, {168 method: 'PATCH',169 headers: { 'Content-Type': 'application/json' },170 body: JSON.stringify(updates)171 })172 173 if (!response.ok) {174 throw new Error('更新失败')175 }176 177 const updatedUser = await response.json()178 user.value = updatedUser179 } catch (error) {180 throw error181 }182}183184const hasPermission = (permission: string): boolean => {185 return user.value?.permissions.includes(permission) || false186}187188// 提供用户上下文189provide(UserKey, {190 user,191 loading,192 login,193 logout,194 updateProfile,195 hasPermission,196 isAdmin197})198199// 初始化时检查登录状态200onMounted(async () => {201 const token = localStorage.getItem('token')202 if (token) {203 try {204 const response = await fetch('/api/auth/me', {205 headers: { Authorization: `Bearer ${token}` }206 })207 208 if (response.ok) {209 user.value = await response.json()210 } else {211 localStorage.removeItem('token')212 }213 } catch (error) {214 console.error('Auth check failed:', error)215 localStorage.removeItem('token')216 }217 }218})219</script>220221<!-- 通知提供者: NotificationProvider.vue -->222<template>223 <div>224 <slot />225 226 <!-- 通知容器 -->227 <Teleport to="body">228 <div class="notification-container">229 <TransitionGroup name="notification" tag="div">230 <div231 v-for="notification in notifications"232 :key="notification.id"233 class="notification"234 :class="`notification--${notification.type}`"235 >236 <div class="notification-content">237 <div class="notification-icon">238 {{ getNotificationIcon(notification.type) }}239 </div>240 <div class="notification-text">241 <div class="notification-title">{{ notification.title }}</div>242 <div v-if="notification.message" class="notification-message">243 {{ notification.message }}244 </div>245 </div>246 <button247 @click="removeNotification(notification.id)"248 class="notification-close"249 >250 ×251 </button>252 </div>253 </div>254 </TransitionGroup>255 </div>256 </Teleport>257 </div>258</template>259260<script setup lang="ts">261import { ref, provide } from 'vue'262263// 通知类型定义264type NotificationType = 'success' | 'error' | 'warning' | 'info'265266interface Notification {267 id: string268 type: NotificationType269 title: string270 message?: string271 duration?: number272}273274interface NotificationContext {275 notifications: Ref<Notification[]>276 showNotification: (notification: Omit<Notification, 'id'>) => void277 showSuccess: (title: string, message?: string) => void278 showError: (title: string, message?: string) => void279 showWarning: (title: string, message?: string) => void280 showInfo: (title: string, message?: string) => void281 removeNotification: (id: string) => void282}283284// 注入键285export const NotificationKey = Symbol('notification') as InjectionKey<NotificationContext>286287// 响应式状态288const notifications = ref<Notification[]>([])289290// 方法291const showNotification = (notification: Omit<Notification, 'id'>) => {292 const id = Date.now().toString()293 const newNotification: Notification = {294 id,295 duration: 5000,296 ...notification297 }298 299 notifications.value.push(newNotification)300 301 // 自动移除302 if (newNotification.duration && newNotification.duration > 0) {303 setTimeout(() => {304 removeNotification(id)305 }, newNotification.duration)306 }307}308309const showSuccess = (title: string, message?: string) => {310 showNotification({ type: 'success', title, message })311}312313const showError = (title: string, message?: string) => {314 showNotification({ type: 'error', title, message })315}316317const showWarning = (title: string, message?: string) => {318 showNotification({ type: 'warning', title, message })319}320321const showInfo = (title: string, message?: string) => {322 showNotification({ type: 'info', title, message })323}324325const removeNotification = (id: string) => {326 const index = notifications.value.findIndex(n => n.id === id)327 if (index > -1) {328 notifications.value.splice(index, 1)329 }330}331332const getNotificationIcon = (type: NotificationType) => {333 const icons = {334 success: '✅',335 error: '❌',336 warning: '⚠️',337 info: 'ℹ️'338 }339 return icons[type]340}341342// 提供通知上下文343provide(NotificationKey, {344 notifications,345 showNotification,346 showSuccess,347 showError,348 showWarning,349 showInfo,350 removeNotification351})352</script>353354<!-- 使用依赖注入的组件 -->355<template>356 <div class="settings-page">357 <div class="settings-header">358 <h1>设置</h1>359 <button @click="toggleTheme" class="theme-toggle">360 {{ isDark ? '🌞' : '🌙' }}361 </button>362 </div>363 364 <div class="settings-content">365 <div class="setting-group">366 <h3>主题设置</h3>367 <select v-model="theme" @change="setTheme(theme)">368 <option value="light">浅色主题</option>369 <option value="dark">深色主题</option>370 <option value="auto">跟随系统</option>371 </select>372 </div>373 374 <div class="setting-group" v-if="isAdmin">375 <h3>管理员设置</h3>376 <button @click="showAdminPanel">管理面板</button>377 </div>378 </div>379 </div>380</template>381382<script setup lang="ts">383import { inject } from 'vue'384import { ThemeKey } from '@/providers/ThemeProvider.vue'385import { UserKey } from '@/providers/UserProvider.vue'386import { NotificationKey } from '@/providers/NotificationProvider.vue'387388// 注入依赖389const themeContext = inject(ThemeKey)390const userContext = inject(UserKey)391const notificationContext = inject(NotificationKey)392393if (!themeContext || !userContext || !notificationContext) {394 throw new Error('Required providers not found')395}396397const { theme, toggleTheme, setTheme, isDark } = themeContext398const { isAdmin } = userContext399const { showInfo } = notificationContext400401// 方法402const showAdminPanel = () => {403 showInfo('管理面板', '即将跳转到管理面板')404}405</script>插槽系统高级用法
插槽系统完整示例
vue
1<!-- 基础插槽组件: BaseCard.vue -->2<template>3 <div class="base-card" :class="cardClasses">4 <!-- 头部插槽 -->5 <header v-if="$slots.header" class="card-header">6 <slot name="header" :title="title" :subtitle="subtitle" />7 </header>8 9 <!-- 默认内容插槽 -->10 <main class="card-content">11 <slot :loading="loading" :error="error" />12 </main>13 14 <!-- 底部插槽 -->15 <footer v-if="$slots.footer" class="card-footer">16 <slot name="footer" :actions="actions" />17 </footer>18 19 <!-- 加载状态插槽 -->20 <div v-if="loading && $slots.loading" class="card-loading">21 <slot name="loading" />22 </div>23 24 <!-- 错误状态插槽 -->25 <div v-if="error && $slots.error" class="card-error">26 <slot name="error" :error="error" :retry="retry" />27 </div>28 </div>29</template>3031<script setup lang="ts">32import { computed } from 'vue'3334// Props定义35interface Props {36 title?: string37 subtitle?: string38 variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger'39 size?: 'small' | 'medium' | 'large'40 loading?: boolean41 error?: string | null42 actions?: Array<{ label: string; onClick: () => void }>43}4445const props = withDefaults(defineProps<Props>(), {46 variant: 'default',47 size: 'medium',48 loading: false,49 error: null50})5152// 事件定义53interface Emits {54 retry: []55}5657const emit = defineEmits<Emits>()5859// 计算属性60const cardClasses = computed(() => [61 `card--${props.variant}`,62 `card--${props.size}`,63 {64 'card--loading': props.loading,65 'card--error': props.error66 }67])6869// 方法70const retry = () => {71 emit('retry')72}73</script>7475<!-- 数据表格组件: DataTable.vue -->76<template>77 <BaseCard78 :loading="loading"79 :error="error"80 @retry="$emit('retry')"81 class="data-table-card"82 >83 <!-- 表格头部 -->84 <template #header="{ title, subtitle }">85 <div class="table-header">86 <div class="table-title">87 <h2>{{ title || '数据表格' }}</h2>88 <p v-if="subtitle">{{ subtitle }}</p>89 </div>90 91 <div class="table-actions">92 <slot name="actions" :selected="selectedRows" />93 </div>94 </div>95 </template>96 97 <!-- 表格内容 -->98 <template #default="{ loading, error }">99 <div class="table-container">100 <!-- 表格工具栏 -->101 <div class="table-toolbar">102 <div class="table-filters">103 <slot name="filters" />104 </div>105 106 <div class="table-controls">107 <button108 @click="toggleSelectAll"109 class="btn-secondary"110 :disabled="loading"111 >112 {{ isAllSelected ? '取消全选' : '全选' }}113 </button>114 115 <slot name="controls" :refresh="refresh" />116 </div>117 </div>118 119 <!-- 表格主体 -->120 <div class="table-wrapper">121 <table class="data-table">122 <thead>123 <tr>124 <th v-if="selectable" class="select-column">125 <input126 type="checkbox"127 :checked="isAllSelected"128 @change="toggleSelectAll"129 />130 </th>131 132 <th133 v-for="column in columns"134 :key="column.key"135 :class="getColumnClass(column)"136 @click="handleSort(column)"137 >138 <div class="column-header">139 <span>{{ column.title }}</span>140 <span141 v-if="column.sortable"142 class="sort-indicator"143 :class="getSortClass(column.key)"144 >145 ↕️146 </span>147 </div>148 </th>149 150 <th v-if="$slots.actions" class="actions-column">151 操作152 </th>153 </tr>154 </thead>155 156 <tbody>157 <tr158 v-for="(row, index) in paginatedData"159 :key="getRowKey(row, index)"160 :class="getRowClass(row, index)"161 @click="handleRowClick(row, index)"162 >163 <td v-if="selectable" class="select-cell">164 <input165 type="checkbox"166 :checked="isRowSelected(row)"167 @change="toggleRowSelection(row)"168 @click.stop169 />170 </td>171 172 <td173 v-for="column in columns"174 :key="column.key"175 :class="getCellClass(column, row)"176 >177 <!-- 自定义列渲染 -->178 <slot179 :name="`column-${column.key}`"180 :row="row"181 :column="column"182 :value="getColumnValue(row, column.key)"183 :index="index"184 >185 <!-- 默认列渲染 -->186 <span>{{ getColumnValue(row, column.key) }}</span>187 </slot>188 </td>189 190 <td v-if="$slots.actions" class="actions-cell">191 <slot name="actions" :row="row" :index="index" />192 </td>193 </tr>194 </tbody>195 </table>196 197 <!-- 空状态 -->198 <div v-if="!loading && data.length === 0" class="empty-state">199 <slot name="empty">200 <div class="empty-content">201 <p>暂无数据</p>202 </div>203 </slot>204 </div>205 </div>206 207 <!-- 分页 -->208 <div v-if="pagination" class="table-pagination">209 <slot210 name="pagination"211 :current="currentPage"212 :total="totalPages"213 :pageSize="pageSize"214 :totalItems="data.length"215 :goToPage="goToPage"216 :changePageSize="changePageSize"217 >218 <!-- 默认分页组件 -->219 <DefaultPagination220 :current="currentPage"221 :total="totalPages"222 :page-size="pageSize"223 @change="goToPage"224 @size-change="changePageSize"225 />226 </slot>227 </div>228 </div>229 </template>230 231 <!-- 加载状态 -->232 <template #loading>233 <slot name="loading">234 <div class="table-loading">235 <div class="loading-spinner"></div>236 <p>数据加载中...</p>237 </div>238 </slot>239 </template>240 241 <!-- 错误状态 -->242 <template #error="{ error, retry }">243 <slot name="error" :error="error" :retry="retry">244 <div class="table-error">245 <p>{{ error }}</p>246 <button @click="retry" class="btn-primary">重试</button>247 </div>248 </slot>249 </template>250 </BaseCard>251</template>252253<script setup lang="ts">254import { ref, computed, watch } from 'vue'255import BaseCard from './BaseCard.vue'256import DefaultPagination from './DefaultPagination.vue'257258// 类型定义259interface Column {260 key: string261 title: string262 sortable?: boolean263 width?: string264 align?: 'left' | 'center' | 'right'265 fixed?: 'left' | 'right'266}267268interface Props {269 data: any[]270 columns: Column[]271 loading?: boolean272 error?: string | null273 selectable?: boolean274 pagination?: boolean275 pageSize?: number276 rowKey?: string | ((row: any) => string)277}278279interface Emits {280 'row-click': [row: any, index: number]281 'selection-change': [selectedRows: any[]]282 'sort-change': [column: string, direction: 'asc' | 'desc' | null]283 'retry': []284}285286const props = withDefaults(defineProps<Props>(), {287 loading: false,288 error: null,289 selectable: false,290 pagination: true,291 pageSize: 10,292 rowKey: 'id'293})294295const emit = defineEmits<Emits>()296297// 响应式数据298const selectedRows = ref<any[]>([])299const currentPage = ref(1)300const sortColumn = ref<string | null>(null)301const sortDirection = ref<'asc' | 'desc' | null>(null)302303// 计算属性304const sortedData = computed(() => {305 if (!sortColumn.value || !sortDirection.value) {306 return props.data307 }308 309 return [...props.data].sort((a, b) => {310 const aValue = getColumnValue(a, sortColumn.value!)311 const bValue = getColumnValue(b, sortColumn.value!)312 313 if (aValue < bValue) return sortDirection.value === 'asc' ? -1 : 1314 if (aValue > bValue) return sortDirection.value === 'asc' ? 1 : -1315 return 0316 })317})318319const paginatedData = computed(() => {320 if (!props.pagination) return sortedData.value321 322 const start = (currentPage.value - 1) * props.pageSize323 const end = start + props.pageSize324 return sortedData.value.slice(start, end)325})326327const totalPages = computed(() => {328 return Math.ceil(props.data.length / props.pageSize)329})330331const isAllSelected = computed(() => {332 return props.data.length > 0 && selectedRows.value.length === props.data.length333})334335// 方法336const getRowKey = (row: any, index: number): string => {337 if (typeof props.rowKey === 'function') {338 return props.rowKey(row)339 }340 return row[props.rowKey] || index.toString()341}342343const getColumnValue = (row: any, key: string) => {344 return key.split('.').reduce((obj, k) => obj?.[k], row)345}346347const getColumnClass = (column: Column) => [348 `column-${column.key}`,349 `align-${column.align || 'left'}`,350 {351 'sortable': column.sortable,352 'fixed-left': column.fixed === 'left',353 'fixed-right': column.fixed === 'right'354 }355]356357const getCellClass = (column: Column, row: any) => [358 `cell-${column.key}`,359 `align-${column.align || 'left'}`360]361362const getRowClass = (row: any, index: number) => [363 'table-row',364 {365 'row-selected': isRowSelected(row),366 'row-even': index % 2 === 0,367 'row-odd': index % 2 === 1368 }369]370371const getSortClass = (columnKey: string) => {372 if (sortColumn.value !== columnKey) return ''373 return `sort-${sortDirection.value}`374}375376const isRowSelected = (row: any): boolean => {377 const rowKey = getRowKey(row, 0)378 return selectedRows.value.some(selected => getRowKey(selected, 0) === rowKey)379}380381const toggleRowSelection = (row: any) => {382 const rowKey = getRowKey(row, 0)383 const index = selectedRows.value.findIndex(selected => getRowKey(selected, 0) === rowKey)384 385 if (index > -1) {386 selectedRows.value.splice(index, 1)387 } else {388 selectedRows.value.push(row)389 }390}391392const toggleSelectAll = () => {393 if (isAllSelected.value) {394 selectedRows.value = []395 } else {396 selectedRows.value = [...props.data]397 }398}399400const handleSort = (column: Column) => {401 if (!column.sortable) return402 403 if (sortColumn.value === column.key) {404 // 切换排序方向405 if (sortDirection.value === 'asc') {406 sortDirection.value = 'desc'407 } else if (sortDirection.value === 'desc') {408 sortDirection.value = null409 sortColumn.value = null410 } else {411 sortDirection.value = 'asc'412 }413 } else {414 sortColumn.value = column.key415 sortDirection.value = 'asc'416 }417 418 emit('sort-change', sortColumn.value, sortDirection.value)419}420421const handleRowClick = (row: any, index: number) => {422 emit('row-click', row, index)423}424425const goToPage = (page: number) => {426 currentPage.value = page427}428429const changePageSize = (size: number) => {430 props.pageSize = size431 currentPage.value = 1432}433434const refresh = () => {435 emit('retry')436}437438// 监听选择变化439watch(selectedRows, (newSelection) => {440 emit('selection-change', newSelection)441}, { deep: true })442</script>443444<!-- 使用示例 -->445<template>446 <div class="user-management">447 <DataTable448 :data="users"449 :columns="columns"450 :loading="loading"451 :error="error"452 selectable453 @row-click="handleRowClick"454 @selection-change="handleSelectionChange"455 @retry="fetchUsers"456 >457 <!-- 操作按钮 -->458 <template #actions="{ selected }">459 <button460 @click="handleBatchDelete"461 :disabled="selected.length === 0"462 class="btn-danger"463 >464 批量删除 ({{ selected.length }})465 </button>466 <button @click="handleAddUser" class="btn-primary">467 添加用户468 </button>469 </template>470 471 <!-- 过滤器 -->472 <template #filters>473 <input474 v-model="searchQuery"475 placeholder="搜索用户..."476 class="search-input"477 />478 <select v-model="roleFilter" class="role-filter">479 <option value="">全部角色</option>480 <option value="admin">管理员</option>481 <option value="user">用户</option>482 </select>483 </template>484 485 <!-- 自定义列渲染 -->486 <template #column-avatar="{ row }">487 <img488 :src="row.avatar || '/default-avatar.png'"489 :alt="row.name"490 class="user-avatar"491 />492 </template>493 494 <template #column-status="{ row }">495 <span496 class="status-badge"497 :class="`status--${row.status}`"498 >499 {{ row.status === 'active' ? '激活' : '禁用' }}500 </span>501 </template>502 503 <!-- 行操作 -->504 <template #actions="{ row }">505 <button @click="editUser(row)" class="btn-sm btn-secondary">506 编辑507 </button>508 <button @click="deleteUser(row)" class="btn-sm btn-danger">509 删除510 </button>511 </template>512 513 <!-- 空状态 -->514 <template #empty>515 <div class="empty-users">516 <h3>暂无用户</h3>517 <p>点击"添加用户"按钮创建第一个用户</p>518 <button @click="handleAddUser" class="btn-primary">519 添加用户520 </button>521 </div>522 </template>523 </DataTable>524 </div>525</template>3. Vue生态系统与工具链
3.1 Vue Router 4路由管理
Vue Router 4为Vue 3应用提供了强大的路由功能,支持嵌套路由、路由守卫、动态路由等特性。
路由配置最佳实践
| 路由特性 | 使用场景 | 配置方式 | 性能影响 |
|---|---|---|---|
| 嵌套路由 | 多层级页面 | children配置 | 中等 |
| 动态路由 | 参数化页面 | :id语法 | 低 |
| 懒加载 | 代码分割 | import()函数 | 优化首屏 |
| 路由守卫 | 权限控制 | beforeEnter等 | 低 |
| 命名路由 | 编程导航 | name属性 | 无 |
- 路由配置
- 导航组件
- Pinia状态管理
完整路由配置示例
Vue Router完整配置
typescript
1// router/index.ts2import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'3import { useUserStore } from '@/stores/user'4import { useNotificationStore } from '@/stores/notification'56// 路由类型定义7declare module 'vue-router' {8 interface RouteMeta {9 title?: string10 requiresAuth?: boolean11 roles?: string[]12 permissions?: string[]13 layout?: string14 keepAlive?: boolean15 showInMenu?: boolean16 icon?: string17 }18}1920// 路由配置21const routes: RouteRecordRaw[] = [22 {23 path: '/',24 name: 'Home',25 component: () => import('@/views/Home.vue'),26 meta: {27 title: '首页',28 showInMenu: true,29 icon: '🏠'30 }31 },32 33 // 认证相关路由34 {35 path: '/auth',36 component: () => import('@/layouts/AuthLayout.vue'),37 children: [38 {39 path: 'login',40 name: 'Login',41 component: () => import('@/views/auth/Login.vue'),42 meta: {43 title: '登录',44 layout: 'auth'45 }46 },47 {48 path: 'register',49 name: 'Register',50 component: () => import('@/views/auth/Register.vue'),51 meta: {52 title: '注册',53 layout: 'auth'54 }55 },56 {57 path: 'forgot-password',58 name: 'ForgotPassword',59 component: () => import('@/views/auth/ForgotPassword.vue'),60 meta: {61 title: '忘记密码',62 layout: 'auth'63 }64 }65 ]66 },67 68 // 用户相关路由69 {70 path: '/user',71 component: () => import('@/layouts/DefaultLayout.vue'),72 meta: {73 requiresAuth: true74 },75 children: [76 {77 path: 'profile',78 name: 'UserProfile',79 component: () => import('@/views/user/Profile.vue'),80 meta: {81 title: '个人资料',82 requiresAuth: true,83 showInMenu: true,84 icon: '👤'85 }86 },87 {88 path: 'settings',89 name: 'UserSettings',90 component: () => import('@/views/user/Settings.vue'),91 meta: {92 title: '用户设置',93 requiresAuth: true,94 showInMenu: true,95 icon: '⚙️'96 }97 },98 {99 path: 'notifications',100 name: 'UserNotifications',101 component: () => import('@/views/user/Notifications.vue'),102 meta: {103 title: '消息通知',104 requiresAuth: true,105 showInMenu: true,106 icon: '🔔'107 }108 }109 ]110 },111 112 // 管理员路由113 {114 path: '/admin',115 component: () => import('@/layouts/AdminLayout.vue'),116 meta: {117 requiresAuth: true,118 roles: ['admin']119 },120 children: [121 {122 path: '',123 name: 'AdminDashboard',124 component: () => import('@/views/admin/Dashboard.vue'),125 meta: {126 title: '管理面板',127 requiresAuth: true,128 roles: ['admin'],129 showInMenu: true,130 icon: '📊'131 }132 },133 {134 path: 'users',135 name: 'AdminUsers',136 component: () => import('@/views/admin/Users.vue'),137 meta: {138 title: '用户管理',139 requiresAuth: true,140 roles: ['admin'],141 permissions: ['users.read'],142 showInMenu: true,143 icon: '👥'144 }145 },146 {147 path: 'users/:id',148 name: 'AdminUserDetail',149 component: () => import('@/views/admin/UserDetail.vue'),150 meta: {151 title: '用户详情',152 requiresAuth: true,153 roles: ['admin'],154 permissions: ['users.read']155 },156 props: true157 },158 {159 path: 'settings',160 name: 'AdminSettings',161 component: () => import('@/views/admin/Settings.vue'),162 meta: {163 title: '系统设置',164 requiresAuth: true,165 roles: ['admin'],166 permissions: ['settings.manage'],167 showInMenu: true,168 icon: '🔧'169 }170 }171 ]172 },173 174 // 动态路由示例175 {176 path: '/posts/:id(\\d+)',177 name: 'PostDetail',178 component: () => import('@/views/posts/PostDetail.vue'),179 meta: {180 title: '文章详情'181 },182 props: route => ({183 id: Number(route.params.id),184 tab: route.query.tab185 })186 },187 188 // 可选参数路由189 {190 path: '/search/:keyword?',191 name: 'Search',192 component: () => import('@/views/Search.vue'),193 meta: {194 title: '搜索'195 }196 },197 198 // 通配符路由199 {200 path: '/docs/:path(.*)*',201 name: 'Documentation',202 component: () => import('@/views/Documentation.vue'),203 meta: {204 title: '文档'205 }206 },207 208 // 重定向209 {210 path: '/dashboard',211 redirect: { name: 'Home' }212 },213 214 // 404页面215 {216 path: '/:pathMatch(.*)*',217 name: 'NotFound',218 component: () => import('@/views/errors/NotFound.vue'),219 meta: {220 title: '页面未找到'221 }222 }223]224225// 创建路由实例226const router = createRouter({227 history: createWebHistory(import.meta.env.BASE_URL),228 routes,229 scrollBehavior(to, from, savedPosition) {230 // 滚动行为231 if (savedPosition) {232 return savedPosition233 } else if (to.hash) {234 return { el: to.hash, behavior: 'smooth' }235 } else {236 return { top: 0 }237 }238 }239})240241// 全局前置守卫242router.beforeEach(async (to, from, next) => {243 const userStore = useUserStore()244 const notificationStore = useNotificationStore()245 246 // 设置页面标题247 if (to.meta.title) {248 document.title = `${to.meta.title} - My App`249 }250 251 // 检查认证要求252 if (to.meta.requiresAuth) {253 if (!userStore.isAuthenticated) {254 notificationStore.showError('请先登录')255 next({256 name: 'Login',257 query: { redirect: to.fullPath }258 })259 return260 }261 262 // 检查角色权限263 if (to.meta.roles && !userStore.hasAnyRole(to.meta.roles)) {264 notificationStore.showError('权限不足')265 next({ name: 'Home' })266 return267 }268 269 // 检查具体权限270 if (to.meta.permissions && !userStore.hasAnyPermission(to.meta.permissions)) {271 notificationStore.showError('权限不足')272 next({ name: 'Home' })273 return274 }275 }276 277 // 已登录用户访问认证页面时重定向278 if (to.path.startsWith('/auth') && userStore.isAuthenticated) {279 next({ name: 'Home' })280 return281 }282 283 next()284})285286// 全局后置钩子287router.afterEach((to, from) => {288 // 页面访问统计289 if (typeof gtag !== 'undefined') {290 gtag('config', 'GA_MEASUREMENT_ID', {291 page_title: to.meta.title,292 page_location: window.location.href293 })294 }295 296 // 清除加载状态297 const loadingStore = useLoadingStore()298 loadingStore.setLoading(false)299})300301// 路由错误处理302router.onError((error) => {303 console.error('Router error:', error)304 const notificationStore = useNotificationStore()305 notificationStore.showError('页面加载失败')306})307308export default router309310// 路由工具函数311export const routeUtils = {312 // 检查当前路由是否匹配313 isCurrentRoute(routeName: string): boolean {314 return router.currentRoute.value.name === routeName315 },316 317 // 检查是否为子路由318 isChildRoute(parentName: string): boolean {319 const current = router.currentRoute.value320 return current.matched.some(route => route.name === parentName)321 },322 323 // 获取面包屑导航324 getBreadcrumbs() {325 const route = router.currentRoute.value326 return route.matched327 .filter(record => record.meta?.title)328 .map(record => ({329 title: record.meta.title,330 name: record.name,331 path: record.path332 }))333 },334 335 // 生成菜单项336 generateMenuItems() {337 const userStore = useUserStore()338 339 const filterRoutes = (routes: RouteRecordRaw[]): any[] => {340 return routes341 .filter(route => {342 // 检查是否显示在菜单中343 if (!route.meta?.showInMenu) return false344 345 // 检查权限346 if (route.meta.requiresAuth && !userStore.isAuthenticated) return false347 if (route.meta.roles && !userStore.hasAnyRole(route.meta.roles)) return false348 if (route.meta.permissions && !userStore.hasAnyPermission(route.meta.permissions)) return false349 350 return true351 })352 .map(route => ({353 name: route.name,354 title: route.meta?.title,355 icon: route.meta?.icon,356 path: route.path,357 children: route.children ? filterRoutes(route.children) : []358 }))359 }360 361 return filterRoutes(routes)362 }363}导航组件实现
导航组件完整实现
vue
1<!-- 主导航组件: MainNavigation.vue -->2<template>3 <nav class="main-navigation">4 <!-- 顶部导航栏 -->5 <div class="nav-header">6 <router-link to="/" class="nav-logo">7 <img src="/logo.svg" alt="Logo" />8 <span>My App</span>9 </router-link>10 11 <!-- 面包屑导航 -->12 <div class="breadcrumb">13 <router-link14 v-for="(crumb, index) in breadcrumbs"15 :key="crumb.name"16 :to="{ name: crumb.name }"17 class="breadcrumb-item"18 :class="{ active: index === breadcrumbs.length - 1 }"19 >20 {{ crumb.title }}21 </router-link>22 </div>23 24 <!-- 用户菜单 -->25 <div class="nav-user">26 <UserDropdown />27 </div>28 </div>29 30 <!-- 侧边导航菜单 -->31 <div class="nav-sidebar">32 <div class="nav-menu">33 <NavMenuItem34 v-for="item in menuItems"35 :key="item.name"36 :item="item"37 :level="0"38 />39 </div>40 </div>41 </nav>42</template>4344<script setup lang="ts">45import { computed } from 'vue'46import { useRoute } from 'vue-router'47import { routeUtils } from '@/router'48import NavMenuItem from './NavMenuItem.vue'49import UserDropdown from './UserDropdown.vue'5051// 当前路由52const route = useRoute()5354// 计算属性55const breadcrumbs = computed(() => routeUtils.getBreadcrumbs())56const menuItems = computed(() => routeUtils.generateMenuItems())57</script>5859<!-- 导航菜单项: NavMenuItem.vue -->60<template>61 <div class="nav-menu-item" :class="itemClasses">62 <!-- 有子菜单的项 -->63 <div64 v-if="item.children && item.children.length > 0"65 class="nav-item-header"66 @click="toggleExpanded"67 >68 <div class="nav-item-content">69 <span class="nav-item-icon">{{ item.icon }}</span>70 <span class="nav-item-title">{{ item.title }}</span>71 </div>72 <span class="nav-item-arrow" :class="{ expanded: isExpanded }">73 ▼74 </span>75 </div>76 77 <!-- 无子菜单的项 -->78 <router-link79 v-else80 :to="{ name: item.name }"81 class="nav-item-link"82 :class="{ active: isActive }"83 >84 <span class="nav-item-icon">{{ item.icon }}</span>85 <span class="nav-item-title">{{ item.title }}</span>86 </router-link>87 88 <!-- 子菜单 -->89 <Transition name="submenu">90 <div v-if="isExpanded && item.children" class="nav-submenu">91 <NavMenuItem92 v-for="child in item.children"93 :key="child.name"94 :item="child"95 :level="level + 1"96 />97 </div>98 </Transition>99 </div>100</template>101102<script setup lang="ts">103import { ref, computed } from 'vue'104import { useRoute } from 'vue-router'105import { routeUtils } from '@/router'106107// Props108interface MenuItem {109 name: string110 title: string111 icon?: string112 path: string113 children?: MenuItem[]114}115116interface Props {117 item: MenuItem118 level: number119}120121const props = defineProps<Props>()122123// 响应式数据124const isExpanded = ref(false)125const route = useRoute()126127// 计算属性128const isActive = computed(() => {129 return routeUtils.isCurrentRoute(props.item.name) ||130 routeUtils.isChildRoute(props.item.name)131})132133const itemClasses = computed(() => [134 `nav-item-level-${props.level}`,135 {136 'nav-item-active': isActive.value,137 'nav-item-expanded': isExpanded.value,138 'nav-item-has-children': props.item.children && props.item.children.length > 0139 }140])141142// 方法143const toggleExpanded = () => {144 isExpanded.value = !isExpanded.value145}146147// 监听路由变化,自动展开激活的菜单148watch(route, () => {149 if (isActive.value && props.item.children) {150 isExpanded.value = true151 }152}, { immediate: true })153</script>154155<!-- 用户下拉菜单: UserDropdown.vue -->156<template>157 <div class="user-dropdown" ref="dropdownRef">158 <button159 @click="toggleDropdown"160 class="user-trigger"161 :class="{ active: isOpen }"162 >163 <img164 :src="user?.avatar || '/default-avatar.png'"165 :alt="user?.name"166 class="user-avatar"167 />168 <span class="user-name">{{ user?.name }}</span>169 <span class="dropdown-arrow">▼</span>170 </button>171 172 <Transition name="dropdown">173 <div v-if="isOpen" class="dropdown-menu">174 <div class="dropdown-header">175 <div class="user-info">176 <img177 :src="user?.avatar || '/default-avatar.png'"178 :alt="user?.name"179 class="user-avatar-large"180 />181 <div class="user-details">182 <div class="user-name">{{ user?.name }}</div>183 <div class="user-email">{{ user?.email }}</div>184 </div>185 </div>186 </div>187 188 <div class="dropdown-body">189 <router-link190 to="/user/profile"191 class="dropdown-item"192 @click="closeDropdown"193 >194 <span class="item-icon">👤</span>195 <span class="item-text">个人资料</span>196 </router-link>197 198 <router-link199 to="/user/settings"200 class="dropdown-item"201 @click="closeDropdown"202 >203 <span class="item-icon">⚙️</span>204 <span class="item-text">设置</span>205 </router-link>206 207 <router-link208 to="/user/notifications"209 class="dropdown-item"210 @click="closeDropdown"211 >212 <span class="item-icon">🔔</span>213 <span class="item-text">通知</span>214 <span v-if="unreadCount > 0" class="notification-badge">215 {{ unreadCount }}216 </span>217 </router-link>218 219 <div class="dropdown-divider"></div>220 221 <button222 @click="handleLogout"223 class="dropdown-item dropdown-item-danger"224 >225 <span class="item-icon">🚪</span>226 <span class="item-text">退出登录</span>227 </button>228 </div>229 </div>230 </Transition>231 </div>232</template>233234<script setup lang="ts">235import { ref, computed, onMounted, onUnmounted } from 'vue'236import { useRouter } from 'vue-router'237import { useUserStore } from '@/stores/user'238import { useNotificationStore } from '@/stores/notification'239240// 响应式数据241const isOpen = ref(false)242const dropdownRef = ref<HTMLElement>()243244// Store245const userStore = useUserStore()246const notificationStore = useNotificationStore()247const router = useRouter()248249// 计算属性250const user = computed(() => userStore.currentUser)251const unreadCount = computed(() => notificationStore.unreadCount)252253// 方法254const toggleDropdown = () => {255 isOpen.value = !isOpen.value256}257258const closeDropdown = () => {259 isOpen.value = false260}261262const handleLogout = async () => {263 try {264 await userStore.logout()265 router.push('/auth/login')266 notificationStore.showSuccess('已成功退出登录')267 } catch (error) {268 notificationStore.showError('退出登录失败')269 } finally {270 closeDropdown()271 }272}273274// 点击外部关闭下拉菜单275const handleClickOutside = (event: MouseEvent) => {276 if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {277 closeDropdown()278 }279}280281onMounted(() => {282 document.addEventListener('click', handleClickOutside)283})284285onUnmounted(() => {286 document.removeEventListener('click', handleClickOutside)287})288</script>289290<style scoped>291/* 导航样式 */292.main-navigation {293 display: flex;294 flex-direction: column;295 height: 100vh;296 background: #fff;297 border-right: 1px solid #e0e0e0;298}299300.nav-header {301 display: flex;302 align-items: center;303 justify-content: space-between;304 padding: 16px 20px;305 border-bottom: 1px solid #e0e0e0;306}307308.nav-logo {309 display: flex;310 align-items: center;311 gap: 8px;312 text-decoration: none;313 color: #333;314 font-weight: 600;315}316317.nav-logo img {318 width: 32px;319 height: 32px;320}321322.breadcrumb {323 display: flex;324 align-items: center;325 gap: 8px;326}327328.breadcrumb-item {329 color: #666;330 text-decoration: none;331 font-size: 14px;332}333334.breadcrumb-item:not(:last-child)::after {335 content: '/';336 margin-left: 8px;337 color: #ccc;338}339340.breadcrumb-item.active {341 color: #333;342 font-weight: 500;343}344345.nav-sidebar {346 flex: 1;347 overflow-y: auto;348}349350.nav-menu {351 padding: 16px 0;352}353354.nav-menu-item {355 margin-bottom: 4px;356}357358.nav-item-header,359.nav-item-link {360 display: flex;361 align-items: center;362 justify-content: space-between;363 padding: 12px 20px;364 color: #666;365 text-decoration: none;366 transition: all 0.2s;367 cursor: pointer;368}369370.nav-item-link:hover,371.nav-item-header:hover {372 background: #f5f5f5;373 color: #333;374}375376.nav-item-link.active {377 background: #e3f2fd;378 color: #1976d2;379 border-right: 3px solid #1976d2;380}381382.nav-item-content {383 display: flex;384 align-items: center;385 gap: 12px;386}387388.nav-item-icon {389 font-size: 16px;390 width: 20px;391 text-align: center;392}393394.nav-item-title {395 font-size: 14px;396}397398.nav-item-arrow {399 font-size: 12px;400 transition: transform 0.2s;401}402403.nav-item-arrow.expanded {404 transform: rotate(180deg);405}406407.nav-submenu {408 background: #f9f9f9;409}410411.nav-item-level-1 .nav-item-link,412.nav-item-level-1 .nav-item-header {413 padding-left: 52px;414}415416/* 下拉菜单样式 */417.user-dropdown {418 position: relative;419}420421.user-trigger {422 display: flex;423 align-items: center;424 gap: 8px;425 padding: 8px 12px;426 background: none;427 border: none;428 border-radius: 6px;429 cursor: pointer;430 transition: background-color 0.2s;431}432433.user-trigger:hover,434.user-trigger.active {435 background: #f5f5f5;436}437438.user-avatar {439 width: 32px;440 height: 32px;441 border-radius: 50%;442 object-fit: cover;443}444445.user-name {446 font-size: 14px;447 color: #333;448}449450.dropdown-arrow {451 font-size: 12px;452 color: #666;453 transition: transform 0.2s;454}455456.user-trigger.active .dropdown-arrow {457 transform: rotate(180deg);458}459460.dropdown-menu {461 position: absolute;462 top: 100%;463 right: 0;464 width: 280px;465 background: #fff;466 border: 1px solid #e0e0e0;467 border-radius: 8px;468 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);469 z-index: 1000;470}471472.dropdown-header {473 padding: 16px;474 border-bottom: 1px solid #e0e0e0;475}476477.user-info {478 display: flex;479 align-items: center;480 gap: 12px;481}482483.user-avatar-large {484 width: 48px;485 height: 48px;486 border-radius: 50%;487 object-fit: cover;488}489490.user-details .user-name {491 font-weight: 500;492 color: #333;493 margin-bottom: 4px;494}495496.user-details .user-email {497 font-size: 12px;498 color: #666;499}500501.dropdown-body {502 padding: 8px 0;503}504505.dropdown-item {506 display: flex;507 align-items: center;508 gap: 12px;509 padding: 12px 16px;510 color: #333;511 text-decoration: none;512 transition: background-color 0.2s;513 border: none;514 background: none;515 width: 100%;516 cursor: pointer;517}518519.dropdown-item:hover {520 background: #f5f5f5;521}522523.dropdown-item-danger {524 color: #d32f2f;525}526527.dropdown-item-danger:hover {528 background: #ffebee;529}530531.item-icon {532 font-size: 16px;533 width: 20px;534 text-align: center;535}536537.item-text {538 flex: 1;539 font-size: 14px;540}541542.notification-badge {543 background: #f44336;544 color: white;545 font-size: 12px;546 padding: 2px 6px;547 border-radius: 10px;548 min-width: 18px;549 text-align: center;550}551552.dropdown-divider {553 height: 1px;554 background: #e0e0e0;555 margin: 8px 0;556}557558/* 动画 */559.submenu-enter-active,560.submenu-leave-active {561 transition: all 0.3s ease;562 overflow: hidden;563}564565.submenu-enter-from,566.submenu-leave-to {567 max-height: 0;568 opacity: 0;569}570571.submenu-enter-to,572.submenu-leave-from {573 max-height: 200px;574 opacity: 1;575}576577.dropdown-enter-active,578.dropdown-leave-active {579 transition: all 0.2s ease;580}581582.dropdown-enter-from,583.dropdown-leave-to {584 opacity: 0;585 transform: translateY(-8px);586}587</style>Pinia状态管理实践
Pinia状态管理完整实现
typescript
1// stores/user.ts2import { defineStore } from 'pinia'3import { ref, computed } from 'vue'4import { useRouter } from 'vue-router'5import { useNotificationStore } from './notification'67// 用户类型定义8interface User {9 id: number10 name: string11 email: string12 avatar?: string13 role: 'admin' | 'user' | 'guest'14 permissions: string[]15 preferences: {16 theme: 'light' | 'dark' | 'auto'17 language: string18 notifications: boolean19 }20 createdAt: string21 lastLoginAt: string22}2324interface LoginCredentials {25 email: string26 password: string27 remember?: boolean28}2930interface RegisterData {31 name: string32 email: string33 password: string34 confirmPassword: string35}3637// 用户Store38export const useUserStore = defineStore('user', () => {39 // 状态40 const user = ref<User | null>(null)41 const loading = ref(false)42 const error = ref<string | null>(null)43 const token = ref<string | null>(localStorage.getItem('token'))44 45 // 计算属性46 const isAuthenticated = computed(() => !!user.value && !!token.value)47 const isAdmin = computed(() => user.value?.role === 'admin')48 const userName = computed(() => user.value?.name || '')49 const userAvatar = computed(() => user.value?.avatar || '/default-avatar.png')50 51 // 权限相关计算属性52 const userPermissions = computed(() => user.value?.permissions || [])53 const userRole = computed(() => user.value?.role || 'guest')54 55 // Actions56 const login = async (credentials: LoginCredentials) => {57 try {58 loading.value = true59 error.value = null60 61 const response = await fetch('/api/auth/login', {62 method: 'POST',63 headers: {64 'Content-Type': 'application/json'65 },66 body: JSON.stringify(credentials)67 })68 69 if (!response.ok) {70 const errorData = await response.json()71 throw new Error(errorData.message || '登录失败')72 }73 74 const { user: userData, token: authToken } = await response.json()75 76 // 保存用户信息和token77 user.value = userData78 token.value = authToken79 80 // 根据remember选项决定存储方式81 if (credentials.remember) {82 localStorage.setItem('token', authToken)83 localStorage.setItem('user', JSON.stringify(userData))84 } else {85 sessionStorage.setItem('token', authToken)86 sessionStorage.setItem('user', JSON.stringify(userData))87 }88 89 // 设置axios默认header90 setAuthHeader(authToken)91 92 return userData93 } catch (err) {94 error.value = err instanceof Error ? err.message : '登录失败'95 throw err96 } finally {97 loading.value = false98 }99 }100 101 const register = async (data: RegisterData) => {102 try {103 loading.value = true104 error.value = null105 106 const response = await fetch('/api/auth/register', {107 method: 'POST',108 headers: {109 'Content-Type': 'application/json'110 },111 body: JSON.stringify(data)112 })113 114 if (!response.ok) {115 const errorData = await response.json()116 throw new Error(errorData.message || '注册失败')117 }118 119 const { user: userData, token: authToken } = await response.json()120 121 user.value = userData122 token.value = authToken123 124 localStorage.setItem('token', authToken)125 localStorage.setItem('user', JSON.stringify(userData))126 127 setAuthHeader(authToken)128 129 return userData130 } catch (err) {131 error.value = err instanceof Error ? err.message : '注册失败'132 throw err133 } finally {134 loading.value = false135 }136 }137 138 const logout = async () => {139 try {140 // 调用后端登出接口141 if (token.value) {142 await fetch('/api/auth/logout', {143 method: 'POST',144 headers: {145 'Authorization': `Bearer ${token.value}`146 }147 })148 }149 } catch (err) {150 console.error('Logout API error:', err)151 } finally {152 // 清除本地状态153 user.value = null154 token.value = null155 error.value = null156 157 // 清除存储158 localStorage.removeItem('token')159 localStorage.removeItem('user')160 sessionStorage.removeItem('token')161 sessionStorage.removeItem('user')162 163 // 清除axios header164 setAuthHeader(null)165 }166 }167 168 const updateProfile = async (updates: Partial<User>) => {169 if (!user.value) throw new Error('用户未登录')170 171 try {172 loading.value = true173 174 const response = await fetch(`/api/users/${user.value.id}`, {175 method: 'PATCH',176 headers: {177 'Content-Type': 'application/json',178 'Authorization': `Bearer ${token.value}`179 },180 body: JSON.stringify(updates)181 })182 183 if (!response.ok) {184 throw new Error('更新失败')185 }186 187 const updatedUser = await response.json()188 user.value = updatedUser189 190 // 更新本地存储191 const storage = localStorage.getItem('token') ? localStorage : sessionStorage192 storage.setItem('user', JSON.stringify(updatedUser))193 194 return updatedUser195 } catch (err) {196 error.value = err instanceof Error ? err.message : '更新失败'197 throw err198 } finally {199 loading.value = false200 }201 }202 203 const changePassword = async (oldPassword: string, newPassword: string) => {204 try {205 loading.value = true206 207 const response = await fetch('/api/auth/change-password', {208 method: 'POST',209 headers: {210 'Content-Type': 'application/json',211 'Authorization': `Bearer ${token.value}`212 },213 body: JSON.stringify({ oldPassword, newPassword })214 })215 216 if (!response.ok) {217 const errorData = await response.json()218 throw new Error(errorData.message || '密码修改失败')219 }220 221 return true222 } catch (err) {223 error.value = err instanceof Error ? err.message : '密码修改失败'224 throw err225 } finally {226 loading.value = false227 }228 }229 230 const refreshToken = async () => {231 try {232 const response = await fetch('/api/auth/refresh', {233 method: 'POST',234 headers: {235 'Authorization': `Bearer ${token.value}`236 }237 })238 239 if (!response.ok) {240 throw new Error('Token刷新失败')241 }242 243 const { token: newToken } = await response.json()244 token.value = newToken245 246 const storage = localStorage.getItem('token') ? localStorage : sessionStorage247 storage.setItem('token', newToken)248 249 setAuthHeader(newToken)250 251 return newToken252 } catch (err) {253 // Token刷新失败,执行登出254 await logout()255 throw err256 }257 }258 259 const checkAuthStatus = async () => {260 const storedToken = localStorage.getItem('token') || sessionStorage.getItem('token')261 const storedUser = localStorage.getItem('user') || sessionStorage.getItem('user')262 263 if (storedToken && storedUser) {264 try {265 token.value = storedToken266 user.value = JSON.parse(storedUser)267 setAuthHeader(storedToken)268 269 // 验证token有效性270 const response = await fetch('/api/auth/me', {271 headers: {272 'Authorization': `Bearer ${storedToken}`273 }274 })275 276 if (response.ok) {277 const userData = await response.json()278 user.value = userData279 280 // 更新存储的用户信息281 const storage = localStorage.getItem('token') ? localStorage : sessionStorage282 storage.setItem('user', JSON.stringify(userData))283 } else {284 // Token无效,清除状态285 await logout()286 }287 } catch (err) {288 console.error('Auth check failed:', err)289 await logout()290 }291 }292 }293 294 // 权限检查方法295 const hasPermission = (permission: string): boolean => {296 return userPermissions.value.includes(permission)297 }298 299 const hasAnyPermission = (permissions: string[]): boolean => {300 return permissions.some(permission => hasPermission(permission))301 }302 303 const hasAllPermissions = (permissions: string[]): boolean => {304 return permissions.every(permission => hasPermission(permission))305 }306 307 const hasRole = (role: string): boolean => {308 return userRole.value === role309 }310 311 const hasAnyRole = (roles: string[]): boolean => {312 return roles.includes(userRole.value)313 }314 315 // 工具方法316 const setAuthHeader = (authToken: string | null) => {317 if (authToken) {318 // 设置axios默认header(如果使用axios)319 // axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}`320 } else {321 // 清除axios默认header322 // delete axios.defaults.headers.common['Authorization']323 }324 }325 326 const clearError = () => {327 error.value = null328 }329 330 // 返回store接口331 return {332 // 状态333 user: readonly(user),334 loading: readonly(loading),335 error: readonly(error),336 token: readonly(token),337 338 // 计算属性339 isAuthenticated,340 isAdmin,341 userName,342 userAvatar,343 userPermissions,344 userRole,345 346 // Actions347 login,348 register,349 logout,350 updateProfile,351 changePassword,352 refreshToken,353 checkAuthStatus,354 355 // 权限方法356 hasPermission,357 hasAnyPermission,358 hasAllPermissions,359 hasRole,360 hasAnyRole,361 362 // 工具方法363 clearError364 }365})366367// stores/notification.ts368export const useNotificationStore = defineStore('notification', () => {369 // 通知类型370 type NotificationType = 'success' | 'error' | 'warning' | 'info'371 372 interface Notification {373 id: string374 type: NotificationType375 title: string376 message?: string377 duration?: number378 persistent?: boolean379 actions?: Array<{380 label: string381 action: () => void382 }>383 }384 385 // 状态386 const notifications = ref<Notification[]>([])387 const unreadCount = ref(0)388 389 // Actions390 const addNotification = (notification: Omit<Notification, 'id'>) => {391 const id = Date.now().toString() + Math.random().toString(36).substr(2, 9)392 const newNotification: Notification = {393 id,394 duration: 5000,395 persistent: false,396 ...notification397 }398 399 notifications.value.push(newNotification)400 401 // 自动移除非持久化通知402 if (!newNotification.persistent && newNotification.duration && newNotification.duration > 0) {403 setTimeout(() => {404 removeNotification(id)405 }, newNotification.duration)406 }407 408 return id409 }410 411 const removeNotification = (id: string) => {412 const index = notifications.value.findIndex(n => n.id === id)413 if (index > -1) {414 notifications.value.splice(index, 1)415 }416 }417 418 const clearAllNotifications = () => {419 notifications.value = []420 }421 422 // 便捷方法423 const showSuccess = (title: string, message?: string, options?: Partial<Notification>) => {424 return addNotification({ type: 'success', title, message, ...options })425 }426 427 const showError = (title: string, message?: string, options?: Partial<Notification>) => {428 return addNotification({ type: 'error', title, message, ...options })429 }430 431 const showWarning = (title: string, message?: string, options?: Partial<Notification>) => {432 return addNotification({ type: 'warning', title, message, ...options })433 }434 435 const showInfo = (title: string, message?: string, options?: Partial<Notification>) => {436 return addNotification({ type: 'info', title, message, ...options })437 }438 439 return {440 notifications: readonly(notifications),441 unreadCount: readonly(unreadCount),442 addNotification,443 removeNotification,444 clearAllNotifications,445 showSuccess,446 showError,447 showWarning,448 showInfo449 }450})451452// stores/app.ts - 应用全局状态453export const useAppStore = defineStore('app', () => {454 // 状态455 const loading = ref(false)456 const sidebarCollapsed = ref(false)457 const theme = ref<'light' | 'dark' | 'auto'>('light')458 const language = ref('zh-CN')459 460 // 计算属性461 const isDarkMode = computed(() => {462 if (theme.value === 'auto') {463 return window.matchMedia('(prefers-color-scheme: dark)').matches464 }465 return theme.value === 'dark'466 })467 468 // Actions469 const setLoading = (value: boolean) => {470 loading.value = value471 }472 473 const toggleSidebar = () => {474 sidebarCollapsed.value = !sidebarCollapsed.value475 }476 477 const setTheme = (newTheme: 'light' | 'dark' | 'auto') => {478 theme.value = newTheme479 localStorage.setItem('theme', newTheme)480 481 // 应用主题到DOM482 document.documentElement.setAttribute('data-theme', newTheme)483 }484 485 const setLanguage = (lang: string) => {486 language.value = lang487 localStorage.setItem('language', lang)488 }489 490 const initializeApp = () => {491 // 恢复主题设置492 const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | 'auto'493 if (savedTheme) {494 setTheme(savedTheme)495 }496 497 // 恢复语言设置498 const savedLanguage = localStorage.getItem('language')499 if (savedLanguage) {500 setLanguage(savedLanguage)501 }502 503 // 监听系统主题变化504 if (theme.value === 'auto') {505 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')506 mediaQuery.addEventListener('change', () => {507 document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'dark' : 'light')508 })509 }510 }511 512 return {513 loading: readonly(loading),514 sidebarCollapsed: readonly(sidebarCollapsed),515 theme: readonly(theme),516 language: readonly(language),517 isDarkMode,518 setLoading,519 toggleSidebar,520 setTheme,521 setLanguage,522 initializeApp523 }524})4. Vue性能优化与最佳实践
4.1 性能优化策略
Vue 3在性能方面有了显著提升,但仍需要开发者采用正确的优化策略来构建高性能应用。
性能优化检查清单
| 优化类型 | 技术方案 | 适用场景 | 性能提升 | 实现难度 |
|---|---|---|---|---|
| 组件缓存 | KeepAlive | 频繁切换的组件 | ⭐⭐⭐⭐ | ⭐⭐ |
| 计算属性 | computed() | 复杂计算逻辑 | ⭐⭐⭐⭐⭐ | ⭐ |
| 虚拟滚动 | 自定义实现 | 大列表渲染 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| 异步组件 | defineAsyncComponent | 代码分割 | ⭐⭐⭐⭐ | ⭐⭐ |
| 响应式优化 | shallowRef/shallowReactive | 大对象处理 | ⭐⭐⭐ | ⭐⭐⭐ |
Vue性能优化原则
- 测量优先:使用Vue DevTools分析性能瓶颈
- 渐进优化:从影响最大的优化开始
- 避免过早优化:在确认性能问题后再优化
- 用户体验导向:关注用户感知的性能指标
- 持续监控:建立性能监控和告警机制
Vue.js作为现代前端开发的重要框架,其组合式API、响应式系统和丰富的生态系统为开发者提供了强大的工具。通过掌握Vue 3的核心特性、组件化开发模式和性能优化技巧,可以构建出高质量、高性能的现代Web应用。
评论