vue-element-admin
作爲一款優秀的前端解決方案,整個框架的結構非常清晰,同時利用Vuex
和Vue Router
來實現SPA(單頁面應用)
開發模式,只需要簡單的編寫組件和配置文件即可完成項目的初步搭建,這對某些需要簡單的前端界面的團隊無疑是個福利。不過對於需要高度自定義的公司可能還需要更加深入的瞭解一下,所以這篇文章將帶你一步步的摸索整個項目的組件是如何加載的。
基礎頁面
要摸索當然首先就要找到入口,對於界面而言當然就是index.html
的作爲入口了。
public\index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
這個界面很簡單就是定義了一個#app
的節點,而是誰在給該節點渲染子節點呢。
src\router\index.js
import Vue from 'vue'
import Cookies from 'js-cookie'
import 'normalize.css/normalize.css' // a modern alternative to CSS resets
import Element from 'element-ui'
import './styles/element-variables.scss'
import enLang from 'element-ui/lib/locale/lang/en'// 如果使用中文語言包請默認支持,無需額外引入,請刪除該依賴
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import './icons' // icon
import './permission' // permission control
import './utils/error-log' // error log
import * 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
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
到這裏我們遇到了第一個路由組件<router-view />
,接下來我們來看看路由配置信息加載了什麼路由組件到這個位置。
src\main.js
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
爲例,該路徑配置配置信息如下:
src\router\index.js
{
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 都定向到當前頁面,從而能夠進行前端的路由管理。詳情請看這裏
src\layout\index.vue
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
<tags-view v-if="needTagsView" />
</div>
<app-main />
<right-panel v-if="showSettings">
<settings />
</right-panel>
</div>
</div>
</template>
<script>
import RightPanel from '@/components/RightPanel'
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import ResizeMixin from './mixin/ResizeHandler'
import { mapState } from 'vuex'
export default {
name: 'Layout',
components: {
AppMain,
Navbar,
RightPanel,
Settings,
Sidebar,
TagsView
},
mixins: [ResizeMixin],
computed: {
...mapState({
sidebar: state => state.app.sidebar,
device: state => state.app.device,
showSettings: state => state.settings.showSettings,
needTagsView: state => state.settings.tagsView,
fixedHeader: state => state.settings.fixedHeader
}),
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<!-- 省略SASS樣式 -->
一看這個模板就能看出整個項目的佈局結構了,大致分佈如下:
其中sidecar
、tabs-view
、 breadcrumd(麪包屑)
、navbar
等等結構,框架都已經自動完成填充,只需要在路由配置中增添相應的內容,對應內容就會顯示在指定位置,我們需要定製的就是AppMain
這一塊的內容。
src\layout\components\AppMain.vue
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
cachedViews() {
return this.$store.state.tagsView.cachedViews
},
key() {
return this.$route.path
}
}
}
</script>
<!-- 省略SASS樣式 -->
打開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
這個組件。
views/dashboard/index
<template>
<div class="dashboard-container">
<component :is="currentRole" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import adminDashboard from './admin'
import editorDashboard from './editor'
export default {
name: 'Dashboard',
components: { adminDashboard, editorDashboard },
data() {
return {
currentRole: 'adminDashboard'
}
},
computed: {
...mapGetters([
'roles'
])
},
created() {
if (!this.roles.includes('admin')) {
this.currentRole = 'editorDashboard'
}
}
}
</script>
這裏呢,index.vue
做了一下權限判斷處理,來決定加載哪一個數據面板界面,我們假設我們是以admin
角色登入,那麼adminDashboard
組件就將被渲染在<component :is="currentRole" />
的位置。所以我們再進一步看一下adminDashboard
組件。
src\views\dashboard\admin\index.vue
<template>
<div class="dashboard-editor-container">
<github-corner class="github-corner" />
<panel-group @handleSetLineChartData="handleSetLineChartData" />
<el-row style="background:#fff;padding:16px 16px 0;margin-bottom:32px;">
<line-chart :chart-data="lineChartData" />
</el-row>
<el-row :gutter="32">
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<raddar-chart />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<pie-chart />
</div>
</el-col>
<el-col :xs="24" :sm="24" :lg="8">
<div class="chart-wrapper">
<bar-chart />
</div>
</el-col>
</el-row>
<el-row :gutter="8">
<el-col :xs="{span: 24}" :sm="{span: 24}" :md="{span: 24}" :lg="{span: 12}" :xl="{span: 12}" style="padding-right:8px;margin-bottom:30px;">
<transaction-table />
</el-col>
<el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<todo-list />
</el-col>
<el-col :xs="{span: 24}" :sm="{span: 12}" :md="{span: 12}" :lg="{span: 6}" :xl="{span: 6}" style="margin-bottom:30px;">
<box-card />
</el-col>
</el-row>
</div>
</template>
<script>
import GithubCorner from '@/components/GithubCorner'
import PanelGroup from './components/PanelGroup'
import LineChart from './components/LineChart'
import RaddarChart from './components/RaddarChart'
import PieChart from './components/PieChart'
import BarChart from './components/BarChart'
import TransactionTable from './components/TransactionTable'
import TodoList from './components/TodoList'
import BoxCard from './components/BoxCard'
const lineChartData = {
newVisitis: {
expectedData: [100, 120, 161, 134, 105, 160, 165],
actualData: [120, 82, 91, 154, 162, 140, 145]
},
messages: {
expectedData: [200, 192, 120, 144, 160, 130, 140],
actualData: [180, 160, 151, 106, 145, 150, 130]
},
purchases: {
expectedData: [80, 100, 121, 104, 105, 90, 100],
actualData: [120, 90, 100, 138, 142, 130, 130]
},
shoppings: {
expectedData: [130, 140, 141, 142, 145, 150, 160],
actualData: [120, 82, 91, 154, 162, 140, 130]
}
}
export default {
name: 'DashboardAdmin',
components: {
GithubCorner,
PanelGroup,
LineChart,
RaddarChart,
PieChart,
BarChart,
TransactionTable,
TodoList,
BoxCard
},
data() {
return {
lineChartData: lineChartData.newVisitis
}
},
methods: {
handleSetLineChartData(type) {
this.lineChartData = lineChartData[type]
}
}
}
</script>
<!-- 省略樣式表 -->
對比着之前的截圖,這個組件中的節點就按照代碼中組織的那樣排列整齊。
動態路由
之前描述都是靜態路由的組件渲染加載工程,動態路由也是一樣的過程,只不過動態路由的配置信息是後面用戶登錄過後動態添加到Vue Router
而已。
由於動態路由是在用戶登錄之後被添加的,所以我們需要從登錄的最初始的地方開發探索。當然首先不能放過,作者配置的導航守衛,也就是攔截器。
src/permission.js
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
.
src\store\modules\permission.js
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
方法,所以掌握這些信息的管理方式也是很有必要的。當然,我們首先要找到狀態管理插件信息的傳遞入口:
src\main.js
import store from './store'
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
src\store\index.js
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
目錄的結構如下:
我們查看其中一個模塊文件
src\store\module\app.js
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()
等方法來調用,就像前面那樣.這些各個模塊通過在index.js中遍歷彙總,最終一併傳入Vue
中。而各個模塊中的state
較爲散亂,作者還特意將各個state
彙總到如下文件中,提供一個統一的訪問接口。
src\store\getter.js
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;
}
}
}