vue-element-admin 登陸

世人發表於2019-08-14

vue-element-admin 登陸

引言

vue-element-adminvue生態圈中,最火的一個後臺管理框架。基於vueelement-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這個是最重要的一環,其實這個檔案就是路由的全域性鉤子(beforeEachafterEach),全域性鉤子的意思就是每次跳轉的時候可以根據情況進行攔截,不讓它進行跳轉。使用場景最常見的就是有些頁面需要使用者登陸之後才能訪問,就可以在beforeEach中校驗使用者是否登陸來進行相對應的攔截處理。下面會詳細的講解permission.js的內容。

util / auth.js

這個檔案主要就是設定tokencookie中的操作封裝。

router

這個是路由中的一些設定,理解這個後面看元件SidebarTagViews將會事半功倍。

/**
  hidden: true                      是否隱藏於Sidebar側邊欄       
  alwaysShow: true					是否顯示在根選單
  redirect: noRedirect				Breadcrumb中重定向的path
  name: 'router-name'				用於keep-alive的Name
  meta: {
	roles: ['admin', 'editor'],		當前路由的訪問所需要許可權
	title: 'title',					Sidebar和Breadcrumb的title
	icon: 'svg-name',				Sibebar的icon
	noCache: true					是否設定不快取
	breadcrumb: true				是否顯示在Breadcrumb上
	activeMenu: '/example/list'		Sidebar高亮時的顯示path
  }
**/
複製程式碼

其他的一些我就沒有介紹了,比如說封裝好axiosrequest.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.redirectthis.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)
      })
    })
 }
複製程式碼

一句話來概括:登陸驗證,登陸成功之後,分別儲存tokenvuexcookie中。

這裡完成之後就會回到之前的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/getInfopermission/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許可權陣列,所以就一直會獲取使用者資訊,從而導致顯示不出來。

完結,撒花

其實到這裡整個登陸流程就已經結束了,可以看到花褲衩大佬想的很多,使用者資訊的獲取和使用者登陸的分離,根據使用者許可權生成可以訪問的路由並新增,這些思路我覺得很厲害,還有這麼多元件可以用,哈哈哈哈。

好的,我們來總結一下:

  1. Login/index.vue點選登陸 提交user/login的actions。
  2. user/login進行登陸驗證,登陸成功之後儲存tokenvuexcookie中。
  3. 回到Login/index.vue跳轉路由,這時就到了permission.js
  4. permission.js進行判斷是否登陸,是否有使用者資訊?沒有使用者資訊就獲取使用者資訊,並且儲存到vuex,然後根據使用者資訊中的roles生成可訪問路由,並通過addRoutes進行新增。

看到這裡,相信你對這個框架的登陸流程已經有一定的瞭解了,自己再去理一遍把,啦啦啦啦,那下面這一張流程圖相信你也可以看懂。

login

相關文章