vue-element-admin 登陸
引言
vue-element-admin是vue生态圈中,最火的一個背景管理架構。基于vue和element-ui實作。
這篇文章主要會講解登陸的流程以及我認為這個架構的厲害的東西:動态路由,之前看代碼的時候,總想着一個登陸搞那麼麻煩,後面仔細品味發現原來一個小小的登陸功能涉及到了這麼多的東西。
準備工作
目錄結構
了解一個架構之前,先要從目錄結構入手(這裡直接引用花褲衩大佬的目錄結構)。目錄結構的了解能夠更加清楚子產品功能的劃分。
├── src # 源代碼│ ├── api # 所有的api請求│ ├── assets # 主題 字型等靜态資源│ ├── components # 全局公用元件│ ├── directive # 全局指令│ ├── filters # 全局 filter過濾器│ ├── icons # 項目所有 svg icons│ ├── layout # 全局 layout│ ├── router # 路由│ ├── store # 全局 store管理│ ├── styles # 全局樣式│ ├── utils # 全局公用方法│ ├── vendor # 公用vendor│ ├── views # views 所有頁面│ ├── App.vue # 入口頁面│ ├── main.js # 入口檔案 加載元件 初始化等│ └── permission.js # 權限管理├── tests # 測試├── .env.xxx # 環境變量配置├── .eslintrc.js # eslint 配置項├── .babelrc # babel-loader 配置├── .travis.yml # 自動化CI配置├── vue.config.js # vue-cli 配置├── postcss.config.js # postcss 配置└── package.json # package.json複制代碼
permission.js
在登陸這個流程中,permission.js這個是最重要的一環,其實這個檔案就是路由的全局鈎子(beforeEach和afterEach),全局鈎子的意思就是每次跳轉的時候可以根據情況進行攔截,不讓它進行跳轉。使用場景最常見的就是有些頁面需要使用者登陸之後才能通路,就可以在beforeEach中校驗使用者是否登陸來進行相對應的攔截處理。下面會詳細的講解permission.js的内容。
util / auth.js
這個檔案主要就是設定token到cookie中的操作封裝。
router
這個是路由中的一些設定,了解這個後面看元件Sidebar、TagViews将會事半功倍。
/** hidden: true 是否隐藏于Sidebar側邊欄 alwaysShow: true是否顯示在根菜單 redirect: noRedirectBreadcrumb中重定向的path name: 'router-name'用于keep-alive的Name meta: {roles: ['admin', 'editor'],目前路由的通路所需要權限title: 'title',Sidebar和Breadcrumb的titleicon: 'svg-name',Sibebar的iconnoCache: true是否設定不緩存breadcrumb: true是否顯示在Breadcrumb上activeMenu: '/example/list'Sidebar高亮時的顯示path }**/複制代碼
其他的一些我就沒有介紹了,比如說封裝好axios的request.js,還有把請求封裝成api,這些可以自行去了解。
view / login / index.vue
省略了一些的細枝末節,直接從點選登陸之後發生了一系列事情開始講起,第一個就是handleLogin方法。
handleLogin() { this.$refs.loginForm.validate(valid => { if (valid) { this.loading = true this.$store.dispatch('user/login', this.loginForm) .then(() => { this.$router.push({ path: this.redirect || '/', query: this.otherQuery }) this.loading = false }) .catch(() => { this.loading = false }) } else { console.log('error submit!!') return false } })}複制代碼
可以看到這個方法很簡單,就是利用validate方法進行表單驗證,驗證通過則使用this.$store.dispatch調用user/login方法并傳遞這個表單的資料,然後有一個.then()方法,方法裡面有this.$router.push(...),可能有同學就會有疑惑了,this.redirect和this.otherQuery是啥,用一句話來概括就是:我從哪裡跳到/login頁面,登陸之後我就傳回到哪裡。
這個user/login是什麼呢?一起來揭開它神秘的面紗。
user / login
先解析這個之前,先來補充一點vuex基礎知識:
vuex中使用namespaced:true開啟命名空間,調用mutations或者調用actions,則是子產品名 + 相對應的方法名。
另外actions是異步的,action處理函數之後傳回的Promise進行相對應的處理。
// user.js// 不開啟命名空間const actions = { login(){}}export default { actions };this.$store.dispatch('login');// user.js// 開啟命名空間const actions = { login(){}}export default { actions, namespaced: true }; // 注意!開啟命名空間this.$store.dispatch('user/login'); // 子產品名user + 方法名 login複制代碼
上面所說的user/login,則就是user子產品中的login方法,核心代碼就如下:
login({ commit }, userInfo) { const { username, password } = userInfo return new Promise((resolve, reject) => { login({ username: username.trim(), password: password }).then(response => { const { data } = response // 解構出data commit('SET_TOKEN', data.token) // 更新store裡面的token setToken(data.token) // token儲存到cookie resolve() }).catch(error => { reject(error) }) }) }複制代碼
一句話來概括:登陸驗證,登陸成功之後,分别儲存token到vuex、cookie中。
這裡完成之後就會回到之前的view/login/index.vue,當user/login調用完成之後,則會進行.then(...)方法,就是一個路由跳轉的過程(this.$router.push(...))。
Permission.js
這個檔案就如同前面介紹所說,路由的全局鈎子,動态路由的實作這裡相當于是一個入口。來看核心的實作代碼。
router.beforeEach(async(to, from, next) => { // 從cookie中取得token const hasToken = getToken() // 如果有token 也就是已經登陸的情況下 if (hasToken) { // 并且要前往的路徑是'/login' 則傳回 '/' if (to.path === '/login') { next({ path: '/' }) } else { // 從store中取得使用者的 roles, 也就是使用者的權限 并且使用者的權限數組必須有一個以上 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) { // 如果出現異常 清空路由 await store.dispatch('user/resetToken') // Message提示錯誤 Message.error(error || 'Has Error') // 跳到login頁面重新登陸 next(`/login?redirect=${to.path}`) } } } } else { // 沒有token 也就是沒有登陸的情況下 // 判斷是否是白名單(也就是說不需要登陸就可以通路的路由) if (whiteList.indexOf(to.path) !== -1) { next() } else { // 其他的一路給我跳到login頁面 老老實實的進行登陸 next(`/login?redirect=${to.path}`) NProgress.done() } }})複制代碼
注釋已經寫的明明白白了,這個思路其實使用的特别多,就是利用全局鈎子進行通路的攔截,如果沒有登陸的話,跳轉到登陸頁面進行登陸。
但是花褲衩大佬的這個有一點點不同,可以看到他将登陸和擷取使用者資訊分成了兩步,原因就是保證使用者資訊是最新的。第二個是在擷取使用者資訊之後傳回的roles生成可通路的路由,也就是這兩步實作了動态路由。
用一句話來概括:是否登陸?沒有就給我老老實實登陸。是否有使用者資訊?沒有就給我擷取使用者資訊,并且生成可通路路由然後利用addRoutes進行添加。
這兩步都是actions:user/getInfo,permission/generateRoutes。
可以看到這是vuex中的actions送出,可能有些小夥伴會有點困惑,為什麼有的請求直接寫在api檔案夾,有些就放在actions裡面?這個問題:我的了解就是,如果傳回的資料要儲存在vuex中的話,可以直接使用actions,原因是action裡面可以送出mutation改變store的狀态,可以少寫一些代碼,同時思路更加清晰,如果傳回的資料隻需要在目前頁面使用的話,并不需要儲存到vuex中,那就直接用寫在api檔案夾中引用即可。當然這個是我的個人了解,如有錯誤請望指出。
user / getInfo
首先我們看第一個user/getInfo。
如上面所說,這是user子產品中的getInfo方法,來看核心代碼。
getInfo({ commit , state }) { return new Promise((resolve, reject) => { // 調用getInfo接口 getInfo(state.token).then(response => { const { data } = response // 解構出data if (!data) { // 進行資料校驗 reject('Verification failed, please Login again.') } // 解構出需要儲存的值 const { roles, name, avatar, introduction } = data // roles權限數組至少有一個權限 if (!roles || roles.length <= 0) { reject('getInfo: roles must be a non-null array!') } // 儲存到vuex commit('SET_ROLES', roles) commit('SET_NAME', name) commit('SET_AVATAR', avatar) commit('SET_INTRODUCTION', introduction) // 将 data 傳回 resolve(data) }).catch(error => { reject(error) }) })}複制代碼
這個就是調用getInfo接口擷取使用者資訊并且儲存到vuex中,為了嚴謹性,進行相對應的資料校驗,然後把data傳回。
用一句話來概括:擷取使用者資訊,并儲存使用者資訊。
permission/generateRoutes
我們來看第二個actions,這個是動态路由中的重要一步,生成可通路路由,根據目前使用者的權限數組,和路由中可通路的權限數組,進行比對生成。
// 判斷是否有權限function hasPermission(roles, route) { if (route.meta && route.meta.roles) { // roles.some => Array.some 相當于是隻要有一個滿足就為true // 判斷使用者的權限于目前路由通路所需要的權限是否有一個滿足 // 比如說使用者權限為 ['one','two'] 目前路由通路所需要權限為 ['two','three'] 那麼就說明目前使用者可以通路這個路由 return roles.some(role => route.meta.roles.includes(role)) } else { // 預設是可通路的 return true }}// 生成可通路路由export function filterAsyncRoutes(routes, roles) { const res = [] routes.forEach(route => { const tmp = { ...route } // 判斷目前路由是否可以通路 if (hasPermission(roles, tmp)) { // 如果目前路由還有子路由 if (tmp.children) { // 進行遞歸處理 tmp.children = filterAsyncRoutes(tmp.children, roles) } // 将可通路路由放入數組中 res.push(tmp) } }) // 傳回 return res}// 為什麼要寫這裡呢,因為後面的Sidebar元件與這個環環相扣const mutations = { SET_ROUTES: (state, routes) => { // 添加的路由 state.addRoutes = routes // 将vuex中的路由進行更新 state.routes = constantRoutes.concat(routes) }}const actions = { generateRoutes({ commit }, roles) { return new Promise(resolve => { let accessedRoutes // 如果roles包含 'admin' 直接可以全部通路 if (roles.includes('admin')) { accessedRoutes = asyncRoutes || [] } else { // 利用 filterAsyncRoutes 過濾出可通路的路由 accessedRoutes = filterAsyncRoutes(asyncRoutes, roles) } // 儲存可通路的路由到store中 commit('SET_ROUTES', accessedRoutes) // 将可通路路由傳回 resolve(accessedRoutes) }) }}複制代碼
歐克,注釋已經全部寫好了,這個就是動态路由中重要一步,慢慢品味發現也不難,越看越覺得666。
用一句話來概括:根據得到的使用者權限生成可以通路的路由。
動态路由的實作
router.addRoutes(accessRoutes)複制代碼
???,沒呢?
對的!動态路由的核心代碼就這一句話,短小精悍,其他的都是為了完成動态路由做的一些 "準備工作" ,user/getInfo擷取使用者資訊得到使用者的roles權限數組,傳回user/generateRoutes生成可通路路由,就是這麼神奇的一步,實作了動态路由。
寫到這裡基本流程就走完了,但是還有兩個注意點。
注意
為什麼項目開啟預設就是登陸頁面
想必通過之前的講解也應該知道了permission.js的作用了,全局路由鈎子,每次路由跳轉都要調用全局路由鈎子(誰讓它是全局的呢),原因就是當頁面加載首頁時也會經過全局路由鈎子,而permission.js判斷目前使用者有沒有登陸,沒有登陸就直接跳轉到/login頁面進行登陸把,就是這麼任性,啦啦啦。
動态路由顯示不出來
有些小夥伴可能會改這個架構的代碼,但是發現顯示不出來?這也是我遇到過的一個坑,需要注意的是permission.js裡面有兩個條件:第一個是否登陸?第二個是否擷取使用者資訊(判斷是否擷取使用者資訊是根據roles)?有些小夥伴可能是登陸的時候就把使用者資訊擷取了,但是!!沒有更新使用者roles權限數組,是以就一直會擷取使用者資訊,進而導緻顯示不出來。
完結,撒花
其實到這裡整個登陸流程就已經結束了,可以看到花褲衩大佬想的很多,使用者資訊的擷取和使用者登陸的分離,根據使用者權限生成可以通路的路由并添加,這些思路我覺得很厲害,還有這麼多元件可以用,哈哈哈哈。
好的,我們來總結一下:
- Login/index.vue點選登陸 送出user/login的actions。
- user/login進行登陸驗證,登陸成功之後儲存token到vuex和cookie中。
- 回到Login/index.vue跳轉路由,這時就到了permission.js
- permission.js進行判斷是否登陸,是否有使用者資訊?沒有使用者資訊就擷取使用者資訊,并且儲存到vuex,然後根據使用者資訊中的roles生成可通路路由,并通過addRoutes進行添加。
看到這裡,相信你對這個架構的登陸流程已經有一定的了解了,自己再去理一遍把,啦啦啦啦,那下面這一張流程圖相信你也可以看懂。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SY5gzMmVmNxcDNyImZ5ImM0EDO1EWY2EWY0E2Y2EWY18CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)