臨近放假,又接到一份來自甲方長長的需求整改表。javascript
而後就參加了一場徹底都聽不懂的會,大多數都是業務上的問題,我本人沒具體參與過這個項目的開發,因此基本上徹底沒插上嘴,加上大屏幕光線太亮,照的我頭暈目眩,後面都快睡着了。html
其中前端方面有幾個問題,其中一項較大的問題是要加上多標籤頁。由於用戶是從舊系統遷移到新系統,交互思惟被舊有的操做習慣所限制,因此沒法適應新系統,這能理解。前端
乙方存在的意義就是解決甲方的問題,員工存在的意義就是解決公司的問題。vue
因此做爲乙方公司的員工,雖然好久沒寫過 Vue,也不太懂這個項目的框架設計,也要解決這些問題。java
原來的項目界面大致上都是這個樣子。npm
能夠看到上面是光禿禿的,連麪包屑都沒有。element-ui
甲方用戶想要看到這個列表頁是屬於哪一個模塊,而且還要加上多頁面間的切換。json
因此麪包屑沒法解決用戶的這個問題,必須添加上標籤頁了。因爲要加多標籤頁,麪包屑也就不須要了。後端
記得在 2017 年左右,因爲 SPA 框架大行其道,多家 UI 庫百花齊放。印象中iview-ui
應該是其中第一個作 admin 版本的(也有些組件庫稱爲 Pro 版本)。那時還在流行多標籤頁的設計。但後來你們慢慢發現這個東西存在嚴重的性能問題,並且沒有好的解決方案。如今我再去看那些 admin 版本的 UI 庫,居然沒有任何一個還保留着多標籤頁的設計,甚至不少連麪包屑都沒有了。數組
時間再早一些,不少網站都是採用window.open()
的方式直接在瀏覽器中打開新的標籤頁。
直到後來有些聰明的開發者研究出來在同一個頁面內實現多標籤頁,但同頁面內的多標籤頁流行的時代仍然比單頁面應用要早。他們也比較簡單粗暴,直接用el.innerHTML
替換掉 html 文本。
真正意義上的同頁面多標籤頁,是指切換標籤頁後,其它標籤頁仍然存活,而且保持原有狀態。
這個玩法,註定會有很大的內存佔用開銷,特別是在單頁面中。
咱們這套框架是前同事經過封裝 Vue 和一大堆 Vue 生態圈的三方庫而成的jboot,目的是爲了簡化 Vue 開發,現已開源。但因爲不是原來的 Vue,用起來若是不熟悉框架的話會比較吃力。
我首先找到了Menu
組件(菜單組件),從中找到了這麼一個方法。
/** * 菜單點擊事件 * @param event * @param menu * @param type */
menuItemClick(event, menu, type) {
if (!menu) return;
this.currentSelectedMenu = menu;
if (this.childrenMenuNotEmp(menu)) {
this.menuIsClick = true;
} else if (type === "click") {
let permission = routerTable().permission;
this.$jump({ name: menu.name });
this.$busBroadcast("menu.event.all-close");
}
}
複製代碼
經過調試,肯定這個地方就是跳轉頁面的地方。核心方法就是this.$jump
。這個 API 是框架提供的,雖然能夠改它的行爲,但我卻不打算改。先不說其它用到它的地方都會受到影響,並且在不熟悉框架的狀況下,亂改東西很是容易引起更多的問題。若是要再去閱讀源碼也費時費力。乾脆就不動它,想其餘辦法。
而後我要找到右側區域在哪裏,它在一個叫作layout
的組件(佈局組件)中。
<div class="content">
<router-view></router-view>
</div>
複製代碼
找到關鍵點,如今要作的事情很清晰了。我要把點擊菜單的事件改爲打開一個標籤頁。
實現的思路就是在layout
組件中維護一個數組,經過數組渲染多標籤頁。
而原來的點擊事件要去掉,換成給數組加入一條新數據。
因爲layout
組件和menu
組件是父子關係,layout
組件嵌套了menu
組件。因此最簡單的方式就是layout
給menu
傳遞一個回調函數。但考慮到這種全局數據,我首先想到的是 Vuex。但奇怪的是我在package.json
文件中沒有發現Vuex
的身影。在和這套框架使用時間最長的後端工程師溝經過後,得知該框架不能正常使用 Vuex。我原本想嘗試修復一下的,但考慮到時間問題,仍是算了。先解決掉現有的問題吧。因而我立馬又想到了event bus
。
肯定了組件間的接口,接下來要肯定用於渲染多標籤頁的數據格式。
簡單起見,我用了大約 5 秒鐘寫出了以下數組:
[
{
title: "首頁",
component: Home
}
],
複製代碼
數組中每一個對象做爲一個標籤頁,標籤頁的標題屬性是title
,標籤頁的渲染組件屬性是component
。
思路有了,接下來就是用代碼把它們實現出來。
bus.js
首先建立一個用於全局組件交互的通道。
bus.js
的用法很是簡單,用過 Vue 的同窗應該明白。
import Vue from "vue";
export default new Vue();
複製代碼
在layout
組件中添加用於渲染多標籤頁的數組以及當前選中的標籤頁title
。
import Home from "../home";
export default {
// 省略其餘代碼
data() {
return {
// 省略其餘代碼
pageTabsValue: "首頁",
pageTabs: [
{
title: "首頁",
component: Home,
},
],
};
},
};
複製代碼
菜單點擊的行爲,我稱之爲menu-add
事件。
在 layout 組件中監聽menu-add
事件。添加如下代碼:
import Bus from "./bus.js";
export default {
// 省略其餘代碼
methods: {
// 省略其餘代碼
// 點擊菜單回調
menuAddHandler() {
Bus.$on("menu-add", component => {
this.pageTabs.push(component);
this.pageTabsValue = component.title;
});
},
// 關閉標籤頁回調,先空着
removeTab() {}
},
created: {
// 省略其餘代碼
this.menuAddHandler();// 初始化組件時監聽 menu-add 事件
}
}
複製代碼
在menu
組件中派發menu-add
事件,修改原來的代碼以下:
import Bus from "./bus.js";
menuItemClick(event, menu, type) {
if (!menu) return;
this.currentSelectedMenu = menu;
if (this.childrenMenuNotEmp(menu)) {
this.menuIsClick = true;
} else if (type === "click") {
let permission = routerTable().permission;
// 經過測試,在菜單點擊回調中,menu.component是渲染的組件,menu.meta.title是頁面標題
Bus.$emit("menu-add", { component: menu.component, title: menu.meta.title });
// this.$jump({ name: menu.name });
// this.$busBroadcast("menu.event.all-close");
}
},
複製代碼
接下來就是最重要的一步,完成這一步,最基本的功能就完成了。
爲了簡單省事,我直接使用了框架內封裝的element-ui
組件庫,它裏面有一個el-tabs
組件。
Vue 中有一個component
標籤,是用於渲染組件用的。用慣了 React,不免會以爲這種寫法很不優雅,並且刻板。
<!-- <div class="content"> <router-view></router-view> </div> 原來的這三行代碼刪除掉,換成下面的代碼,再改改樣式便可。 -->
<el-tabs style="background-color: white; height: calc(100% - 55px);" v-model="pageTabsValue" closable @tab-remove="removeTab" >
<el-tab-pane style="height: 100%;" v-for="(item, index) in pageTabs" :key="item.name || index" :label="item.title" :name="item.title" >
<component :is="item.component" />
</el-tab-pane>
</el-tabs>
複製代碼
完成這一步,就能看到以下效果。點了幾個菜單,發現多標籤頁可以正常顯示了。
最後實現上面寫的removeTab
方法。el-tabs
組件的@tab-remove
事件會默認附帶一個targetName
參數,其實就是要關閉的標籤頁title
。作法也簡單粗暴,找到它,而後刪掉它。
removeTab(targetName) {
const removeIndex = this.pageTabs.findIndex(
item => item.title === targetName
);
this.pageTabs.splice(targetName, 1);
}
複製代碼
試了一下,確實能夠關閉。
至此,基本的功能已經實現。
接下來纔是重點。雖然基本功能已經實現,但還存在好多問題須要解決。我簡單羅列了一下:
通過嘗試與思考,我肯定不能使用splice
來操做pageTabs
,由於 Vue 的 DOM 更新策略,致使被刪除的節點後面節點都會被刷新。若是刷新的話,就會執行各個生命週期,狀態天然就沒法保存了。
爲了解決這個問題,我想到了另外一個辦法,給pageTabs
數組中的每一個元素添加一個show
屬性,用於區別該標籤是否顯示。
首先將數組的默認顯示的元素添加一個show
屬性。
pageTabs: [
{
title: "首頁",
component: Home,
show: true
}
],
複製代碼
而後在渲染標籤的地方添加一個v-if
。
<el-tab-pane style="height: 100%;" v-for="(item, index) in pageTabs" v-if="item.show" :key="item.name||index" :label="item.title" :name="item.title" >
<component :is="item.component" />
</el-tab-pane>
複製代碼
修改removeTab
的邏輯,關閉標籤再也不直接操做數組,而是將show
屬性改成false
。而且將首頁設置成不可關閉。
removeTab(targetName) {
const removeIndex = this.pageTabs.findIndex(
item => item.title === targetName
);
const currentIndex = this.pageTabs.findIndex(
item => item.title === this.pageTabsValue
);
if (removeIndex === 0) {
this.$message("首頁不能夠關閉");
return;
} else {
this.pageTabs[removeIndex].show = false; // 隱藏頁面
if (removeIndex === currentIndex) {
for (let i = 1; i < this.pageTabs.length; i++) {
if (this.pageTabs[removeIndex - i].show) {
this.pageTabsValue = this.pageTabs[removeIndex - i].title;
return;
}
}
}
}
}
複製代碼
對應的,打開頁面的方法也要變更。這裏進行一個判斷,若是這個標籤頁以前被打開過,那麼這個頁面組件仍然存在於內存中,只須要將show
屬性改成true
,它就會自動顯出出來。若是這個標籤頁第一次被打開,就須要再給這個對象添加show
屬性,並設置爲true
。
menuAddHandler() {
Bus.$on("menu-add", component => {
const isExist = this.pageTabs.some(tab => {
if (tab.title === component.title) {
return tab.show = true;
}
});
if (isExist) {
this.pageTabsValue = component.title;
} else {
this.pageTabs.push(Object.assign({ show: true }, component));
this.pageTabsValue = component.title;
}
});
},
複製代碼
至此,問題已經被解決,如今能夠提交給測試了,固然還有優化空間,其實封裝成獨立的組件可能更好的選擇,由於性能實在是太差,多開幾個標籤頁就會明顯卡頓,若是明年有時間的話能夠研究一下性能優化。
這篇文章想表達的思想,主要是解決問題的思路和具體實現的步驟。最重要的不是細節,而是思路。
說實話,咱們工做中遇到的不少問題均可以經過搜索引擎和書本上的知識來解決。可是咱們要問本身幾個問題。認識到問題是什麼?解決問題的宏觀思路是什麼?解決問題的微觀思路又是什麼?能把問題解決到什麼程度?解決問題的效率如何?這些問題的答案加起來,就是我的能力的體現。
獨立解決問題,特別是解決本身不熟悉、不擅長的問題,是一個工程師最基本能力的體現。