临近放假,又接到一份来自甲方长长的需求整改表。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;
}
});
},
复制代码
至此,问题已经被解决,如今能够提交给测试了,固然还有优化空间,其实封装成独立的组件可能更好的选择,由于性能实在是太差,多开几个标签页就会明显卡顿,若是明年有时间的话能够研究一下性能优化。
这篇文章想表达的思想,主要是解决问题的思路和具体实现的步骤。最重要的不是细节,而是思路。
说实话,咱们工做中遇到的不少问题均可以经过搜索引擎和书本上的知识来解决。可是咱们要问本身几个问题。认识到问题是什么?解决问题的宏观思路是什么?解决问题的微观思路又是什么?能把问题解决到什么程度?解决问题的效率如何?这些问题的答案加起来,就是我的能力的体现。
独立解决问题,特别是解决本身不熟悉、不擅长的问题,是一个工程师最基本能力的体现。