本文共 12023 字,大约阅读时间需要 40 分钟。
vue-element-admin
作为一款优秀的前端解决方案,整个框架的结构非常清晰,同时利用Vuex
和Vue Router
来实现SPA(单页面应用)
开发模式,只需要简单的编写组件和配置文件即可完成项目的初步搭建,这对某些需要简单的前端界面的团队无疑是个福利。不过对于需要高度自定义的公司可能还需要更加深入的了解一下,所以这篇文章将带你一步步的摸索整个项目的组件是如何加载的。
要摸索当然首先就要找到入口,对于界面而言当然就是index.html
的作为入口了。
<%= webpackConfig.name %>
这个界面很简单就是定义了一个#app
的节点,而是谁在给该节点渲染子节点呢。
import Vue from 'vue'import Cookies from 'js-cookie'import 'normalize.css/normalize.css' // a modern alternative to CSS resetsimport Element from 'element-ui'import './styles/element-variables.scss'import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文语言包请默认支持,无需额外引入,请删除该依赖import '@/styles/index.scss' // global cssimport App from './App'import store from './store'import router from './router'import './icons' // iconimport './permission' // permission controlimport './utils/error-log' // error logimport * as filters from './filters' // global filters//如果是开发模式,则启动本地mock服务器if (process.env.NODE_ENV === 'production') { const { mockXHR } = require('../mock') mockXHR()}//使用element-ui插件Vue.use(Element, { size: Cookies.get('size') || 'medium', // set element-ui default size locale: enLang // 如果使用中文,无需设置,请删除})//指定组件过滤器Object.keys(filters).forEach(key => { Vue.filter(key, filters[key])})Vue.config.productionTip = false//为index.html绑定基础组件App.vue,同时绑定路由和状态管理插件new Vue({ el: '#app', router, store, render: h => h(App)})// App.vue
到这里我们遇到了第一个路由组件<router-view />
,接下来我们来看看路由配置信息加载了什么路由组件到这个位置。
import router from './router'new Vue({ el: '#app', router, store, render: h => h(App)})
import Vue from 'vue'import Router from 'vue-router'Vue.use(Router)/* Layout */import Layout from '@/layout'/* Router Modules */import componentsRouter from './modules/components'import chartsRouter from './modules/charts'import tableRouter from './modules/table'import nestedRouter from './modules/nested'export const constantRoutes = [ { path: '/redirect', component: Layout, hidden: true, children: [ { path: '/redirect/:path(.*)', component: () => import('@/views/redirect/index') } ] }, { path: '/login', component: () => import('@/views/login/index'), hidden: true }, { path: '/auth-redirect', component: () => import('@/views/login/auth-redirect'), hidden: true }, { path: '/404', component: () => import('@/views/error-page/404'), hidden: true }, { path: '/401', component: () => import('@/views/error-page/401'), hidden: true }, { path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/dashboard/index'), name: 'Dashboard', meta: { title: 'Dashboard', icon: 'dashboard', affix: true } } ] }, ...]/** * asyncRoutes * the routes that need to be dynamically loaded based on user roles */export const asyncRoutes = [ { path: '/permission', component: Layout, redirect: '/permission/page', alwaysShow: true, // will always show the root menu name: 'Permission', meta: { title: 'Permission', icon: 'lock', roles: ['admin', 'editor'] // you can set roles in root nav }, children: [ { path: 'page', component: () => import('@/views/permission/page'), name: 'PagePermission', meta: { title: 'Page Permission', roles: ['admin'] // or you can only set roles in sub nav } }, { path: 'directive', component: () => import('@/views/permission/directive'), name: 'DirectivePermission', meta: { title: 'Directive Permission' // if do not set roles, means: this page does not require permission } }, { path: 'role', component: () => import('@/views/permission/role'), name: 'RolePermission', meta: { title: 'Role Permission', roles: ['admin'] } } ] }, /** when your routing map is too long, you can split it into small modules **/ componentsRouter, chartsRouter, nestedRouter, tableRouter, ..... { path: '*', redirect: '/404', hidden: true }]const createRouter = () => new Router({ //切换界面后自动滚动到顶部 scrollBehavior: () => ({ y: 0 }), routes: constantRoutes})const router = createRouter()export function resetRouter() { const newRouter = createRouter() router.matcher = newRouter.matcher // reset router}export default router
vue-element-admin
框架将路由分为静态路由(constantRoutes)
和动态路由(asyncRoutes)
两部分,静态路由
指定的是这些访问路径不需要权限就可以访问,比如404/401/login
这些界面。而动态路由是指需要获取相应权限的才可以访问的路径,例如:文章编辑界面
、权限管理界面
等等都是需要有权限才能访问的。
这里存在静态路由的原因是本项目是作为一个示例Demo运行的,而实际项目大部分都是利用当前用户的权限查询之后,然后动态添加到
Vue Router
中的(后面会谈到)
我们以/dashboard
为例,该路径配置配置信息如下:
{ path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/dashboard/index'), name: 'Dashboard', meta: { title: 'Dashboard', icon: 'dashboard', affix: true } } ]},
根据Vue Router
的文档可知,当浏览器访问到http://localhost:9527/#/dashboard
路径时,App.vue
文件中的<router-view />
将会被组件Layout
给替换。这里多了个#
是因为本项目默认采用的是 hashHistory
,还有一种browserHistory
。
两者的区别简单来说是对路由方式的处理不一样,
hashHistory
是以#
后面的路径进行处理,通过HTML 5 History
进行前端路由管理,而browserHistory
则是类似我们通常的页面访问路径,并没有#
,但要通过服务端的配置,能够访问指定的 url 都定向到当前页面,从而能够进行前端的路由管理。
一看这个模板就能看出整个项目的布局结构了,大致分布如下:
其中sidecar
、tabs-view
、 breadcrumd(面包屑)
、navbar
等等结构,框架都已经自动完成填充,只需要在路由配置中增添相应的内容,对应内容就会显示在指定位置,我们需要定制的就是AppMain
这一块的内容。 打开AppMain
组件之后你会发现他就定义一个简单的布局,并留有一个<router-view :key="key" />
(key主要是用于区分不同路由下的相同组件的)子节点,用于路由时的填充。还是之前的那个示例:
{ path: '/', component: Layout, redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/dashboard/index'), name: 'Dashboard', meta: { title: 'Dashboard', icon: 'dashboard', affix: true } } ]},
可以看出在该路由下,AppMain
中的<router-view>
将被填入@/views/dashboard/index
这个组件。
这里呢,index.vue
做了一下权限判断处理,来决定加载哪一个数据面板界面,我们假设我们是以admin
角色登入,那么adminDashboard
组件就将被渲染在<component :is="currentRole" />
的位置。所以我们再进一步看一下adminDashboard
组件。
对比着之前的截图,这个组件中的节点就按照代码中组织的那样排列整齐。
之前描述都是静态路由的组件渲染加载工程,动态路由也是一样的过程,只不过动态路由的配置信息是后面用户登录过后动态添加到Vue Router
而已。
由于动态路由是在用户登录之后被添加的,所以我们需要从登录的最初始的地方开发探索。当然首先不能放过,作者配置的导航守卫,也就是拦截器。
const whiteList = ['/login', '/auth-redirect']router.beforeEach(async(to, from, next) => { //启动导航进度条 NProgress.start() //设置页面标题 document.title = getPageTitle(to.meta.title) //获取从cookie中获取token const hasToken = getToken() if (hasToken) { if (to.path === '/login') { //如果有token的情况下进入login界面,则直接默认登录跳转到Dashboard界面 next({ path: '/' }) NProgress.done() } else { //如果是其他界面,则查询该用户的角色信息 const hasRoles = store.getters.roles && store.getters.roles.length > 0 if (hasRoles) { //有角色信息则允许继续路由 next() } else { try { //尝试查询角色信息 const { roles } = await store.dispatch('user/getInfo') //这里就是根据角色信息添加动态路由,这里是异步的 const accessRoutes = await store.dispatch('permission/generateRoutes', roles) router.addRoutes(accessRoutes) next({ ...to, replace: true }) } catch (error) { //如果出错则说明token有问题需要重新获取 await store.dispatch('user/resetToken') Message.error(error || 'Has Error') next(`/login?redirect=${ to.path}`) NProgress.done() } } } } else { // 没有获取token的说明第一次登录,重定向到登录界面 // 如果是login、auth-redirect则允许继续路由 if (whiteList.indexOf(to.path) !== -1) { next() } else { next(`/login?redirect=${ to.path}`) NProgress.done() } }})
从上面可以看出动态的路由信息是利用Vuex
的异步action
获取的,即调用permission/generateRoutes
.
const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes if (roles.includes('admin')) { accessedRoutes = asyncRoutes || [] } else { //这里是根据每个路由配置信息中的meta元信息中的rules过滤 accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } //同步调用下面的mutations方法 commit('SET_ROUTES', accessedRoutes) resolve(accessedRoutes) }) }}const mutations = { SET_ROUTES: (state, routes) => { state.addRoutes = routes //合并静态路由和动态路由 state.routes = constantRoutes.concat(routes) }}
到这里整个路由的构成和功能就介绍完毕了。
在vue-element-admin
中使用了很多Vuex
提供的getter
、action
、mutations
方法,所以掌握这些信息的管理方式也是很有必要的。当然,我们首先要找到状态管理插件信息的传递入口:
import store from './store'new Vue({ el: '#app', router, store, render: h => h(App)})
import Vue from 'vue'import Vuex from 'vuex'import getters from './getters'//使用Vuex插件Vue.use(Vuex)//获取store目录的下的modules目录的所有文件const modulesFiles = require.context('./modules', true, /\.js$/)//遍历上述文件const modules = modulesFiles.keys().reduce((modules, modulePath) => { //将每个文件注册为文件名对应的模块 const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, '$1') const value = modulesFiles(modulePath) modules[moduleName] = value.default return modules}, { })const store = new Vuex.Store({ modules, getters})export default store
src\store
目录的结构如下:
我们查看其中一个模块文件
import Cookies from 'js-cookie'const state = { sidebar: { opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, withoutAnimation: false }, device: 'desktop', size: Cookies.get('size') || 'medium'}const mutations = { TOGGLE_SIDEBAR: state => { state.sidebar.opened = !state.sidebar.opened state.sidebar.withoutAnimation = false if (state.sidebar.opened) { Cookies.set('sidebarStatus', 1) } else { Cookies.set('sidebarStatus', 0) } }, ......}const actions = { toggleSideBar({ commit }) { commit('TOGGLE_SIDEBAR') }, ......}export default { namespaced: true, state, mutations, actions}
这个模块中定义了mutations
、actions
等,可以通过commit()
、dispatch()
等方法来调用,就像.这些各个模块通过在中遍历汇总,最终一并传入Vue
中。而各个模块中的state
较为散乱,作者还特意将各个state
汇总到如下文件中,提供一个统一的访问接口。
const getters = { sidebar: state => state.app.sidebar, size: state => state.app.size, device: state => state.app.device, visitedViews: state => state.tagsView.visitedViews, cachedViews: state => state.tagsView.cachedViews, token: state => state.user.token, avatar: state => state.user.avatar, name: state => state.user.name, introduction: state => state.user.introduction, roles: state => state.user.roles, permission_routes: state => state.permission.routes, errorLogs: state => state.errorLog.logs}export default getters
通过这种方式,我们只需要通过mapGetters
辅助函数就可以将store
中的对应 getter
映射到局部计算属性中,或者通过如下方式访问到:
import { mapGetters } from 'vuex'export default { // ... computed: { ...mapGetters([ //如果计算属性和getter中命名相同, // 则可以直接写为'sidebar', sidebar:'sidebar', // ... ]) }}//上述方法等效于如下:export default { // ... computed: { sidebar(){ return this.$store.getters.sidebar; } }}
转载地址:http://mqfen.baihongyu.com/