前言:什么都2023年了,你还只会用面包屑?作为一个在校学子,如需进行系统的编写,大都会使用面包屑的方式作为页面的标志,但是如果都使用面包屑,那么编程的趣味就少了点。俗话说:“编程重要的是勤奋努力,更重要的是想象力!”那有啥好的方案了,且看文章!
需求分析
我:师妹,你能想到除了面包屑之外还有其他的导航组件吗?
师妹:emm,我想想,好像还真想不出,还有其他吗?
我:当然啦,你看俺们教务网不就是一种吗!你能模仿一下咩?
师妹:当然!
几天后,当然师妹是还没能做出来的,让本mo出马!
为啥使用面包屑?主要是因为其占用空间较小、对当前路径能够进行定位、对用户较为友好且更方便的是,很多UI组件库都对其进行了封装
,方便开发者快速食用。但如前言所说的除了面包屑,我们还需要其他类似的组件用于这种导航场景。
如下图所示,这种导航效果对于展示而言同样方便定位、用户友好,比起面包屑而言,这个组件还多了一个历史记录的效果。
分析一下,这个组件所需要的功能:
历史路由记录
:需要历史路由进行渲染用户友好的类似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(代码时刻)
组件效果
<template>
<HistoryList :stateRoute="memberCenterBaseRoute"></HistoryList>
</template>
<script setup lang="ts">
// 引用组件
import HistoryList from "./HistoryList.vue";
// 引用路由信息,路由信息的格式是 "vue-router"中的RouteRecordRaw类型
import {memberCenterBaseRoute} from "@/router/static"
</script>
组件需要父级路由作为信息对子组件进行传递,其中静态路由就是父级路由重定向的默认路由。
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 天,点击查看活动详情