[前端随笔][Vue] 多级菜单实现思路——组件嵌套
说在前面
本篇记录学习了vue-element-admin中的多级菜单的实现 [传送门]
@vue/cli 4.2.2;vuex;scss;组件嵌套
正文
创建项目
npm create 项目名 //或npm init webpack 项目名
安装element-ui
npm add element-ui //或npm i element-ui
安装vuex
npm add vuex //或npm i vuex
安装完vuex后会出现src/store目录,同时在src/main.js中vue实例添加了store(这里是关于vuex的知识先放一下)
首先侧边栏的内容哪来?需要根据路由表来展示。
所以我们需要
一、 构造子页面并配置路由
1 在src/views目录建两个目录和三个vue文件book/read.vue,book/write.vue和 movie/watch.vue (template+script构造页面)
2 接着配置这三个页面的路由如下
const routes = [
{
path: '/book',
component: Layout,
redirect: '/book/write',
children: [
{
path: '/book/write',
component: () => import('@/views/book/write'),
name: 'book',
meta: { title: '写书', icon: 'edit', roles: ['admin'] }
},
{
path: '/book/read',
component: () => import('@/views/book/read'),
name: 'book',
meta: { title: '读书', icon: 'edit' }
}
]
},
{
path: '/movie',
component: Layout,
redirect: '/movie/watch',
children: [
{
path: '/movie/watch',
component: () => import('@/views/movie/watch'),
name: 'movie',
meta: { title: '看电影', icon: 'edit' }
}
]
}
]
这里面的component:layout是什么呢?
二、构造主页面(准备引用菜单栏)
简单说layout它是一个整体的页面结构,比如一个页面有侧边栏也有正文内容,还有顶部底部等,他们都在这里被引入。
接下来就来实现它:
建立一个目录和主文件src/layout/index.vue,再建立一个目录src/layout/components存放整体结构下的一些部件,比如侧边栏、设置按钮等。
这里的index.vue
<template>
<div :class="classObj">
<sidebar class="sidebar-container" />
<!--页面的其他元素-->
</div>
</template> <script>
import { Sidebar } from './components'
import { mapState } from 'vuex' export default {
name: 'Layout',
components: {
Sidebar
},
mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
其中...mapState({})的作用是将store中的getter映射到局部计算属性 [文档传送门]
handleClickOutside()是控制侧边栏折叠,通过封装element-ui原生代码实现,可删去不细讲。
sidebar就是我们从./components引入的组件,也就是本篇的主角。
三、构造菜单栏
首先要了解element的菜单栏是怎么样子的 [文档传送门]
可以发现最外层是el-menu,内层可以是el-submenu或者el-menu-item,
若为el-submenu则其内部包含el-menu-item-group,内部可以继续包含el-submenu或者el-menu-item,如此形成多级菜单。
但要根据路由来实现,就要获取路由,可以通过vuex中的mapGetters来获取。
所以Sidebar/index.vue如下
<template>
<div>
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template> <script>
import { mapGetters } from 'vuex'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss' export default {
components: { SidebarItem },
computed: {
...mapGetters([
'permission_routes',
'sidebar'
]),
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>
el-scrollbar是element的隐藏属性,表示如果侧边栏过长会产生自定义的滚动条。
其中mapGetters方法前面加了三个点(对象展开运算符),使得它可以混入computed属性 (官方解释:使用对象展开运算符将 getter 混入 computed 对象中)
此处mapGetters的作用是将store中的getter映射到局部计算属性。
至此Sidebar/index.vue获得了路由表,进一步将它传给了内部的sidebar-item组件。
接着SidebarItem.vue文件如下,这里就是最精华的地方(组件嵌套自己)
<template>
<div v-if="!item.hidden"><!--来自于路由配置表-->
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<!--三个条件:1有且只有一个子项目,2子菜单是否还包含子菜单,3是否必须展示-->
<!--app-link在meta存在时展示,相当于添加点击功能,即a标签(处理内链还是外链)-->
<!--item是实际展示,该模版只有一个render()方法来渲染(渲染图标,标题等)-->
<!--若child小于2执行以上步骤--> <!--若child大于2执行以下步骤(我调我自己sidebar-item)-->
<!--插槽title展示父路由(render方法渲染)-->
<!--sidebar-item遍历child(is-nest控制isNest,决定submenu-title-noDropdown即子菜单各项目的样式是否展示,bath-path处理该菜单各项目的链接)-->
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template> <script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link' export default {
name: 'SidebarItem',
components: { Item, AppLink },
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {//把需要显示的children存入showingChildren
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
}) // When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
} // Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }//没有子菜单,则用parent覆盖onlyOneChild来展示
return true
} return false//child大于2时,则展示ELSubmenu
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>
SidebarItem.vue
首先要明确上面这段html分文两部分:v-if 和 v-else
v-if部分
这里的item来源于Sidebar/index.vue中sidebar-item标签的:item属性,即遍历路由表时的当前路由。
1 若有定义hidden属性将直接不显示;
2 template有三个条件的判断;
3 app-link是增加点击跳转的功能,给菜单项添加跳转路径(自定义的,详见Sidebar/Link.vue);
4 el-menu-item就是element的菜单栏最小单元,再往里面就是文字内容了
5 item是自定义的,为的是将icon和文字合在一起,所以这里又要定义一个自定义组件。(自定义的,详见Sidebar/Item.vue)
<template>
<!-- eslint-disable vue/require-component-is -->
<component v-bind="linkProps(to)"> <!--相当于:is :href :target :rel都写在linkProps中了,因为这里有两套属性,所以写在方法中-->
<slot />
</component>
</template> <script>
import { isExternal } from '@/utils/validate' export default {
props: {
to: {
type: String,
required: true
}
},
methods: {
linkProps(url) {
if (isExternal(url)) {//判断路由是否包含http,即外链。一般内链(路由)是类似相对路径的写法
return {
is: 'a',
href: url,
target: '_blank',
rel: 'noopener'
}
}
return {//若是内链(路由),则显示routerLink
is: 'router-link',
to: url
}
}
}
}
</script>
Sidebar/Link.vue
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = [] if (icon) {
vnodes.push(<svg-icon icon-class={icon}/>)
} if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
Sidebar/Item.vue
Link中主要是插槽slot,和对外链内链的区分;
Item中主要是用render函数渲染。
至此完成了一级菜单,接下来是子菜单的实现
v-else部分
el-submenu下面套用了Sidebar-item,也就是自己调用了自己。
这里与index下面调用的Sidebar-item区别在于多了is-nest(见注释)
如此递归调用,遍历子菜单时,还会继续检测子菜单是否有子菜单,有的话继续递归,这样就实现了无限层级的菜单。
下面补充vuex的内容
src/store/index.js(代码默认生成如下,结构由State、Getters、Mutation、Actions这四种组成)
import Vue from 'vue'
import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
可以理解为Store是一个容器,Store里面的状态与单纯的全局变量是不一样的,因为无法直接改变store中的状态。想要改变store中的状态,只有一个办法,显式地提交mutation。
当我们需要不止一个store的时候,为了便于维护,可以建立一个目录存放不同的store模块,再把它引入到store/index.js中。[两步:1建立目录,2引入]
所以我们在src/store目录下增加一个getters.js(映射)和建立modules目录(该目录用于存放不同store模块,所谓store模块就是一个个的形如src/store/index.js的代码,如上,但具体内容与作用不同),
getter.js(申明一个getters)
const getters = {
sidebar: state => state.app.sidebar,
permission_routes: state => state.permission.routes,
}
export default getters
接着建立app.js和permission.js
分别写入如下内容
import Cookies from 'js-cookie' const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
}
} const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
}
} const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
}
} export default {
namespaced: true,
state,
mutations,
actions
}
app.js
import { asyncRoutes, constantRoutes } from '@/router' /**
* Use meta.role to determine if the current user has permission
* @param roles
* @param route
*/
function hasPermission(roles, route) {
if (route.meta && route.meta.roles) {//若有配置权限,进行some判断(只要符合一条即为true),判断传过来的角色是否存在
return roles.some(role => route.meta.roles.includes(role))//传过来的roles符合该route所要求的用户权限,返回true,否则false
} else {//若未配置权限则默认可以访问,返回true
return true
}
} /**
* Filter asynchronous routing tables by recursion
* @param routes asyncRoutes
* @param roles
*/
export function filterAsyncRoutes(routes, roles) {
const res = [] routes.forEach(route => {
const tmp = { ...route }//浅拷贝routes
if (hasPermission(roles, tmp)) {//判断是否有权限访问当前遍历到的route
if (tmp.children) {
tmp.children = filterAsyncRoutes(tmp.children, roles)//过滤不该显示的子routes,并更新children
}
res.push(tmp)//存入空数组res
}
}) return res
} const state = {
routes: [],
addRoutes: []
} const mutations = {
SET_ROUTES: (state, routes) => {
state.addRoutes = routes//更新state中的addRoutes,暂时不用
state.routes = constantRoutes.concat(routes)//将constantRoutes和新的routes进行合并(侧边栏直接用state.routes)
}
} const actions = {
generateRoutes({ commit }, roles) {
return new Promise(resolve => {
let accessedRoutes
if (roles.includes('admin')) {//角色包含admin
accessedRoutes = asyncRoutes || []
} else {//角色不包含admin
accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)
}
commit('SET_ROUTES', accessedRoutes)//调用上方mutation中的SET_ROUTES,保存accessedRoutes
resolve(accessedRoutes)
})
}
} export default {
namespaced: true,
state,
mutations,
actions
}
permission.js
最后改写src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex' Vue.use(Vuex)
/****************这一段用来生成modules,见下面解释*******************/
// https://webpack.js.org/guides/dependency-management/#requirecontext
const modulesFiles = require.context('./modules', true, /\.js$/)
//多文件情况下整个文件夹统一导入
//(你创建了)一个module文件夹下面(不包含子目录),能被require请求到,所有文件名以 `.js` 结尾的文件形成的上下文(模块) // you do not need `import app from './modules/app'`
// it will auto require all vuex module from modules file
const modules = modulesFiles.keys().reduce((modules, modulePath) => {
// set './app.js' => 'app'
const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1')
const value = modulesFiles(modulePath)
modules[moduleName] = value.default
return modules
}, {})
/*****************这一段用来生成modules,见下面解释*******************/ export default new Vuex.Store({
modules,
getters
}) export default store
那么这里是如何把modules引入呢
1 其中require.context()会返回一个webpack的回调函数对象,
2 调用该对象内的keys()则会把各个路径以数组方式列出,
3 对该数组进行reduce()合成为一个总对象,
这一波操作最后会生成一个包含modules内所有模块的总对象,完成引入(由于我们modules是空的,所以会打印为空。)
这里为了举例用了vue-element-admin写好的,大概如下。(这里可以看出modules下面有app.js,errorLog.js等等不同作用的store,每一个store都有它自己的四件套State、Getters、Mutation、Actions)
到这里就设置好了自己项目的vuex,随着项目增大,需要的内容就会更多,小项目不用vuex更好。
[前端随笔][Vue] 多级菜单实现思路——组件嵌套的更多相关文章
- vue 基础-->进阶 教程(3):组件嵌套、组件之间的通信、路由机制
前面的nodejs教程并没有停止更新,因为node项目需要用vue来实现界面部分,所以先插入一个vue教程,以免不会的同学不能很好的完成项目. 本教程,将从零开始,教给大家vue的基础.高级操作.组件 ...
- Vue 关于多个父子组件嵌套传值
prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来.这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解. props: { selectMember: { ...
- 循序渐进VUE+Element 前端应用开发(26)--- 各种界面组件的使用(2)
在我们使用Vue+Element开发前端的时候,往往涉及到很多界面组件的使用,其中很多直接采用Element官方的案例即可,有些则是在这个基础上封装更好利用.更少代码的组件:另外有些则是直接采用第三方 ...
- Vue&Element 前端应用开发之菜单和路由的关系
我们一般的应用系统,菜单是很多功能界面的入口,菜单为了更好体现功能点的设置,一般都是动态从数据库生成的,而且还需要根据用户角色的不同,过滤掉部分没有权限的菜单:在Vue&Element的纯前端 ...
- 前端开发css实战:使用css制作网页中的多级菜单
前端开发css实战:使用css制作网页中的多级菜单 在日常工作中,大家都会遇到一些显示隐藏类菜单,比如页头导航.二维码显示隐藏.文本提示等等......而这些效果都是可以使用纯css实现的(而且非常简 ...
- 从后端到前端之Vue(六)表单组件
表单组件 做项目的时候会遇到一个比较头疼的问题,一个大表单里面有好多控件,一个一个做设置太麻烦,更头疼的是,需求还总在变化,一会多选.一会单选.一会下拉的,变来变去的烦死宝宝了. 那么怎么解决这个问题 ...
- EasyDSS RTMP流媒体服务器web前端:vue组件之间的传值,父组件向子组件传值
之前接触最多的都是EasyNVR,主要针对的都是前端的一些问题.也有接触到一些easydss流媒体服务器. 前端方面的,EasyDSS流媒体服务器与EasyNVR有着根本的不同.EasyNVR使用的是 ...
- EasyDSS高性能RTMP、HLS(m3u8)、HTTP-FLV、RTSP流媒体服务器web前端:vue组件之间的传值,父组件向子组件传值
前端方面,EasyDSS流媒体服务器与EasyNVR有着根本的不同.EasyNVR使用的是传统的js来进行开发,而EasyDSS使用的是webpack+vue来进行开发的,了解vue+webpack的 ...
- 前端面试 vue 部分 (5)——VUE组件之间通信的方式有哪些
VUE组件之间通信的方式有哪些(SSS) 常见使用场景可以分为三类: 父子通信: null 父向子传递数据是通过 props ,子向父是通过 $emit / $on $emit / $bus Vuex ...
随机推荐
- CF587F-Duff is Mad【AC自动机,根号分治】
正题 题目链接:https://www.luogu.com.cn/problem/CF587F 题目大意 给出\(n\)个字符串\(s\).\(q\)次询问给出\(l,r,k\)要求输出\(s_{l. ...
- P6499-[COCI2016-2017#2]Burza【状压dp】
正题 题目链接:https://www.luogu.com.cn/problem/P6499 题目大意 \(n\)个点的一棵树,开始有一个棋子在根处,开始先手选择一个点封锁,然后后手封锁棋子所在点然后 ...
- ARC115E-LEQ and NEQ【容斥,dp,线段树】
正题 题目链接:https://atcoder.jp/contests/arc115/tasks/arc115_d 题目大意 \(n\)个数字的序列\(x\),第\(x_i\in [1,A_i]\ca ...
- CF932G-Palindrome Partition【PAM】
正题 题目链接:https://www.luogu.com.cn/problem/CF932G 题目大意 给出一个长度为\(n\)的字符串,将其分为\(k\)段(\(k\)为任意偶数),记为\(p\) ...
- 三千字介绍Redis主从+哨兵+集群
一.Redis持久化策略 1.RDB 每隔几分钟或者一段时间会将redis内存中的数据全量的写入到一个文件中去. 优点: 因为他是每隔一段时间的全量备份,代表了每个时间段的数据.所以适合做冷备份. R ...
- HttpClient遭遇Connection Reset异常,如何正确配置?
最近工作中使用的HttpClient工具遇到的Connection Reset异常.在客户端和服务端配置不对的时候容易出现问题,下面就是记录一下如何解决这个问题的过程. 出现Connection Re ...
- Feed 流系统杂谈
什么是 Feed 流 Feed 流是社交和资讯类应用中常见的一种形态, 比如微博知乎的关注页.微信的订阅号和朋友圈等.Feed 流源于 RSS 订阅, 用户将自己感兴趣的网站的 RSS 地址登记到 R ...
- HTML5背景知识
目录 HTML5背景知识 HTML的历史 JavaScript出场 浏览器战争的结束 插件称雄 语义HTML浮出水面 发展态势:HTML标准滞后于其使用 HTML5简介 新标准 引入原生多媒体支持 引 ...
- js--标签语法的使用
前言 在日常开发中我们经常使用到递归.break.continue.return等语句改变程序运行的位置,其实,在 JavaScript 中还提供了标签语句,用于标记指定的代码块,便于跳转到指定的位置 ...
- Python中的sys.stdin和input、sys.stdout与print--附带讲解剑指offer42-连续子数组的最大和
2020秋招季,终于开始刷第一套真题了,整套试卷就一道编程题,还是剑指offer上的原题,结果答案死活不对,最后干脆直接提交答案算了,看了下别人的答案,原来是输入数据没有获取的原因,不过这个语法sys ...