上回书说到同步路由配置是在router/config.js中,与此同时,还可以通过开关asyncRoutes设置异步路由的加载。不得不说,作者思路确实思路清晰,不仅把项目中很多配置项如主题、导航布局、动画效果等开关抽象到了配置文件中,还设置了异步路由的配置以及根据路由匹配的菜单权限。
权限咱们暂且不聊,先看看路由以及左侧菜单树是如何生成的。
先看下生成后的完整路由配置项如下:
{
"path": "/", //路由路径 (必有)
"name": "首页", //路由名称
"component": { //路由加载的组件
"name": "TabsView",
"i18n": {
},
"components": { //子组件
},
"computed": {}, //计算属性
"beforeDestroy": [], //销毁钩子事件
"watch": {}, //属性监听
"methods": {},
"staticRenderFns": [], //render函数使用的静态方法
"_compiled": true,
"_scopeId": "data-v-012d64c5",
"beforeCreate": [],
"__file": "src/layouts/tabs/TabsView.vue" //文件路径
},
"redirect": "/login",
"children": [
{
"path": "dashboard",
"name": "Dashboard",
"meta": {
"icon": "dashboard",
"authority": { //用户角色权限
"permission": "*"
},
"pAuthorities": [ //操作权限
{
"permission": "*"
}
]
},
"component": {
"name": "BlankView",
"components": {
},
"computed": {},
"staticRenderFns": [],
"_compiled": true,
"_scopeId": "data-v-2172e1ea",
"beforeCreate": [
null
],
"beforeDestroy": [
null
],
"__file": "src/layouts/BlankView.vue"
},
"children": [
{
"path": "workplace",
"name": "工作台",
"meta": {
"page": {
"closable": false
},
"authority": {
"permission": "*"
},
"pAuthorities": [
{
"permission": "*"
},
{
"permission": "*"
}
]
}
},
{
"path": "analysis",
"name": "分析页",
"meta": {
"authority": {
"permission": "*"
},
"pAuthorities": [
{
"permission": "*"
},
{
"permission": "*"
}
]
}
}
]
}
]
}
比在config.js中配置的多了不少配置项呀,那是怎么生成的呢?
在第一篇中,我们看到主入口main中调用了initRouter方法
const router = initRouter(store.state.setting.asyncRoutes) //加载路由
//@router/index.js中如下方法
function initRouter(isAsync) {
const options = isAsync ? require('./async/config.async').default : require('./config').default
formatRoutes(options.routes)
return new Router(options)
}
其中asyncRouters为true表示加载异步路由,并引用了config.async.js(其实在项目实战中,这一步是要调用服务端接口去获取配置);asyncRouters为t为false,加载本地config.js配置。
formatRoutes方法做了两件事情:
- 把配置项中path不以/开头的都加上斜杠/
- 遍历各个路由配置项中是否在meta元数据中加了权限配置,没有则都加上permisson:*
做完后就通过new Router(options)返回router对象。
获取到router对象后,接下来就要把路由对象挂载到Vue中
bootstrap({router, store, i18n, message: Vue.prototype.$message}) //初始化路由以及路由守卫、axios拦截器
//对应的方法
function bootstrap({router, store, i18n, message}) {
// 设置应用配置
setAppOptions({router, store, i18n})
// 加载 axios 拦截器
loadInterceptors(interceptors, {router, store, i18n, message})
// 加载路由
loadRoutes()
// 加载路由守卫
loadGuards(guards, {router, store, i18n, message})
}
通过loadRoutes方法加载路由,loadRoutes中做了哪些事情呢?以下是关键部分
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) { //从服务端获取的route配置
store.commit('account/setRoutesConfig', routesConfig)
} else {
routesConfig = store.getters['account/routesConfig']
}
// 如果开启了异步路由,则加载异步路由配置
const asyncRoutes = store.state.setting.asyncRoutes
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap) //解析路由配置
const finalRoutes = mergeRoutes(basicOptions.routes, routes) //合并路由
formatRoutes(finalRoutes)
router.options = {...router.options, routes: finalRoutes}
router.matcher = new Router({...router.options, routes:[]}).matcher
router.addRoutes(finalRoutes) //重新挂载路由
}
}
// 提取路由国际化数据
mergeI18nFromRoutes(i18n, router.options.routes)
// 初始化Admin后台菜单数据
const rootRoute = router.options.routes.find(item => item.path === '/')
const menuRoutes = rootRoute && rootRoute.children
if (menuRoutes) { //把菜单数据放入到store中
store.commit('setting/setMenuData', menuRoutes)
}
把配置的路由数据和同步路由数据做一次合并,重新挂载到Vue中。同时把菜单数据存入到store中,原来是在解析路由时就把数据存储了。那后面就是拿到数据,去渲染!
在AdminLayout.vue中加载menuData数据
//AdminLayout.vue
...
//@components/menu/SideMenu.vue
{{systemName}}
渲染左侧菜单需要用到menu.js,里面绕来绕去有一套复杂的逻辑,到底是怎么生成的呢?这里就需要提到组件的render函数了。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
render函数是调用createElement(Vue 将 h 作为 createElement 的别名是 生态系统中的一个通用惯例)来创建dom元素,参数如下
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
理解了createElement函数,我们再回回过头来看menu.js的调用
render (h) {
return h(
Menu, //antd中的menu组件
{
props: {
theme: this.menuTheme, //主题
mode: this.$props.mode, //模式
selectedKeys: this.selectedKeys, //选择的菜单path
openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
},
on: { //事件
'update:openKeys': (val) => {
this.sOpenKeys = val
},
click: (obj) => {
obj.selectedKeys = [obj.key]
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.options) //子节点
)
}
//遍历store中的menuData数据,调用renderMenuItem方法嵌套生成dom树
renderMenuItem: function (h, menu) {
let tag = 'router-link' //每个菜单使用router-link标签创建
let config = {props: {to: menu.fullPath}, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}} //属性配置
if (menu.meta && menu.meta.link) {
tag = 'a'
config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}} //元数据包含link属性,则使用a标签创建
}
return h(
Item, {key: menu.fullPath},
[
h(tag, config,
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)
]
)
}
以上就是路由和菜单的生成分析,下回分析一下权限控制如何完成的。