【前端·momo的项目回顾】vue3二次封装el-tag作为导航HistoryList

1,108 阅读5分钟

前言:什么都2023年了,你还只会用面包屑?作为一个在校学子,如需进行系统的编写,大都会使用面包屑的方式作为页面的标志,但是如果都使用面包屑,那么编程的趣味就少了点。俗话说:“编程重要的是勤奋努力,更重要的是想象力!”那有啥好的方案了,且看文章!

需求分析

我:师妹,你能想到除了面包屑之外还有其他的导航组件吗?
师妹:emm,我想想,好像还真想不出,还有其他吗?
我:当然啦,你看俺们教务网不就是一种吗!你能模仿一下咩?
师妹:当然!
几天后,当然师妹是还没能做出来的,让本mo出马!

  为啥使用面包屑?主要是因为其占用空间较小、对当前路径能够进行定位、对用户较为友好且更方便的是,很多UI组件库都对其进行了封装,方便开发者快速食用。但如前言所说的除了面包屑,我们还需要其他类似的组件用于这种导航场景。
  如下图所示,这种导航效果对于展示而言同样方便定位、用户友好,比起面包屑而言,这个组件还多了一个历史记录的效果。 image.png
  分析一下,这个组件所需要的功能:

  • 历史路由记录:需要历史路由进行渲染
  • 用户友好的类似tag的UI:可以看到,组件的相关样式与Tag相近,需要编写或引用类似的用户友好的效果
  • 当前页面定位:本组件与面包屑类似,则一定要进行当前页面的定位和凸显相关样式
  • 对历史信息的删除:从图中直接观察到,用户可以操作打开过的历史记录

技术选型

  从需求分析知道,这次组件的实现需要对路由信息进行劫持,则需要选择的一项技术是vue-router。
  样式是非常重要的,为了提升开发效率,我从项目的的UI组件库Element-Plus中选择el-tag进行二次封装。
  操作路由历史,我们需要将路由历史进行备份。项目使用了Pinia作为状态库,对于备份问题,除了Pinia外,我还曾想封装sessionStorage起到类似的效果。因为,如果单单使用Pinia,当页面刷新时,路由历史全部丢弃,这是我们所不希望看到的,而当使用Pinia持久化时,用户退出重登也会显示相关内容,sessionStorage能完美解决这个问题。但是,作为V3项目,我们当然可以借助V3的生命周期对其进行清理,同时Pinia又不用俺们自己封装,岂不是很利于效率!
  综上所述:组件使用Vue3、Vue-Router、Pinia、Element-Plus。

Coding Monent(代码时刻)

组件效果

image.png

<template>
    <HistoryList :stateRoute="memberCenterBaseRoute"></HistoryList>
</template>
<script setup lang="ts">
// 引用组件
import HistoryList from "./HistoryList.vue";
// 引用路由信息,路由信息的格式是 "vue-router"中的RouteRecordRaw类型
import {memberCenterBaseRoute} from "@/router/static"
</script>

组件需要父级路由作为信息对子组件进行传递,其中静态路由就是父级路由重定向的默认路由。

image.png

Pinia 部分

  Pinia需要对路由信息、显示项进行存储,并提供操作状态的相关方法。

import { defineStore } from "pinia";
import { RouteLocationNormalized } from "vue-router";
/**
 * 历史记录
 * 用来设置历史记录
 */
const HistoryList = defineStore({
  persist: true,// 持久化缓存Pinia
  id: "historyList",
  state: () => {
    return {
      path:"",// 父级别路由重定向的路径
      checkNum: 0,// 组件中的显示项Id,0是默认显示的父级路由重定向之后的组件
      // 历史记录列表
      historyList: [] as RouteLocationNormalized[],
    };
  },
  actions: {
     // 对路由历史进行劫持
    addHistory(router: RouteLocationNormalized) {
      // 判断是否是父级路由重定向的路由
      if (router.fullPath == this.path) return;
      // 从传入的路由中截取信息
      let { fullPath } = router;
      // 读fullPath进行格式化截取,截取?query前的
      if (fullPath.indexOf("?") != -1) {
        fullPath = fullPath.substr(0, fullPath.indexOf("?"));
      }
      // 查重,查看当前劫持信息是否已经存在
      let exist = this.historyList.some((item: RouteLocationNormalized,index:number) => {
        if (fullPath == item.fullPath) {
        // 如果存在的时候更新当前显示项,并退出函数
          this.checkNum = index+1;
          return true;
        }
      });
      // 不存在时
      if (!exist) {
      //则对信息进行劫持
        this.historyList.push(router)
        // 更新当前显示项的Id
        this.checkNum = this.historyList.length;
      }
      
    },
    // 删除接触的路由数组的对应项
    delete(index: number) {
      this.historyList.splice(index, 1);
    },
    // 更新当前展示的路由
    setCheckNum(checkNow: number) {
      while (this.checkNum != checkNow) {
        this.checkNum = checkNow;
      }
    },
    // 暴露给vue生命周期,对状态进行清理
    clear(){
      this.historyList=[]
      this.checkNum=0
    }
  },
});
export default HistoryList;

