Vue 进阶实战:从待办清单到完整应用(路由 / 状态管理 / 性能优化全攻略)
在上一篇博客里,我们一起实现了能本地存储的待办清单,不少朋友留言说:“学会了基础,但遇到‘登录后才能访问页面’‘多组件共享数据’就卡壳了,该怎么突破?”
其实我刚学 Vue 时也有过这种困惑 —— 基础语法会用,但一到实际项目的复杂场景(比如用户权限、多页面数据共享)就手足无措。今天这篇进阶指南,就带你解决这些 “实战痛点”,把简单的待办清单升级成带登录拦截、分类管理、全局状态共享的完整应用,同时掌握让项目更流畅的性能优化技巧。
一、路由进阶:从 “页面跳转” 到 “权限控制”
新手对 Vue Router 的认知可能停留在 “点击导航跳页面”,但实际项目中,我们需要更灵活的路由控制 —— 比如 “未登录不能进待办页面”“不同用户看不同菜单”。这部分就带你掌握 3 个核心进阶技巧:
1. 嵌套路由:实现 “布局复用”(比如页面侧边栏 + 内容区)
很多管理系统、工具类应用都有 “侧边栏导航 + 顶部栏 + 内容区” 的布局,用嵌套路由就能实现 “布局只写一次,内容区动态切换”。
实操步骤:
① 先创建布局组件 src/views/Layout.vue
(侧边栏 + 内容区框架):
<template>
<div class="layout">
<router-link to="/category" class="link">分类管理</router-link>
<button @click="logout" class="logout-btn">退出登录</button>
</aside>
<!-- 内容区:嵌套路由的出口,匹配的子路由会渲染在这里 -->
<main class="content">
<router-view />
</main>
</div>
</template>
<script>
export default {
methods: {
logout() {
// 清除本地存储的登录状态
localStorage.removeItem("isLogin");
// 跳回登录页
this.$router.push("/login");
}
}
};
</script>
<style scoped>
.layout { display: flex; height: 100vh; }
.sidebar { width: 200px; background: #333; color: #fff; padding: 20px; }
.link { display: block; color: #fff; text-decoration: none; margin: 15px 0; }
.link.active { color: #42b983; } /* 路由激活时的样式 */
.content { flex: 1; padding: 20px; overflow: auto; }
.logout-btn { margin-top: 30px; padding: 8px 16px; background: #f44336; color: #fff; border: none; cursor: pointer; }
</style>
② 配置嵌套路由(修改 src/router/index.js
):
import Vue from "vue";
import Router from "vue-router";
// 引入组件
import Login from "@/views/Login";
import Layout from "@/views/Layout";
import Todo from "@/views/Todo"; // 待办清单页面(原App.vue内容迁移过来)
import Category from "@/views/Category"; // 新增分类管理页面
Vue.use(Router);
export default new Router({
routes: [
// 登录页(无嵌套)
{ path: "/login", name: "Login", component: Login },
// 布局页(嵌套路由的父路由)
{
path: "/",
component: Layout,
meta: { requiresAuth: true }, // 标记:该路由需要登录才能访问
children: [
// 待办清单(子路由,路径空表示默认显示)
{ path: "", name: "Todo", component: Todo },
// 分类管理(子路由)
{ path: "category", name: "Category", component: Category }
]
}
]
});
- 效果:访问
/login
是单独的登录页;登录后进入/
,会显示侧边栏 + 内容区,点击侧边栏切换/todo
和/category
时,只有内容区变化,侧边栏始终存在 —— 这就是嵌套路由的核心价值:复用公共布局。
2. 路由守卫:实现 “登录拦截”(未登录不准进)
前面我们给 Layout 路由加了 meta: { requiresAuth: true }
,现在需要用 “路由守卫” 检测这个标记:如果用户没登录就想进 /todo
或 /category
,自动跳回登录页。
在 src/router/index.js
末尾添加全局前置守卫:
// 全局前置路由守卫:每次路由跳转前都会执行
router.beforeEach((to, from, next) => {
// 1. 判断目标路由是否需要登录(看meta.requiresAuth)
const needLogin = to.meta.requiresAuth;
// 2. 判断用户是否已登录(从localStorage取状态)
const isLogin = localStorage.getItem("isLogin") === "true";
if (needLogin) {
// 3. 需要登录:已登录则放行,未登录跳登录页
if (isLogin) {
next(); // 放行,继续跳转到目标路由
} else {
next({ path: "/login" }); // 强制跳登录页
}
} else {
// 不需要登录:直接放行(比如登录页)
next();
}
export default router; // 注意:这里要把原来的export default new Router(...)改成先赋值给router,再导出
再写个简单的登录页 src/views/Login.vue
:
<template>
<div class="login-container">
data() {
return { username: "" };
},
methods: {
login() {
if (this.username.trim()) {
// 存储登录状态和用户名(实际项目会对接后端接口,这里简化)
localStorage.setItem("isLogin", "true");
localStorage.setItem("username", this.username);
// 登录成功跳回之前想访问的页面(比如用户直接输/todo,被拦截后登录,登录后自动跳/todo)
this.$router.push(this.$route.query.redirect || "/");
} else {
alert("请输入用户名!");
}
}
}
};
</script>
<style scoped>
.login-container { width: 300px; margin: 100px auto; text-align: center; }
.input { width: 100%; padding: 10px; margin: 15px 0; border: 1px solid #ddd; border-radius: 4px; }
.login-btn { width: 100%; padding: 10px; background: #42b983; color: #fff; border: none; border-radius: 4px; cursor: pointer; }
</style>
- 避坑点:路由守卫里一定要调用
next()
!新手常忘写,导致页面卡住;另外,next({ path: "/login" })
会触发新一轮守卫,别在登录页也加requiresAuth
,否则会无限循环。
二、状态管理:用 Pinia 解决 “多组件数据共享”
上一篇的待办数据存在组件里,现在有了 Todo 和 Category 两个页面,需要共享 “分类列表”(比如待办要按分类筛选,分类管理要增删分类)—— 如果还用组件传值,会非常麻烦。这时候就需要 “状态管理工具”,Vue 3 推荐用 Pinia(比 Vuex 更简洁,支持 Vue 2 和 3)。
1. 先装 Pinia 并初始化
① 安装 Pinia(Vue 2 需要额外装适配包):
\# Vue 2项目
npm install pinia @pinia/vue2-plugin
\# Vue 3项目直接装pinia即可:npm install pinia
② 在 src/main.js
中引入并使用 Pinia:
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
// 引入Pinia
import { createPinia, PiniaVuePlugin } from "pinia";
Vue.use(PiniaVuePlugin); // Vue 2必须加这行
const pinia = createPinia();
new Vue({
router,
pinia, // 挂载Pinia到Vue实例
render: h => h(App)
}).$mount("#app");
2. 创建 Pinia 仓库:管理 “分类” 和 “待办” 状态
在 src/store
文件夹下新建 todoStore.js
(Pinia 的 “仓库” 相当于 Vuex 的 “模块”):
import { defineStore } from "pinia";
// 删除分类(同时删除该分类下的所有待办)
deleteCategory(categoryId) {
this.categories = this.categories.filter(c => c.id !== categoryId);
this.todos = this.todos.filter(t => t.categoryId !== categoryId);
this.saveToLocal();
// 如果删除的是当前选中的分类,切换到“全部”
if (this.activeCategoryId === categoryId) {
this.activeCategoryId = 0;
}
},
// 切换当前选中分类(用于筛选)
setActiveCategory(categoryId) {
this.activeCategoryId = categoryId;
},
// 同步state到localStorage(Pinia状态默认不持久化,需手动处理)
saveToLocal() {
localStorage.setItem("vueTodos", JSON.stringify(this.todos));
localStorage.setItem("vueCategories", JSON.stringify(this.categories));
}
}
});
3. 在组件中使用 Pinia 仓库
以 Todo 页面(src/views/Todo.vue
)为例,用 Pinia 替代原来的组件内数据:
<template>
<div class="todo-page">
useTodoStore().addTodo(this.newTodoText, this.selectedCategoryId);
this.newTodoText = "";
}
},
toggleTodoDone(todoId) {
useTodoStore().toggleTodoDone(todoId);
},
deleteTodo(todoId) {
useTodoStore().deleteTodo(todoId);
},
setActiveCategory(categoryId) {
useTodoStore().setActiveCategory(categoryId);
}
}
};
</script>
<style scoped>
/* 样式省略,可参考上一篇的待办清单样式,新增分类筛选的样式 */
.category-filter { margin: 15px 0; }
.category-filter button { margin-right: 10px; padding: 5px 10px; }
.category-filter button.active { background: #42b983; color: #fff; border: none; }
.add-todo .category-select { margin: 0 10px; padding: 5px; }
.todo-item .done { text-decoration: line-through; color: #999; }
</style>
- 核心优势:现在 Category 页面也能直接用
useTodoStore()
获取和修改分类数据,不用再通过父子组件传值;而且所有组件修改数据后,其他使用该状态的组件会自动更新 —— 这就是全局状态管理的价值。
三、组件通信高级技巧:不止 props 和 $emit
除了 “父子组件用 props/$emit”“全局数据用 Pinia”,实际项目中还会遇到 “兄弟组件通信”“跨多层级组件通信”,这时候用以下两种方法更高效:
1. EventBus:解决 “兄弟组件 / 无关联组件” 通信
比如 “分类管理页面删除分类后,待办页面要实时更新筛选状态”,用 EventBus 可以快速实现:
① 在 src/main.js
中创建 EventBus:
// 给Vue原型添加\$bus,所有组件都能访问
Vue.prototype.\$bus = new Vue();
② 发送事件(Category 页面删除分类时):
// Category.vue中删除分类的方法里,添加发送事件
deleteCategory(categoryId) {
useTodoStore().deleteCategory(categoryId);
// 发送事件:通知其他组件“分类已删除”
this.$bus.$emit("categoryDeleted", categoryId);
}
③ 接收事件(Todo 页面监听事件,更新筛选状态):
// Todo.vue的created钩子中监听事件
created() {
// 监听“分类已删除”事件
this.$bus.$on("categoryDeleted", (deletedId) => {
if (this.activeCategoryId === deletedId) {
this.setActiveCategory(0); // 切换到“全部”分类
}
});
},
// 组件销毁时移除监听,避免内存泄漏
beforeDestroy() {
this.$bus.$off("categoryDeleted");
}
- 避坑点:一定要在组件销毁时用
$off
移除事件监听,否则组件重复创建会导致事件多次触发。
2. provide/inject:解决 “跨多层级组件” 通信
比如 “Layout 组件的侧边栏需要显示用户名,而用户名在 Login 组件登录后存储在 localStorage”,如果用 props 传,需要 Layout→Sidebar 层层传递,很麻烦。用 provide/inject 可以直接 “跨级传递”:
① 父组件(比如 Layout.vue)用 provide 提供数据:
<script>
export default {
provide() {
// 提供“用户名”数据,所有子组件(无论层级多深)都能注入
return {
username: localStorage.getItem("username") || ""
};
}
};
</script>
② 子组件(比如 Sidebar.vue,假设是 Layout 的子组件)用 inject 接收数据:
<template>
<aside class="sidebar">
<div class="user-info">欢迎,{{ username }}</div>
<!-- 其他导航链接 -->
</aside>
</template>
<script>
export default {
// 注入父组件提供的“username”
inject: ["username"]
};
</script>
- 适用场景:全局配置(如主题色、接口基础 URL)、跨多层级的固定数据传递;不适合频繁变化的数据(频繁变化建议用 Pinia)。
四、性能优化:让你的 Vue 项目更流畅
新手写的项目常出现 “页面卡顿”“加载慢”,其实只需几个小技巧就能大幅提升性能,这部分带你掌握 4 个高频优化点:
1. v-for 必须加唯一 key,且不用 index 当 key
很多新手图方便用 v-for="(item, index) in list" :key="index"
,但当列表删除、排序时,index 会变化,Vue 会误判组件 “复用”,导致渲染错误。正确做法是用数据的唯一 ID 当 key:
\<!-- 错误:用index当key -->
\<li v-for="(todo, index) in todos" :key="index">{{ todo.text }}\</li>
\<!-- 正确:用数据的唯一ID当key -->
\<li v-for="todo in todos" :key="todo.id">{{ todo.text }}\</li>
2. 用 computed 缓存计算结果,避免重复计算
如果组件中多次用到 “筛选后的待办列表”,直接写表达式会重复计算,用 computed 缓存后只算一次:
<!-- 错误:多次重复计算 -->
<div>{{ todos.filter(t => !t.done).length }}</div>
<div>{{ todos.filter(t => !t.done).map(t => t.text).join(",") }}</div>
<!-- 正确:用computed缓存 -->
<template>
<div>{{ unDoneTodos.length }}</div>
<div>{{ unDoneTodos.map(t => t.text).join(",") }}</div>
</template>
<script>
export default {
computed: {
unDoneTodos() {
return this.todos.filter(t => !t.done); // 只计算一次,多次复用
}
}
};
</script>
3. 组件懒加载:减少首屏加载时间
默认情况下,Vue 会把所有组件打包成一个大 JS 文件,首屏加载慢。用 “路由懒加载” 让组件在需要时才加载:
// src/router/index.js 中修改组件引入方式
// 原来的方式:import Todo from "@/views/Todo";
// 懒加载方式:
const Todo = () => import("@/views/Todo");
const Category = () => import("@/views/Category");
const Login = () => import("@/views/Login");
const Layout = () => import("@/views/Layout");
// 路由配置不变
export default new Router({
routes: [/* ... */]
});
- 效果:首屏只加载 Login 或 Layout 的核心代码,进入 Todo 页面时才加载 Todo 组件的代码,首屏加载时间大幅缩短。
4. v-if 和 v-show 按需使用,避免频繁 DOM 操作
v-if:条件不满足时会 “销毁组件”,满足时 “重新创建”(适合不常切换的场景,如登录 / 未登录状态)
v-show:条件不满足时只是 “隐藏(display: none)”,组件始终存在(适合频繁切换的场景,如标签页、下拉菜单)
\<!-- 适合v-if:登录状态切换不频繁 -->
\<div v-if="isLogin">欢迎回来\</div>
\<div v-else>请登录\</div>
\<!-- 适合v-show:标签页频繁切换 -->
\<div v-show="activeTab === 'todo'">待办内容\</div>
\<div v-show="activeTab === 'category'">分类内容\</div>
五、实战升级:把待办应用变成 “可部署的产品”
现在我们的应用已经有登录、待办、分类功能了,最后做两个 “产品级” 优化,让它能真正部署上线:
1. 数据持久化优化:Pinia 结合 localStorage 自动同步
之前我们在 Pinia 的 actions 里手动调用 saveToLocal()
,现在可以用 Pinia 的 “订阅” 功能,让 state 变化时自动同步到 localStorage,不用每次手动调用:
// src/store/todoStore.js 中添加订阅
export const useTodoStore = defineStore("todo", {
state: () => ({ /* ... */ }),
getters: { /* ... */ },
actions: { /* ... */ }
});
// 订阅state变化:每次state修改后自动同步到localStorage
if (localStorage) {
const todoStore = useTodoStore();
todoStore.$subscribe((mutation, state) => {
localStorage.setItem("vueTodos", JSON.stringify(state.todos));
localStorage.setItem("vueCategories", JSON.stringify(state.categories));
});
}
2. 打包部署:生成可上线的静态文件
执行以下命令,Vue 会把项目打包成静态 HTML/CSS/JS 文件(在 dist 文件夹下):
npm run build
打包后,把 dist 文件夹里的文件上传到服务器(如 Nginx、Netlify、Vercel),就能通过域名访问你的 Vue 应用了!
避坑点:打包后如果打开 index.html 是空白页,需要修改
vue.config.js
配置公共路径(如果部署在服务器子目录):
// 项目根目录新建vue.config.js
module.exports = {
publicPath: "./" // 表示相对路径,适合本地打开或部署在子目录
};
六、进阶后的学习方向:从 “会用” 到 “精通”
掌握以上内容后,你已经能独立开发中小型 Vue 应用了,接下来可以向这些方向深入:
Vue 3 + Composition API:用更灵活的语法组织代码(比如把 Pinia 仓库的逻辑拆分成组合式函数),适合大型项目维护;
TypeScript 整合:给 Vue 项目加类型校验,减少 bug,尤其适合团队协作(推荐先学 TS 基础,再用
defineProps
defineEmits
等 Vue 3 的 TS 语法);后端接口对接:用 Axios 发送请求,处理异步数据(比如登录对接后端接口,待办数据存数据库而非 localStorage);
Nuxt.js(服务端渲染):解决 Vue 单页应用 “SEO 差” 的问题,适合做博客、商城等需要 SEO 的项目;
组件库二次开发:基于 Element Plus/Vant 封装业务组件(比如公司专属的表单组件、表格组件),提升团队开发效率。
最后:进阶的核心是 “解决实际问题”
很多人学进阶知识时会陷入 “只看文档不实践” 的误区,其实最好的学习方式是:找一个小项目(比如个人博客、简易商城),遇到 “权限控制” 就学路由守卫,遇到 “数据共享” 就学 Pinia,遇到 “卡顿” 就学性能优化 —— 带着问题学,才能真正把知识变成能力。
如果你在实践中遇到具体问题(比如 Pinia 状态同步失败、路由守卫循环跳转),欢迎在评论区留言,咱们一起拆解解决!也可以把你升级后的待办应用分享出来,互相交流学习~
(附:进阶学习资源:Pinia 官方文档、Vue Router 官方文档(进阶部分)、B 站 “Vue 3+TS 实战项目” 教程)