一次Vue改版多标签页的实录

简介: 一次Vue改版多标签页的实录

问题来源:来自甲方的需求整改表


临近放假,又接到一份来自甲方长长的需求整改表。

然后参加了一场完全都听不懂的会,大多数都是业务上的问题,由于我本人没具体参与过这个项目的开发,所以基本上完全没插上嘴,加上大屏幕光线太亮,照的我头晕目眩,后面都快睡着了。

其中前端方面有几个问题,其中一项较大的问题是要加上多标签页。用户是从旧系统迁移到新系统,交互思维被旧有的操作习惯所限制,无法适应新系统,这能理解。

但是,乙方存在的意义就是解决甲方的问题,员工存在的意义就是解决公司的问题。

所以作为乙方公司的员工,虽然很久没写过 Vue,也不太懂这个项目的框架设计,我仍要解决这些问题。


问题分析:同页面中的多标签页


原来的项目界面大体上都是这个样子。

可以看到上面是光秃秃的,连面包屑都没有。

甲方用户想要看到这个列表页是属于哪个模块,最重要的是还要加上多页面间的切换。

所以面包屑无法解决用户的这个问题,必须添加上标签页了。

记得在 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组件。所以最简单的方式就是layoutmenu传递一个回调函数。但考虑到这种全局数据,我首先想到的是 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 事件


菜单点击的行为,我称之为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;
        }
    });
},


问题解决


至此,问题已经被解决,现在可以提交给测试了,当然还有优化空间,封装成独立的组件可能是更好的选择;因为性能实在是太差,多开几个标签页就会明显卡顿,如果明年有时间的话可以研究一下性能的优化。

这篇文章想表达的思想,主要是解决问题的思路和具体实现的步骤。最重要的不是细节,而是思路。

说实话,我们工作中遇到的很多问题都可以通过搜索引擎和书本上的知识来解决。

但是我们要通过几个问题给自己一个答案。

认识到问题是什么?

解决问题的宏观思路是什么?

解决问题的微观思路又是什么?

能把问题解决到什么程度?

解决问题的效率如何?

这些问题的答案加起来,就是我们要的那个答案。

独立解决问题,特别是解决自己不熟悉、不擅长的问题,是一个工程师最基本能力的体现。



相关文章
|
4天前
|
缓存 监控 JavaScript
探讨优化Vue应用性能和加载速度的策略
【5月更文挑战第17天】本文探讨了优化Vue应用性能和加载速度的策略:1) 精简代码和组件拆分以减少冗余;2) 使用计算属性和侦听器、懒加载、预加载和预获取优化路由;3) 数据懒加载和防抖节流处理高频事件;4) 图片压缩和选择合适格式,使用CDN加速资源加载;5) 利用浏览器缓存和组件缓存提高效率;6) 使用Vue Devtools和性能分析工具监控及调试。通过这些方法,可提升用户在复杂应用中的体验。
14 0
|
4天前
|
JavaScript 前端开发
vue(1),小白看完都会了
vue(1),小白看完都会了
|
3天前
|
JavaScript 开发工具 git
Vue 入门系列:.env 环境变量
Vue 入门系列:.env 环境变量
10 1
|
4天前
|
JavaScript 前端开发 定位技术
Vue使用地图以及实现轨迹回放 附完整代码
Vue使用地图以及实现轨迹回放 附完整代码
Vue使用地图以及实现轨迹回放 附完整代码
|
4天前
|
JavaScript
Vue中避免滥用this去读取data中数据
Vue中避免滥用this去读取data中数据
|
4天前
|
JavaScript
vue中使用pinia及持久化
vue中使用pinia及持久化
8 0
|
4天前
|
JavaScript 前端开发 UED
Vue class和style绑定:动态美化你的组件
Vue class和style绑定:动态美化你的组件
|
4天前
|
JavaScript 前端开发 API
Vue 监听器:让你的应用实时响应变化
Vue 监听器:让你的应用实时响应变化
|
4天前
|
JavaScript
vue封装svg
vue封装svg
10 0
|
4天前
|
JavaScript
vue封装面包屑组件
vue封装面包屑组件
8 0

相关实验场景

更多