本专栏将从基础开始,循序渐进的讲解Vue的基本概念以及使用,希望大家都能够从中有所收获,也请大家多多支持。
**专栏地址: Vue专栏 **
相关软件地址: 相关安装包地址 **
** 如果文章知识点有错误的地方,请指正!大家一起学习,一起进步。
文章目录
1 介绍
该工程是基于vue的动态路由案例,复用一个组件,打开多个tab标签页,实现商品类组件复用,可以多tab打开
2 TODO
- base on vue-cli3 2019.9.21
3 特性
- 复用组件多tab打开
- 刷新动态路由页面不丢失
- 支持路由传参
- 修复刷新时,动态路由参数丢失
- 删除tab页签(包含注入的动态路由)
- 功能实现文档编写
- 代码优化,整合,方便快速部署该功能
- 代码Eslint 规则修改,消除warning
4 预览
代码地址:多标签案例
5 代码详细说明
5.1 设计vuex的存储结构并编写相关工具函数
首先
安装Vuex
,然后在
src
下新建
store.js
文件,并在store.js中添加
pageOpendList数组
,用来
存储导航栏的路由信息
,同时定义操作pageOpendList的函数,分别是用于
初始化
设置值tab的
setOpenedList函数
、路由调用后的钩子函数用于给
pageOpendList
动态新增路由的函数
increateTag
以及关闭标签页调用的
closeOpendList
函数,具体代码如下:
import Vue from"vue";import Vuex from"vuex";import{ storeSetting }from"./utils/config";
Vue.use(Vuex);// 本地存储sessionStorage键值const{ storeName }= storeSetting
const state ={/**
* 默认tabview 首页
*/pageOpendList:[{path:"/dashboard",name:"dashboard_index",component:()=>import("@views/DashBoard"),meta:{title:"首页",isTabView:true}}]};const mutations ={/**
* 初始化设置tab 一般默认首页,页面加载时调用
* @method tabOpendListInit
*/setOpenedList(state){const local =
sessionStorage[storeName]&&JSON.parse(sessionStorage[storeName]).length >0;if(local){
state.pageOpendList =JSON.parse(sessionStorage[storeName]);}},/**
* 路由调用前的钩子函数,用于更新参数
* @method setPageOpendList
*/setPageOpendList(state, res){const{ index, query, params, meta, path }= res;let opendPage = state.pageOpendList[index];if(params){
opendPage.params = params;}if(query){
opendPage.query = query;}if(meta){
opendPage.meta = meta;}if(path){
opendPage.path = path;}
state.pageOpendList.splice(index,1, opendPage);
sessionStorage[storeName]=JSON.stringify(state.pageOpendList);},//tabview中新增打开的页面increateTag(state, tag){
state.pageOpendList.push(tag);},/**
* @param {*} state
* @param {当前页签信息} obj
* @param { 当前实例 } obj.vm
* @param { 路由name} obj.name
*/closeOpendList(state, obj){// 临时解决方案 后续再完善const lists = state.pageOpendList;if(obj.name ==="dashboard_index"){return;}for(let i =0; i < lists.length; i++){if(lists[i].name === obj.name){const lastName = state.pageOpendList[i -1].name;
state.pageOpendList.splice(i,1);
sessionStorage.setItem(
storeName,JSON.stringify(state.pageOpendList));
obj.vm.$router.push({name: lastName
});}}}};const store =newVuex.Store({
state,
mutations
});exportdefault store;
其中utils目录存放的是项目的工具函数,包括动态插入路由insertRouter、动态移除路由removeRouter以及刷新路由refreshRouterSync等。config.js中配置了本地存储sessionStorage的键。base.js中编写了路由完成后的回调函数addOpendPage,用于给store.js中的变量pageOpendList更新或添加路由。index.js中配置了给外部使用的工具函数。具体代码分别如下:
- utils/dynamic-router/delete-local-router.js
import{ storeSetting }from"../config";/**
* @param {name} 动态路由编号,提交后删除本地存储的路由
*/exportconstdeleteLocalRouter=name=>{const{ dynamicName }= storeSetting;const localRoutes =
sessionStorage.getItem(dynamicName)&&JSON.parse(sessionStorage.getItem(dynamicName));if(localRoutes && localRoutes.length >0){for(let i =0; i < localRoutes.length; i++){if(localRoutes[i].name === name){//splic删除一个元素
localRoutes.splice(i,1);
sessionStorage.setItem(dynamicName,JSON.stringify(localRoutes));break;}}}};
- utils/dynamic-router/insert-router.js
import Main from"@views/Main.vue";//引入如果没有定义则会作export default的对象使用import configMapping from"../config";import{ storeSetting }from"../config";const{ dynamicName }= storeSetting;// const { storeName } = storeSetting;//logconstaddRouter=param=>{const vm = param.vm;const com = param.com;const name = param.name;const params = param.params;const query = param.query;const component = param.component;//新的路由const tab ={name: name,path:"/"+ name,component: component,//meta存储当前路由下可用的信息,访问方式为:$route.meta.xxxxmeta:{component: com,title: name,isTabView:true,
params,
query
}};/**
* 动态路由
*/// 判断是否已经存在该路由let flag =false;const routes =[];//对应路径的动态路由,增量式,不会覆盖"/"之前的内容const routerItem ={path:"/",name: dynamicName,component: Main,children:[]};// Stroage.setitem(key,value)保存数据;// sessionStroage.getitem(key)获取数据;//JSON.parse()可以把JSON规则的字符串转换为JSONObject//() && () 如果前面为ture,赋值为后面的变量//() || () 如果前面为false 赋值为后面的变量const dynamic =(sessionStorage.getItem(dynamicName)&&JSON.parse(sessionStorage.getItem(dynamicName)))||[];if(dynamic.length >0){const len = dynamic.length;for(let i =0; i < len; i++){if(dynamic[i].name === name){
flag =true;break;}}}/**
* 如果未打开,则新增路由
*/if(!flag){
routerItem.children.push(tab);
routes.push(routerItem);
dynamic.push(tab);//JSON.stringify 对象转成JSON字符串
sessionStorage.setItem(dynamicName,JSON.stringify(dynamic));//增量式添加路由
vm.$router.addRoutes(routes);}/**
* 跳转路由
*/
vm.$router.push({name: name,
params,
query
});};/**
* 这里只是针对一个组件进行复用,可根据业务进行动态传入组件name
* 具体请看@/pages/DashBoard/index.vue 具体用法
* @param {路由信息对象} message
*/exportconstinsertRouter=(message)=>{var obj ={vm: message.vm,//组件的名字为GoodDetail,需要写详细的组件地址component: configMapping[message.component],com: message.com,name: message.name,params: message.params,query: message.query
};//增量式添加路由addRouter(obj);};
- utils/dynamic-router/refresh-router.js
import Main from"@views/Main.vue";import configMapping from"../config";import{ storeSetting }from"../config";const{ dynamicName }= storeSetting;/**
* 防止页面刷新时, 路由丢失问题
* @param { routerMap } 动态路由模板映射
* @param { routes } 空数组,因为addRoutes只支持数组
* @param { childrens } children 不多解释
* @param {当前实例} vm
*/exportconstrefreshRouterSync=vm=>{// 此处正则提取的是路由的文件夹名,请注意,是为了map映射取key, 会得到GoodDetail// GoodDetail: () => import('@views/GoodDetail/GoodDetail.vue')const dynamic =(sessionStorage.getItem(dynamicName)&&JSON.parse(sessionStorage.getItem(dynamicName)))||[];const routes =[];const routerItem ={path:"/",name: dynamicName,component: Main,children:[]};if(dynamic.length >0){for(let i =0; i < dynamic.length; i++){//组件的全路径const FullPath = dynamic[i].meta.component;const mapName = FullPath.substring(
FullPath.lastIndexOf("/")+1,
FullPath.lastIndexOf("."));
dynamic[i].component = configMapping[mapName];
routerItem.children.push(dynamic[i]);}
routes.push(routerItem);//动态添加路由
vm.$router.addRoutes(routes);}};
- utils/dynamic-router/remove-router.js
import{ resetRouter }from"./reset-router";import{ deleteLocalRouter }from"./delete-local-router";import{ refreshRouterSync }from"./refresh-router";import store from"@/store";exportfunctionremoveRouter(vm, name){deleteLocalRouter(name);//重新设置路由表//设置为@/router/routes.js中配置的路由resetRouter(vm);//添加动态路由refreshRouterSync(vm);
store.commit("closeOpendList",{
vm,
name
});}
- utils/dynamic-router/reset-router.js
import VueRouter from"vue-router";//相当于export default对应的对象import mainRoutes from"@/router/routes";exportconstresetRouter=vm=>{//获取@/router/routes中所有的路由信息const routes =[...mainRoutes];let newRouter =newVueRouter({// mode: "hash",
routes
});//重新初始化静态路由表
vm.$router.matcher = newRouter.matcher;};
- utils/base.js
import store from"@/store";exportdefault{/**
* @method addOpendPage
* @param vm 当前实例
* @param name 当前路由name
* @param query 查询参数
* @param param 查询参数
* 一般放在router BeforeAfter(BeforeEach) 执行
*/addOpendPage:(vm, name, params ="", query ="", meta ="", path ="")=>{let pageOpendList = store.state.pageOpendList;let opendLen = pageOpendList.length;let i =0;let tagHasOpened =false;if(opendLen >0){for(; i < opendLen; i++){if(name === pageOpendList[i].name){
vm.$store.commit("setPageOpendList",{index: i,
params,
query,
meta
});
tagHasOpened =true;break;}}}/**
* 注入参数
*/if(!tagHasOpened && name){let tag ={name: name
};if(params){
tag.params = params;}if(query){
tag.query = query;}if(meta && meta.isTabView){
tag.meta = meta;}elseif(meta &&!meta.isTabView){return;}if(path){
tag.path = path;}
store.commit("increateTag", tag);}}};
- utils/config.js
/**
* @storeSetting
* 本地存储sessionStorage的键
* @param {storeName} String 打开tab的本地存储name
* @param {dynamicName} String 打开tab的动态路由存储name
*/exportconst storeSetting ={storeName:"tab-view-router-list",dynamicName:"dynamic-router-list"};/**
* @description
* 动态路由配置在此处
* 可以全局使用
*/exportdefault{GoodDetail:()=>import("@views/GoodDetail/GoodDetail.vue")};
- utils/index.js
import{ removeRouter }from"./dynamic-router/remove-router";import{ insertRouter }from"./dynamic-router/insert-router";import{ refreshRouterSync }from"./dynamic-router/refresh-router";exportdefault{
removeRouter,
insertRouter,
refreshRouterSync
};
5.2设置路由文件
在src目录下新建文件夹router用于存放路由信息,并在router文件夹中新建index.js以及routes.js文件,index.js用于配置路由信息,包括路由前以及路由后调用的钩子函数,routes.js中配置具体的路由信息,代码如下:
- router/index.js
import Vue from"vue";import VueRouter from"vue-router";import utils from"@/utils/base";import routes from"./routes";
Vue.use(VueRouter);const router =newVueRouter({mode:"hash",
routes
});//路由前调用//to是跳转到的路由,from是跳转前的路由,next是表示的是路由是否放行,常用的有next(true)和next(false)
router.beforeEach((to, from, next)=>{next();});//路由完成后调用
router.afterEach((to, from, next)=>{
utils.addOpendPage(
router.app,
to.name,
to.params,
to.query,
to.meta,
to.path
);});exportdefault router;
- router/routes.js
/**
* @param isTabView 是否放入Tabs管理
*/exportdefault[{path:"/",// name: "home_index",name:"home_index",//对应的主页面是Main.vue,在Main.vue默认显示/dashboardcomponent:()=>import("@views/Main.vue"),redirect:"/dashboard",children:[{path:"dashboard",name:"dashboard_index",component:()=>import("@views/DashBoard/index.vue"),meta:{isTabView:true,title:"首页"}},{path:"docs",name:"docs_index",component:()=>import("@views/Docs/index.vue"),meta:{isTabView:true,title:"文档使用"}}]}];
5.3 编写容器页面
分别编写DashBoard、HeaderArea、AsideArea、TabView、GoodDetail、Docs等页面组件,页面布局如下图所示:
- App.vue
<template><div id="app"><router-view></router-view></div></template><script>import utils from"./utils";exportdefault{name:"app",data(){return{};},beforeCreate(){
utils.refreshRouterSync(this);//setTimeout(f,0)为了手动调配优先级不高的代码靠后执行,将该语句添加到运行队列的队尾setTimeout(()=>{this.$store.commit("setOpenedList");},0);}};</script><style lang="scss">*{margin:0;padding:0;
box-sizing: border-box;}
html,
body {width:100%;height:100%;}
#app {
font-family:"Avenir", Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;width:100%;height:100%;}</style>
- Main.vue
<template><div class="main-wrapper"><header-area></header-area><aside-area></aside-area><div class="main-container"><!-- tabview栏区域--><tab-view></tab-view><!-- 右下角的路由区域--><router-view></router-view></div></div></template><script>import HeaderArea from"./HeaderArea";import AsideArea from"./AsideArea";import TabView from"./TabView";exportdefault{data(){return{};},components:{
HeaderArea,
AsideArea,
TabView
},methods:{}};</script><style lang="scss">.main-wrapper {width:100%;height:100%;.main-container {
margin-left: 120px;}
@media screen and(max-width: 800px){.main-container {
margin-left:0;}}}</style>
- HeaderArea
<template><div class="header-container">
Vue项目多tab标签实战
<a href="https://gitee.com/codinginn" target="_blank">@author</a
></div></template><style lang="scss" scoped>.header-container {height: 60px;
line-height: 60px;
text-align: center;
margin-left: 120px;
background-color: #fff;color: #000;
font-weight: bold;
box-shadow:0 1px 3px 0rgba(0,0,0,0.12),00 3px 0rgba(0,0,0,0.04);
a {color: #999;&:hover {color: #1fc7c7;}}
@media screen and(max-width: 800px){
margin-left:0;}}</style>
- AsideArea
<template><div class="aside-container"><div class="aside-container__text">侧边栏</div></div></template><style lang="scss" scoped>.aside-container {position: fixed;top:0;left:0;width: 120px;height:100%;color: #fff;
background-color: #304156;
text-align: center;
padding-top: 150px;&__text {width: 20px;margin:0 auto;
line-height: 24px;
font-size: 20px;}}
@media screen and(max-width: 800px){.aside-container {display: none;}}</style>
- TabView
<template><div class="tag-view-wrap"><el-tag
class="tag-view"
v-for="item in tagList":key="item.id":class="{ active: item.name === $route.name }"
@click.native="jump(item)"
@close="close(item.name)":closable="item.name !== 'dashboard_index'">{{ item.meta.title }}</el-tag></div></template><script>import utils from"@/utils";exportdefault{computed:{tagList(){returnthis.$store.state.pageOpendList;}},methods:{jump(item){const{ params, query }= item;/**
* @description
* 下面四种情况考虑到参数传递的问题,所以单独处理
*/if(params){this.$router.push({name: item.name,params: params
});return;}if(query){this.$router.push({name: item.name,query: query
});return;}if(query && params){this.$router.push({name: item.name,params: params,query: query
});return;}this.$router.push({name: item.name
});},close(name){
utils.removeRouter(this, name);}}};</script><style lang="scss">.tag-view-wrap {padding: 4px 0;
border-bottom: 1px solid rgb(230,230,230);.tag-view {
border-radius:0;margin:0 2px;border: 1px solid #eee;color: #495060;height: 26px;
line-height: 26px;
background-color: #fff;transition: all 0.2s;cursor: pointer;&.active {
background-color: #1fc7c7;color: #fff;.el-tag__close {color: #fff;}}}}</style>
- DashBoard
<template><div class="dash-board"><el-card class="box-card"><div slot="header"class="clearfix"><h2>模拟商品列表</h2></div><div v-for="o in 6":key="o"class="text-item" @click="jump(o)">{{"列表内容 "+ o }}</div></el-card><br /><el-button type="primary" @click="goToDocs">跳转到文档页[普通路由]</el-button></div></template><script>import utils from"@/utils";exportdefault{data(){return{};},methods:{jump(o){const name ="listing"+ o;/**
* @param { 当前实例 } vm
* @param { 当前动态路由模板名字 } component
* @param { 临时缓存组件地址 } com
* @param { 路由name } name
* @param { 具体传参数 } params
* @param { 查询参数 } query
*/var obj ={vm:this,component:"GoodDetail",com:"@views/GoodDetail/GoodDetail.vue",/*存储在meta中,表示主键的详细地址*/name: name,//tab标签名params:{id: o
},query:{}};//插入路由
utils.insertRouter(obj);},goToDocs(){this.$router.push({name:"docs_index"});}}};</script><style lang="scss" scoped>.text-item {
border-bottom: 1px dashed rgb(44,136,179);cursor: pointer;}</style>
- GoodDetail
<template><div class="good-detail"><h2>我是组件GoodDetail.vue</h2><h3>商品编号 {{ $route.meta.params.id }}</h3></div></template><script>exportdefault{mounted(){}};</script><style scoped>.good-detail {padding: 20px;}</style>
- Docs
<template><div class="docs-container"><el-card class="box-card"><div slot="header"class="clearfix"><span>普通路由</span></div><div>
常规(未复用的组件)路由直接在路由表配置
</div></el-card><el-card class="box-card"><div slot="header"class="clearfix"><span>动态路由</span></div><div>动态路由(需要复用的组件),路由需要在utils/config配置,使用utils(utils/dunamic-router)导出的方法进行跳转,删除.具体请看Dashboard/ 组件 有两种使用方式
</div></el-card></div></template><style lang="scss" scoped>.docs-container {display: flex;
justify-content: space-between;padding: 20px;.box-card {width:50%;margin:0 10px;}}</style>
版权归原作者 每天都要努力的小颓废呀 所有, 如有侵权,请联系我们删除。