Vue3 项目实战
概述
本文档将介绍 Vue3 项目开发中的实战经验,包括项目结构设计、开发规范、最佳实践、常见问题解决方案等。通过实际项目案例,帮助开发者更好地应用 Vue3 技术。
项目结构设计
1. 标准项目结构
src/
├── assets/ # 静态资源
│ ├── images/ # 图片资源
│ ├── styles/ # 样式文件
│ └── icons/ # 图标文件
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ ├── business/ # 业务组件
│ └── layout/ # 布局组件
├── views/ # 页面组件
│ ├── home/ # 首页
│ ├── user/ # 用户相关
│ └── admin/ # 管理后台
├── router/ # 路由配置
│ ├── index.js # 主路由
│ ├── modules/ # 路由模块
│ └── guards.js # 路由守卫
├── stores/ # 状态管理
│ ├── modules/ # 状态模块
│ └── index.js # 状态入口
├── api/ # API 接口
│ ├── modules/ # 接口模块
│ ├── request.js # 请求封装
│ └── types.ts # 类型定义
├── utils/ # 工具函数
│ ├── auth.js # 认证相关
│ ├── storage.js # 存储工具
│ └── validate.js # 验证工具
├── hooks/ # 组合式函数
│ ├── useAuth.js # 认证钩子
│ ├── useTable.js # 表格钩子
│ └── useForm.js # 表单钩子
├── directives/ # 自定义指令
├── plugins/ # 插件配置
└── App.vue # 根组件
2. 组件命名规范
// 组件文件命名:PascalCase
// UserProfile.vue, DataTable.vue
// 组件注册命名:PascalCase
export default {
name: 'UserProfile',
// ...
}
// 组件使用:kebab-case
<user-profile />
<data-table />
开发规范与最佳实践
1. 组合式 API 使用规范
响应式数据定义
import { ref, reactive, computed, watch } from "vue";
export default {
setup() {
// 基础类型使用 ref
const count = ref(0);
const loading = ref(false);
const message = ref("");
// 对象类型使用 reactive
const formData = reactive({
name: "",
email: "",
phone: "",
});
// 计算属性
const isValid = computed(() => {
return formData.name && formData.email;
});
// 监听器
watch(
formData,
(newVal, oldVal) => {
console.log("表单数据变化:", newVal);
},
{ deep: true }
);
return {
count,
loading,
message,
formData,
isValid,
};
},
};
逻辑复用 - 组合式函数
// hooks/useTable.js
import { ref, computed } from "vue";
export function useTable(api, options = {}) {
const loading = ref(false);
const data = ref([]);
const pagination = ref({
current: 1,
pageSize: 10,
total: 0,
});
const fetchData = async () => {
loading.value = true;
try {
const response = await api({
page: pagination.value.current,
pageSize: pagination.value.pageSize,
...options.params,
});
data.value = response.data;
pagination.value.total = response.total;
} catch (error) {
console.error("获取数据失败:", error);
} finally {
loading.value = false;
}
};
const handlePageChange = (page) => {
pagination.value.current = page;
fetchData();
};
return {
loading,
data,
pagination,
fetchData,
handlePageChange,
};
}
// 使用示例
export default {
setup() {
const { loading, data, pagination, fetchData, handlePageChange } = useTable(
userApi.getList,
{ params: { status: "active" } }
);
onMounted(() => {
fetchData();
});
return {
loading,
data,
pagination,
handlePageChange,
};
},
};
2. 组件通信最佳实践
Props 和 Emit
// 子组件
export default {
props: {
title: {
type: String,
required: true
},
items: {
type: Array,
default: () => []
}
},
emits: ['update', 'delete'],
setup(props, { emit }) {
const handleUpdate = (item) => {
emit('update', item)
}
const handleDelete = (id) => {
emit('delete', id)
}
return {
handleUpdate,
handleDelete
}
}
}
// 父组件
<template>
<data-table
:title="tableTitle"
:items="tableData"
@update="handleUpdate"
@delete="handleDelete"
/>
</template>
Provide/Inject 跨层级通信
// 祖先组件
import { provide, ref } from 'vue'
export default {
setup() {
const theme = ref('light')
const user = ref(null)
provide('theme', theme)
provide('user', user)
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
return {
theme,
user
}
}
}
// 后代组件
import { inject } from 'vue'
export default {
setup() {
const theme = inject('theme')
const user = inject('user')
const updateTheme = inject('updateTheme')
return {
theme,
user,
updateTheme
}
}
}
3. 路由管理最佳实践
路由配置
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const routes = [
{
path: "/",
name: "Home",
component: () => import("@/views/Home.vue"),
meta: { requiresAuth: false },
},
{
path: "/dashboard",
name: "Dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { requiresAuth: true, roles: ["admin", "user"] },
children: [
{
path: "profile",
name: "Profile",
component: () => import("@/views/Profile.vue"),
},
],
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 路由守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next("/login");
} else if (to.meta.roles && !to.meta.roles.includes(authStore.userRole)) {
next("/403");
} else {
next();
}
});
export default router;
动态路由
// 动态添加路由
export function addDynamicRoutes(menus) {
const dynamicRoutes = menus.map((menu) => ({
path: menu.path,
name: menu.name,
component: () => import(`@/views/${menu.component}.vue`),
meta: menu.meta,
}));
dynamicRoutes.forEach((route) => {
router.addRoute(route);
});
}
4. 状态管理最佳实践
Pinia Store 设计
// stores/user.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { userApi } from "@/api";
export const useUserStore = defineStore("user", () => {
// 状态
const user = ref(null);
const token = ref("");
const permissions = ref([]);
// 计算属性
const isAuthenticated = computed(() => !!token.value);
const userRole = computed(() => user.value?.role || "guest");
const hasPermission = computed(() => (permission) => {
return permissions.value.includes(permission);
});
// 方法
const login = async (credentials) => {
try {
const response = await userApi.login(credentials);
user.value = response.user;
token.value = response.token;
permissions.value = response.permissions;
// 持久化存储
localStorage.setItem("token", response.token);
localStorage.setItem("user", JSON.stringify(response.user));
return response;
} catch (error) {
throw error;
}
};
const logout = () => {
user.value = null;
token.value = "";
permissions.value = [];
localStorage.removeItem("token");
localStorage.removeItem("user");
};
const updateProfile = async (profileData) => {
try {
const response = await userApi.updateProfile(profileData);
user.value = { ...user.value, ...response };
return response;
} catch (error) {
throw error;
}
};
return {
user,
token,
permissions,
isAuthenticated,
userRole,
hasPermission,
login,
logout,
updateProfile,
};
});
性能优化实践
1. 组件懒加载
// 路由懒加载
const routes = [
{
path: "/dashboard",
component: () => import("@/views/Dashboard.vue"),
},
];
// 组件懒加载
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() =>
import("@/components/HeavyComponent.vue")
);
2. 虚拟滚动
// 使用虚拟滚动处理大量数据
import { VirtualList } from 'vue-virtual-scroll-list'
export default {
components: {
VirtualList
},
setup() {
const items = ref(Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` })))
return {
items
}
}
}
// 模板
<virtual-list
:data-key="'id'"
:data-sources="items"
:data-component="ItemComponent"
:estimate-size="50"
/>
3. 图片懒加载
// 自定义指令
const lazyLoad = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
}
}
// 使用
<img v-lazy="imageUrl" alt="lazy image" />
常见问题与解决方案
1. 响应式数据丢失
// 问题:解构响应式对象会丢失响应性
const user = reactive({ name: "John", age: 30 });
const { name, age } = user; // 丢失响应性
// 解决方案1:使用 toRefs
import { toRefs } from "vue";
const { name, age } = toRefs(user); // 保持响应性
// 解决方案2:使用 computed
const name = computed(() => user.name);
const age = computed(() => user.age);
2. 内存泄漏
// 问题:事件监听器未清理
export default {
setup() {
const handleResize = () => {
console.log("window resized");
};
window.addEventListener("resize", handleResize);
// 解决方案:在 onUnmounted 中清理
onUnmounted(() => {
window.removeEventListener("resize", handleResize);
});
},
};
3. 异步组件加载失败
// 问题:异步组件加载失败时的处理
const AsyncComponent = defineAsyncComponent({
loader: () => import("@/components/HeavyComponent.vue"),
loadingComponent: LoadingSpinner,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry();
} else {
fail();
}
},
});
测试策略
1. 单元测试
// 使用 Vitest 进行单元测试
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import { createPinia } from "pinia";
import UserProfile from "@/components/UserProfile.vue";
describe("UserProfile", () => {
it("renders user information correctly", () => {
const wrapper = mount(UserProfile, {
props: {
user: {
name: "John Doe",
email: "john@example.com",
},
},
global: {
plugins: [createPinia()],
},
});
expect(wrapper.text()).toContain("John Doe");
expect(wrapper.text()).toContain("john@example.com");
});
});
2. 集成测试
// 测试组件与 Store 的集成
import { createTestingPinia } from "@pinia/testing";
const wrapper = mount(Component, {
global: {
plugins: [
createTestingPinia({
initialState: {
user: { user: { name: "Test User" } },
},
}),
],
},
});
部署与构建
1. 环境配置
// .env.development
VITE_API_BASE_URL=http://localhost:3000
VITE_APP_TITLE=Vue3 App (Dev)
// .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=Vue3 App
2. 构建优化
// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ["vue", "vue-router", "pinia"],
utils: ["lodash-es", "dayjs"],
},
},
},
chunkSizeWarningLimit: 1000,
},
});
项目监控与错误处理
1. 全局错误处理
// 全局错误处理器
app.config.errorHandler = (err, vm, info) => {
console.error("全局错误:", err);
console.error("错误信息:", info);
// 发送错误到监控服务
errorTracker.captureException(err, {
extra: { info, component: vm?.$options?.name },
});
};
// 未捕获的 Promise 错误
window.addEventListener("unhandledrejection", (event) => {
console.error("未处理的 Promise 错误:", event.reason);
event.preventDefault();
});
2. 性能监控
// 性能监控
import { onMounted } from "vue";
export default {
setup() {
onMounted(() => {
// 监控页面加载性能
if ("performance" in window) {
const perfData = performance.getEntriesByType("navigation")[0];
console.log(
"页面加载时间:",
perfData.loadEventEnd - perfData.loadEventStart
);
}
});
},
};
总结
Vue3 项目实战需要综合考虑项目结构、开发规范、性能优化、测试策略等多个方面。通过合理的架构设计和最佳实践,可以构建出高质量、可维护的 Vue3 应用。
关键要点:
- 合理规划项目结构,遵循约定优于配置的原则
- 充分利用组合式 API,提高代码复用性和可维护性
- 注重性能优化,从组件设计到构建配置都要考虑性能
- 完善的测试策略,确保代码质量和稳定性
- 监控和错误处理,及时发现和解决问题
通过不断实践和总结,你将能够掌握 Vue3 项目开发的核心技能,构建出优秀的前端应用。