vue-router

  二次封装组件需要对路由进行劫持,当路由转发时,我们需要把信息放在我们希望的Pinia状态中,而劫持备份路由信息的最佳选择则是路由拦截器。

import {
  createRouter,
  createWebHistory,
  NavigationGuardNext,
  RouteLocationNormalized,
} from "vue-router";
// Pinia中的历史记录状态
import HistoryList from "@/store/HistoryList";
let historyStore = null;
/**
 * 全局前置路由守卫
 * 每次路由加载前
 */
router.beforeEach(
  async (
    to: RouteLocationNormalized,
    from: RouteLocationNormalized,
    next: NavigationGuardNext
  ) => {
    // 没有的时候构建
    if (historyStore == null) {
      historyStore = HistoryList();
    }
    // 对路由进行备份劫持
    historyStore.addHistory(to)
    next();
  }
);

HistoryList组件封装

  下面的代码是对组件进行封装,可以看到,组件是以el-tag的相关样式和行为,运用之前写的代码,对其进行某些封装和限定。
  值得注意的是,组件样式更新的Id是我们对其进行安排的,不能被删除的tag被对应成0,而能被删除等相关操作则用数组下边index+1的形式进行限定,这些下标的行为方便进行对比和状态的改变。

<template>
  <el-tag
    size="large"
    :type="store.$state.checkNum  == 0 ? 'primary' : 'info'"
    @click="toUserDetail"
    >{{ props.stateRoute.meta?.title }}</el-tag
  >
  <el-tag
    size="large"
    :type="store.$state.checkNum  == (index + 1 )? 'primary' : 'info'"
    v-for="(item, index) in pathList"
    :key="item.fullPath"
    closable="true"
    @close="closeTag(item, index)"
    @click="clickTag(item, index)"
    >{{ item.meta.title }}</el-tag
  >
</template>
<script lang="ts" setup>
// 路由跳转
import router from "@/router";
// Pinia状态的获取
import HistoryList from "@/store/HistoryList";
import { onUnmounted } from 'vue'
import { RouteRecordRaw } from "vue-router";
name: "HistoryList";
// 获取传过来的静态路由
const props = defineProps<{
  stateRoute: RouteRecordRaw;
}>();
// 获取历史记录状态和路由数组
const store = HistoryList();
store.$state.path=<string> props.stateRoute.redirect
let pathList = store.$state.historyList;

// 为用户详情页单独设置一个事件
const toUserDetail = () => {
  store.setCheckNum(0)
  router.push(props.stateRoute.path);
};
// 点击选项,对样式进行改变,对路由进行跳转
const clickTag = async (item: any, index: number) => {
  store.setCheckNum(index + 1)
  router.push(item.fullPath);
};
// 删除显示的选项
const closeTag = (item: any, index: number) => {
  // 删除
  store.delete(index);
  pathList = store.$state.historyList;
  store.setCheckNum(index)
  // 重新加载
  if (index == 0)   router.push(props.stateRoute.path);
  else router.push(pathList[index - 1].fullPath);
};

// 利用生命周期函数对信息进行重置
onUnmounted(()=>{
  store.clear()
})

</script>
<style lang="scss"></style>

结语

  好了,本次的组件就已经封装好了。面包屑是一个非常方便的组件,但是对于某种要求,作为合格的程序员,我们需要有多种呈现方式,这就是本次组件封装的原因之一。
  智者不入爱河,寡王一路offer!
  我是momo,我们下期见!

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情