Vue 进阶实战:从待办清单到完整应用(路由 / 状态管理 / 性能优化全攻略)

发布于:2025-09-13 ⋅ 阅读:(17) ⋅ 点赞:(0)

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 应用了,接下来可以向这些方向深入:

  1. Vue 3 + Composition API:用更灵活的语法组织代码(比如把 Pinia 仓库的逻辑拆分成组合式函数),适合大型项目维护;

  2. TypeScript 整合:给 Vue 项目加类型校验,减少 bug,尤其适合团队协作(推荐先学 TS 基础,再用 defineProps defineEmits 等 Vue 3 的 TS 语法);

  3. 后端接口对接:用 Axios 发送请求,处理异步数据(比如登录对接后端接口,待办数据存数据库而非 localStorage);

  4. Nuxt.js(服务端渲染):解决 Vue 单页应用 “SEO 差” 的问题,适合做博客、商城等需要 SEO 的项目;

  5. 组件库二次开发:基于 Element Plus/Vant 封装业务组件(比如公司专属的表单组件、表格组件),提升团队开发效率。

最后:进阶的核心是 “解决实际问题”

很多人学进阶知识时会陷入 “只看文档不实践” 的误区,其实最好的学习方式是:找一个小项目(比如个人博客、简易商城),遇到 “权限控制” 就学路由守卫,遇到 “数据共享” 就学 Pinia,遇到 “卡顿” 就学性能优化 —— 带着问题学,才能真正把知识变成能力。

如果你在实践中遇到具体问题(比如 Pinia 状态同步失败、路由守卫循环跳转),欢迎在评论区留言,咱们一起拆解解决!也可以把你升级后的待办应用分享出来,互相交流学习~

(附:进阶学习资源:Pinia 官方文档Vue Router 官方文档(进阶部分)、B 站 “Vue 3+TS 实战项目” 教程)