一、进阶必要性:基础Vue为何无法应对复杂项目?
第八篇我们掌握了Vue基础语法、组件通信和接口对接,能实现简单的单页面应用。但职场中,项目往往具备"规模大、模块多、数据交互复杂"的特点,基础Vue会暴露3个核心问题:① 组件复用效率低 :相同逻辑(如表单验证、数据请求)在多个组件中重复编写,维护成本高;② 跨组件通信繁琐 :当需要"爷爷-孙子""兄弟组件"通信时,用父子通信层层传递会导致代码冗余且难以维护;③ 无页面路由管理:单页面应用无法实现"不同URL对应不同页面"的跳转和状态保存。
职场解决方案:掌握组件复用技巧(Mixin/组合式函数) 、全局状态管理(Pinia) 、路由管理(Vue Router)------这三大技能是Vue进阶的核心,也是区分"初级前端"和"中级前端"的关键标志。
职场数据:据2024年Vue开发者薪资调研,掌握Pinia和Vue Router的开发者薪资比仅会Vue基础的高20%-30%,且复杂项目(如管理系统、电商平台)招聘要求中,这两项技能为"强制要求"。
二、Day25:组件复用进阶------从"重复编写"到"一次封装,多处复用"
基础开发中,多个组件若需要相同的逻辑(如加载状态管理、表单验证、接口请求),会重复编写代码。Vue提供了两种核心复用方案:Mixin(混入) 和组合式函数(Composition Function),其中组合式函数是Vue 3推荐方案,更符合组合式API思想。
1. 方案1:Mixin(混入)------简单场景的快速复用
Mixin是将多个组件的公共逻辑提取到一个独立对象中,组件通过"混入"该对象,即可拥有其数据、方法和生命周期钩子。适合简单的逻辑复用,但在复杂场景下会存在"命名冲突""逻辑来源不清晰"等问题。
实战1:用Mixin封装"加载状态管理"逻辑
提取多个组件共用的"加载状态(loading)"管理逻辑,实现"显示加载→请求数据→隐藏加载"的统一流程:
(1)创建Mixin文件(src/mixins/loadingMixin.js)
// 加载状态管理Mixin export default { // 混入的数据(会合并到组件的data中) data() { return { loading: false, // 加载状态 errorMsg: '' // 错误提示 }; }, // 混入的方法(会合并到组件的方法中) methods: { // 显示加载状态 showLoading() { this.loading = true; this.errorMsg = ''; }, // 隐藏加载状态 hideLoading() { this.loading = false; }, // 处理请求错误 handleError(err) { this.errorMsg = err.message || '操作失败,请重试'; console.error('请求错误:', err); } } };
(2)组件中使用Mixin(复用逻辑)
修改第八篇的`src/views/UserManage.vue`,通过Mixin复用加载状态逻辑,替代手动定义的loading和相关方法:
<template> <div class="user-manage" style="max-width: 800px; margin: 0 auto; padding: 20px;"> <h2 style="text-align: center; margin-bottom: 30px;">用户管理系统(Mixin复用版)</h2> <div v-if="errorMsg" style="padding: 10px; background: #fef2f2; color: #b42323; border-radius: 4px; margin-bottom: 20px;"> {``{ errorMsg }} </div> <form @submit.prevent="handleSubmit" style="border: 1px solid #eee; padding: 20px; border-radius: 8px; margin-bottom: 30px;"> <button type="submit" :disabled="loading" style="padding: 8px 16px; border: none; border-radius: 4px; background: #42b983; color: #fff; cursor: pointer; font-size: 14px;"> {``{ loading ? '提交中...' : '新增用户' }} </button> </form> <div v-if="loading" style="text-align: center; padding: 50px 0;">加载中...</div> <UserList v-else :users="users" @toggle-active="toggleActive" @delete-user="deleteUser" /> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import UserList from '../components/UserList.vue'; import { addUser, getUserList as getApiUserList, deleteUser as deleteApiUser } from '../api/userApi.js'; // 1. 引入Mixin import loadingMixin from '../mixins/loadingMixin.js'; // 2. 应用Mixin(Vue 3 setup语法中需用useAttrs配合,或改用Options API) // 注:setup语法中Mixin支持有限,推荐用组合式函数,此处为演示Mixin用法 const { loading, errorMsg, showLoading, hideLoading, handleError } = loadingMixin.data().loading ? loadingMixin.methods : {}; const formData = ref({ name: '', phone: '', active: true }); const users = ref([]); // 3. 复用Mixin的加载逻辑 const getUserList = async () => { showLoading(); try { const res = await getApiUserList(); users.value = res.data; } catch (err) { handleError(err); } finally { hideLoading(); } }; const handleSubmit = async () => { if (!formData.value.name || !formData.value.phone) { handleError(new Error('姓名和手机号不能为空')); return; } if (!/^1[3-9]\d{9}$/.test(formData.value.phone)) { handleError(new Error('请输入有效的手机号')); return; } showLoading(); try { await addUser(formData.value); alert('新增成功!'); formData.value = { name: '', phone: '', active: true }; getUserList(); } catch (err) { handleError(err); } finally { hideLoading(); } }; // 删除用户(复用加载逻辑) const deleteUser = async (userId) => { if (!confirm('确定删除该用户?')) return; showLoading(); try { await deleteApiUser(userId); alert('删除成功!'); getUserList(); } catch (err) { handleError(err); } finally { hideLoading(); } }; onMounted(getUserList); </script>
Mixin的局限性(职场避坑指南)
-
命名冲突:若组件和Mixin有同名数据(如都定义了loading),会发生覆盖(组件数据优先级高于Mixin),难以排查;
-
逻辑来源模糊:组件中使用的方法/数据可能来自Mixin,后期维护时难以追溯其来源;
-
组合式API适配差:Vue 3的setup语法中,Mixin的支持有限,无法充分利用组合式API的优势。
2. 方案2:组合式函数(Composition Function)------Vue 3推荐的复用方案
组合式函数是将公共逻辑封装成一个"函数",组件通过调用函数获取逻辑相关的数据和方法,能明确逻辑来源,避免命名冲突,完美适配Vue 3组合式API,是职场复杂项目的首选复用方案。
实战2:用组合式函数重构"加载状态+请求"逻辑
将"加载状态管理+接口请求"逻辑封装成组合式函数,解决Mixin的局限性:
(1)创建组合式函数(src/composables/useRequest.js)
// 导入Vue响应式API import { ref } from 'vue'; /** * 组合式函数:封装请求逻辑+加载状态管理 * @param {Function} requestFn - 接口请求函数(Promise) * @returns {Object} - { data: 响应数据, loading: 加载状态, errorMsg: 错误信息, execute: 执行请求的方法 } */ export function useRequest(requestFn) { // 响应数据(泛型,适配不同接口返回格式) const data = ref(null); // 加载状态 const loading = ref(false); // 错误信息 const errorMsg = ref(''); // 执行请求的方法 const execute = async (...args) => { loading.value = true; errorMsg.value = ''; try { // 调用传入的请求函数,传递参数 const result = await requestFn(...args); data.value = result.data; // 假设接口返回格式为{ code: 200, data: ... } return result; // 返回完整响应,供组件自定义处理 } catch (err) { errorMsg.value = err.message || '请求失败,请重试'; console.error('请求错误:', err); throw err; // 抛出错误,供组件捕获 } finally { loading.value = false; } }; // 返回组件需要的数据和方法 return { data, loading, errorMsg, execute }; }
(2)组件中使用组合式函数(清晰复用)
修改`src/views/UserManage.vue`,用`useRequest`替代Mixin,逻辑来源更清晰:
<template> <div class="user-manage" style="max-width: 800px; margin: 0 auto; padding: 20px;"> <h2 style="text-align: center; margin-bottom: 30px;">用户管理系统(组合式函数版)</h2> <div v-if="userList.errorMsg" style="padding: 10px; background: #fef2f2; color: #b42323; border-radius: 4px; margin-bottom: 20px;"> {``{ userList.errorMsg }} </div> <form @submit.prevent="handleSubmit" style="border: 1px solid #eee; padding: 20px; border-radius: 8px; margin-bottom: 30px;"> <button type="submit" :disabled="addUser.loading" style="padding: 8px 16px; border: none; border-radius: 4px; background: #42b983; color: #fff; cursor: pointer; font-size: 14px;"> {``{ addUser.loading ? '提交中...' : '新增用户' }} </button> </form> <div v-if="userList.loading" style="text-align: center; padding: 50px 0;">加载中...</div> <UserList v-else :users="userList.data || []" @toggle-active="toggleActive" @delete-user="handleDelete" /> </div> </template> <script setup> import { ref, onMounted } from 'vue'; import UserList from '../components/UserList.vue'; import { addUser, getUserList, deleteUser } from '../api/userApi.js'; // 1. 引入组合式函数 import { useRequest } from '../composables/useRequest.js'; // 2. 调用组合式函数,封装不同接口的逻辑 // 封装"获取用户列表"请求 const userList = useRequest(getUserList); // 封装"新增用户"请求 const addUserReq = useRequest(addUser); // 封装"删除用户"请求 const deleteUserReq = useRequest(deleteUser); // 表单数据 const formData = ref({ name: '', phone: '', active: true }); // 3. 表单提交(使用封装的新增请求) const handleSubmit = async () => { if (!formData.value.name || !formData.value.phone) { alert('姓名和手机号不能为空'); return; } if (!/^1[3-9]\d{9}$/.test(formData.value.phone)) { alert('请输入有效的手机号'); return; } try { // 调用组合式函数返回的execute方法,传递表单数据 await addUserReq.execute(formData.value); alert('新增用户成功!'); formData.value = { name: '', phone: '', active: true }; // 重新获取用户列表 userList.execute(); } catch (err) { // 错误已在useRequest中处理,此处可自定义额外逻辑 } }; // 切换用户状态(使用封装的更新请求,假设已有updateUser接口) const toggleActive = async (userId, currentActive) => { try { await useRequest((id) => updateUser(id, { active: !currentActive })).execute(userId); userList.execute(); } catch (err) {} }; // 删除用户(使用封装的删除请求) const handleDelete = async (userId) => { if (!confirm('确定删除?')) return; try { await deleteUserReq.execute(userId); alert('删除成功!'); userList.execute(); } catch (err) {} }; // 页面挂载时执行请求 onMounted(() => { userList.execute(); }); </script>
组合式函数的职场优势
-
逻辑来源清晰:组件中明确调用`useRequest`获取数据和方法,可直接追溯逻辑来源;
-
无命名冲突:多个组合式函数可返回同名数据(如都有loading),组件通过变量名区分(如`userList.loading`、`addUser.loading`);
-
高灵活性:可向函数传递参数(如请求函数、默认值),适配不同场景的复用需求;
-
完美适配Vue 3:充分利用组合式API的响应式能力,支持在setup语法中无缝使用。
三、Day26:全局状态管理------Pinia(Vue官方推荐,替代Vuex)
当项目中多个组件(如"用户列表""用户详情""导航栏")需要共享同一数据(如"当前登录用户信息""购物车数据")时,父子通信或组合式函数会显得繁琐。此时需要全局状态管理工具,Pinia是Vue 3官方推荐的工具,替代了传统的Vuex,具有"语法简洁、无模块嵌套、支持TypeScript"等优势,是当前职场主流方案。
1. 核心概念:Pinia为何能替代Vuex?
| 对比维度 | Vuex(传统方案) | Pinia(Vue 3推荐) |
|---|---|---|
| 语法复杂度 | 需区分state、mutations、actions、getters,语法繁琐 | 仅需定义state、actions、getters,无mutations,语法简洁 |
| 模块管理 | 支持模块嵌套,但深层嵌套会导致代码冗余 | 无模块嵌套,通过创建多个Store实现模块拆分,更灵活 |
| TypeScript支持 | 支持有限,需手动编写大量类型定义 | 原生支持TypeScript,类型推导清晰,开发体验好 |
| Vue 3适配 | 需配合Vuex 4版本,适配性一般 | 专为Vue 3设计,完美适配组合式API |
2. 实战3:用Pinia实现"用户状态"全局管理
需求:实现"用户登录/退出"功能,全局共享"当前登录用户信息",让导航栏、用户列表等组件都能访问和修改该状态。
(1)第一步:安装并初始化Pinia
# 1. 安装Pinia(Vue 3项目) npm install pinia # 2. 初始化Pinia(修改src/main.js)
// src/main.js import { createApp } from 'vue'; import { createPinia } from 'pinia'; // 导入Pinia import App from './App.vue'; import router from './router'; // 后续路由会用到 // 创建Pinia实例 const pinia = createPinia(); const app = createApp(App); // 应用Pinia和路由 app.use(pinia); app.use(router); app.mount('#app');
(2)第二步:创建Pinia Store(全局状态容器)
Store是Pinia的核心,每个Store对应一个全局状态模块,此处创建"用户状态"Store:
// src/stores/userStore.js import { defineStore } from 'pinia'; import { loginApi, logoutApi, getUserInfoApi } from '../api/authApi.js'; // 假设的登录相关接口 // 定义Store:第一个参数是Store唯一标识,第二个参数是配置对象 export const useUserStore = defineStore('user', { // 1. 全局状态数据(类似组件的data) state: () => ({ userInfo: null, // 当前登录用户信息(null表示未登录) token: localStorage.getItem('token') || '', // 登录令牌(从本地存储获取,实现刷新页面不丢失) loading: false // 登录/退出加载状态 }), // 2. 计算属性(类似组件的computed,对state进行加工) getters: { // 判断是否登录 isLogin: (state) => !!state.userInfo, // 获取用户名(未登录返回'游客') userName: (state) => state.userInfo?.name || '游客' }, // 3. 异步/同步方法(类似组件的methods,用于修改state) actions: { // 登录方法 async login(data) { this.loading = true; try { // 调用登录接口 const res = await loginApi(data); const { token } = res.data; // 保存令牌到state和本地存储 this.token = token; localStorage.setItem('token', token); // 登录成功后获取用户信息 await this.fetchUserInfo(); return res; // 返回响应数据 } catch (err) { console.error('登录失败:', err); throw err; // 抛出错误,供组件处理 } finally { this.loading = false; } }, // 获取用户信息(登录后调用,或页面刷新时调用) async fetchUserInfo() { if (!this.token) return; // 无令牌则不请求 try { const res = await getUserInfoApi(); // 修改state中的用户信息(直接赋值即可,无需像Vuex那样用mutation) this.userInfo = res.data; } catch (err) { // 令牌失效,执行退出操作 this.logout(); throw err; } }, // 退出登录 async logout() { this.loading = true; try { // 调用退出接口(可选,根据后端需求) await logoutApi(); } catch (err) { console.error('退出失败:', err); } finally { // 清空state和本地存储 this.userInfo = null; this.token = ''; localStorage.removeItem('token'); this.loading = false; } } } });
(3)第三步:组件中使用Pinia Store(读取/修改全局状态)
① 登录组件(修改全局状态)
<template> <div class="login-page" style="max-width: 400px; margin: 100px auto; padding: 20px; border: 1px solid #eee; border-radius: 8px;"> <h2 style="text-align: center; margin-bottom: 30px;">用户登录</h2> <form @submit.prevent="handleLogin"> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">用户名:</label> <input v-model="formData.username" required style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"> </div> <div style="margin-bottom: 15px;"> <label style="display: block; margin-bottom: 5px;">密码:</label> <input type="password" v-model="formData.password" required style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;"> </div> <button type="submit" :disabled="userStore.loading" style="width: 100%; padding: 10px; border: none; border-radius: 4px; background: #42b983; color: #fff; cursor: pointer;"> {````{ userStore.loading ? '登录中...' : '登录' }} </button> </form> </div> </template> <script setup> import { ref } from 'vue'; import { useUserStore } from '../stores/userStore.js'; import { useRouter } from 'vue-router'; // 路由跳转,后续学习 // 1. 获取Store实例 const userStore = useUserStore(); const router = useRouter(); // 表单数据 const formData = ref({ username: '', password: '' }); // 登录处理 const handleLogin = async () => { try { // 2. 调用Store的login方法(修改全局状态) await userStore.login(formData.value); alert('登录成功!'); // 登录成功后跳转到首页 router.push('/'); } catch (err) { alert(`登录失败:${err.message}`); } }; </script>
② 导航栏组件(读取全局状态)
<template> <nav style="background: #333; padding: 10px 20px; color: #fff;"> <div style="max-width: 1200px; margin: 0 auto; display: flex; justify-content: space-between; align-items: center;"> <div>Vue进阶实战</div> <div> <span v-if="userStore.isLogin" style="margin-right: 20px;">欢迎您,{``{ userStore.userName }}</span> <button @click="userStore.logout()" :disabled="userStore.loading" style="padding: 6px 12px; border: none; border-radius: 4px; background: #ff4d4f; color: #fff; cursor: pointer;" > {``{ userStore.loading ? '退出中...' : (userStore.isLogin ? '退出' : '登录') }} </button> </div> </div> </nav> </template> <script setup> // 1. 引入并获取Store实例 import { useUserStore } from '../stores/userStore.js'; import { useRouter } from 'vue-router'; const userStore = useUserStore(); const router = useRouter(); // 未登录时,点击"登录"按钮跳转到登录页 document.querySelector('button').addEventListener('click', () => { if (!userStore.isLogin) { router.push('/login'); } }); </script>
Pinia核心语法总结(职场必背)
-
定义Store:用`defineStore('唯一标识', { state, getters, actions })`,其中state是状态数据,getters是计算属性,actions是修改状态的方法(支持异步);
-
使用Store:组件中用`const store = useStore()`获取实例,直接访问`store.state`、`store.getters`,调用`store.actions()`;
-
状态持久化:通过`localStorage`或`sessionStorage`在state中初始化数据,实现刷新页面状态不丢失(如示例中的token);
-
多模块管理:创建多个Store(如`userStore`、`cartStore`、`settingStore`),实现不同模块的状态隔离。