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.jsmodule.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 實戰項目” 教程)