Vue3寫一個(gè)后臺(tái)管理系統(tǒng)(3)通用后臺(tái)登錄方案解析

viewslogin 文件夾,創(chuàng)建 index.vue 文件,書寫登錄界面,

都是一些基本的UI操作,我下邊直接粘貼出代碼,

<template>
  <div class="login-container">
    <el-form ref="loginFromRef" class="login-form" :model="loginForm" :rules="loginRules">
      <div class="title-container">
        <h3 class="title">后臺(tái)管理系統(tǒng)</h3>
      </div>

      <el-form-item prop="username">
        <span class="icon-container">
          <i class="el-icon-user"></i>
        </span>
        <el-input placeholder="請輸入用戶名" name="username" type="text" v-model="loginForm.username" />
      </el-form-item>

      <el-form-item prop="password">
        <span class="icon-container">
          <i class="el-icon-lock"></i>
        </span>
        <el-input placeholder="請輸入密碼" name="password" :type="passwordType" v-model="loginForm.password" />
        <span class="show-pwd">
          <svg-icon
            :icon="passwordType === 'password' ? 'eye' : 'eye-open'"
            @click="onChangePwdType"
          />

        </span>
      </el-form-item>

      <el-form-item class="code-box">
       <span class="icon-container">

         <i class="el-icon-tickets"></i>

        </span>
        <el-input
          placeholder="圖形驗(yàn)證碼"
          v-model="loginForm.captcha_code" class="code-input" maxlength="4">
        </el-input>
        <div class="code-img" @click="getCodeImg">{{loginForm.captcha_code}}</div>
      </el-form-item>


      <el-button type="primary" style="width: 100%; margin-bottom: 30px;" :loading="loading"
        @click="handleLogin">登錄</el-button>
    </el-form>
  </div>
</template>

<script setup>
import {} from 'vue'
</script>

<style lang="scss" scoped></style>
router/index.js 中增加以下路由配置
/**
 * 公開路由表
 */
const publicRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login/index')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes: publicRoutes
})

其中的一些icon圖標(biāo)用的是自己封裝的SvgIcon組件(包含element-plus 的圖標(biāo)和自定義的 svg 圖標(biāo)),我們這節(jié)主要講的是后臺(tái)系統(tǒng)整個(gè)登錄的方案實(shí)現(xiàn),一些設(shè)計(jì)的簡單UI及其UI組件,相信只要有過 element-ui 使用經(jīng)驗(yàn)的同學(xué),應(yīng)該對這里都不陌生,所以這里就不對這塊內(nèi)容進(jìn)行過多贅述了,如果有想要代碼的小伙伴,下方留言,我可以私發(fā)你具體的代碼。

整個(gè)登錄界面的UI搭建完成后,效果就是這樣的
image.png

處理完了基本UI表單操作之后,接下來就是登錄操作的實(shí)現(xiàn)了。
對于登錄操作在后臺(tái)項(xiàng)目中是一個(gè)通用的解決方案,具體可以分為以下幾點(diǎn):

  1. 封裝 axios 模塊
  2. 封裝 接口請求 模塊
  3. 封裝登錄請求動(dòng)作
  4. 保存服務(wù)端返回的 token
  5. 登錄鑒權(quán)

這些內(nèi)容就共同的組成了一套 后臺(tái)登錄解決方案 。接下來,我們就分別來去處理介紹這些內(nèi)容。

封裝 axios 模塊

在當(dāng)前這個(gè)場景下,我們希望封裝出來的 axios 模塊,至少需要具備一種能力,那就是:根據(jù)當(dāng)前模式的不同,設(shè)定不同的 BaseUrl ,因?yàn)橥ǔG闆r下企業(yè)級項(xiàng)目在 開發(fā)狀態(tài)生產(chǎn)狀態(tài) 下它的 baseUrl 是不同的。

對于 @vue/cli 來說,它具備三種不同的模式:

  1. development
  2. test
  3. production

根據(jù)我們前面所提到的 開發(fā)狀態(tài)和生產(chǎn)狀態(tài) 那么此時(shí)我們的 axios 必須要滿足:在 開發(fā) || 生產(chǎn) 狀態(tài)下,可以設(shè)定不同 BaseUrl 的能力

那么想要解決這個(gè)問題,就必須要使用到 @vue/cli 所提供的 環(huán)境變量 來去進(jìn)行實(shí)現(xiàn)。

我們可以在項(xiàng)目中創(chuàng)建兩個(gè)文件:

  1. .env.development

  2. .env.production

它們分別對應(yīng) 開發(fā)狀態(tài)生產(chǎn)狀態(tài)

我們可以在上面兩個(gè)文件中分別寫入以下代碼:
.env.development

# 標(biāo)志
ENV = 'development'

# base api
VUE_APP_BASE_API = '/api'

.env.production

# 標(biāo)志
ENV = 'production'

# base api
VUE_APP_BASE_API = '/prod-api'

有了這兩個(gè)文件之后,我們就可以創(chuàng)建對應(yīng)的 axios 模塊

創(chuàng)建 utils/request.js ,寫入如下代碼:

import axios from 'axios'

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 5000
})

export default service
封裝請求動(dòng)作

創(chuàng)建 api 文件夾,創(chuàng)建 sys.js

import request from '@/utils/request'

/**
 * 登錄
 */
export const login = data => {
  return request({
    url: '/sys/login',
    method: 'POST',
    data
  })
}

封裝登錄請求動(dòng)作:

該動(dòng)作我們期望把它封裝到 vuexaction

store 下創(chuàng)建 modules 文件夾,創(chuàng)建 user.js 模塊,用于處理所有和 用戶相關(guān) 的內(nèi)容:

import { login } from '@/api/sys'
export default {
  namespaced: true,
  state: () => ({}),
  mutations: {},
  actions: {
    login(context, userInfo) {
      const { username, password } = userInfo
      return new Promise((resolve, reject) => {
        login({
          username,
          password: password  
        })
          .then(data => {
            resolve()
          })
          .catch(err => {
            reject(err)
          })
      })
    }
  }
}

store/index 中完成注冊:

import { createStore } from 'vuex'
import user from './modules/user.js'
export default createStore({
  modules: {
    user
  }
})
登錄觸發(fā)動(dòng)作

<script setup>
import { ref, onMounted } from 'vue'
import { validatePassword } from './rules'
import { getCode } from '@/api/user'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'



onMounted(() => {
  getCodeImg()
})

//數(shù)據(jù)源
const loginForm = ref({
   username: 'test',
  password: '123456',
  captcha_code: '',
  code_key: ''
})
//驗(yàn)證規(guī)則
const loginRules = ref({
  username: [
    {
      required: true,
      trigger: 'blur',
      message: '請輸入用戶名'
    },

  ],
  password: [
    {
      required: true,
      trigger: 'blur',
      validator: validatePassword()
    },

  ],
})

// 處理密碼框文本顯示狀態(tài)
const passwordType = ref('password')
const onChangePwdType = () => {
  if (passwordType.value === 'password') {
    passwordType.value = 'text'
  } else {
    passwordType.value = 'password'
  }
}

// 登錄動(dòng)作處理
const loading = ref(false)
const loginFromRef = ref(null)
const store = useStore()
const router = useRouter()

/**
 * 登錄
 */
const handleLogin = () => {
  loginFromRef.value.validate(valid => {
    if (!valid) return
    loading.value = true
    store.dispatch('user/login', loginForm.value)
      .then(() => {
        loading.value = false
        // TODO: 登錄后操作
        router.push('/')

      })
      .catch(err => {
        getCodeImg()
        loading.value = false
      })

  })


}
/**
 * 獲取圖形驗(yàn)證碼
 */
const getCodeImg = () => {
  getCode({})
    .then(data => {
      let obj = data.bizobj

      loginForm.value.code_key = obj.code_key
      loginForm.value.captcha_code = obj.code
    })
    .catch(err => {
    })
}
</script>

當(dāng)然這時(shí)候你如果,去觸發(fā)登錄操作,可能會(huì)報(bào)錯(cuò),因?yàn)槲覀冞€沒有設(shè)置代理
vue.config.js 中,加入以下代碼:

  devServer: {
    open: true,//啟動(dòng)自動(dòng)打開瀏覽器
    // 當(dāng)?shù)刂分杏?api的時(shí)候會(huì)觸發(fā)代理機(jī)制
    proxy: {
      '/': {
        target: '填寫你們自己公司的測試URL', //測試環(huán)境
        changeOrigin: true,
        ws: false,
        pathRewrite: {
          // '^/api': '' //需要rewrite重寫的,
        }
      }
    }
  },

重新啟動(dòng)服務(wù),再次進(jìn)行請求,就可以得到返回?cái)?shù)據(jù)了

本地緩存處理方案

通常情況下,在獲取到 token 之后,我們會(huì)把 token 進(jìn)行緩存,而緩存的方式將會(huì)分為兩種:

  1. 本地緩存:LocalStorage
  2. 全局狀態(tài)管理:Vuex

保存在 LocalStorage 是為了方便實(shí)現(xiàn) 自動(dòng)登錄功能

保存在 vuex 中是為了后面在其他位置進(jìn)行使用

那么下面我們就分別來實(shí)現(xiàn)對應(yīng)的緩存方案:

LocalStorage:

  1. 創(chuàng)建 utils/storage.js 文件,封裝三個(gè)對應(yīng)方法:

    /**
     * 存儲(chǔ)數(shù)據(jù)
     */
    export const setItem = (key, value) => {
      // 將數(shù)組、對象類型的數(shù)據(jù)轉(zhuǎn)化為 JSON 字符串進(jìn)行存儲(chǔ)
      if (typeof value === 'object') {
        value = JSON.stringify(value)
      }
      window.localStorage.setItem(key, value)
    }
    
    /**
     * 獲取數(shù)據(jù)
     */
    export const getItem = key => {
      const data = window.localStorage.getItem(key)
      try {
        return JSON.parse(data)
      } catch (err) {
        return data
      }
    }
    
    /**
     * 刪除數(shù)據(jù)
     */
    export const removeItem = key => {
      window.localStorage.removeItem(key)
    }
    
    /**
     * 刪除所有數(shù)據(jù)
     */
    export const removeAllItem = key => {
      window.localStorage.clear()
    }
    
  2. vuexuser 模塊下,處理 token 的保存

    import { login } from '@/api/sys'
    import { setItem, getItem } from '@/utils/storage'
    import { TOKEN } from '@/constant'
    export default {
      namespaced: true,
      state: () => ({
        token: getItem(TOKEN) || ''
      }),
      mutations: {
        setToken(state, token) {
          state.token = token
          setItem(TOKEN, token)
        }
      },
      actions: {
        login(context, userInfo) {
          ...
              .then(data => {
                this.commit('user/setToken', data.data.data.token)
                resolve()
              })
              ...
          })
        }
      }
    }
    
    
  3. 處理保存的過程中,需要?jiǎng)?chuàng)建 constant 常量目錄 constant/index.js

    export const TOKEN = 'token'
    

此時(shí),當(dāng)點(diǎn)擊登陸時(shí),即可把 token 保存至 vuexlocalStorage

響應(yīng)數(shù)據(jù)的統(tǒng)一處理

utils/request.js 中實(shí)現(xiàn)以下代碼:

import axios from 'axios'
import { ElMessage } from 'element-plus'

...
// 響應(yīng)攔截器
service.interceptors.response.use(
response => {
 const { success, message, data } = response.data
 //   要根據(jù)success的成功與否決定下面的操作
 if (success) {
   return data
 } else {
   // 業(yè)務(wù)錯(cuò)誤
   ElMessage.error(message) // 提示錯(cuò)誤消息
   return Promise.reject(new Error(message))
 }
},
error => {
 ElMessage.error(error.message) // 提示錯(cuò)誤信息
 return Promise.reject(error)
}
)

export default service

此時(shí),對于 vuex 中的 user 模塊 就可以進(jìn)行以下修改了:

this.commit('user/setToken', data.token)
登錄后操作

那么截止到此時(shí),我們距離登錄操作還差最后一個(gè)功能就是 登錄鑒權(quán)

只不過在進(jìn)行 登錄鑒權(quán) 之前我們得先去創(chuàng)建一個(gè)登錄后的頁面,也就是我們所說的登錄后操作。

  1. 創(chuàng)建 layout/index.vue ,寫入以下代碼:

    <template>
      <div class="">Layout 頁面</div>
    </template>
    
    <script setup>
    import {} from 'vue'
    </script>
    
    <style lang="scss" scoped></style>
    
    
  2. router/index 中,指定對應(yīng)路由表:

    const publicRoutes = [
     ...
      {
        path: '/',
        component: () => import('@/layout/index')
      }
    ]
    
  3. 在登錄成功后,完成跳轉(zhuǎn)

    // 登錄后操作
    router.push('/')
    
    登錄鑒權(quán)解決方案

在處理了登陸后操作之后,接下來我們就來看一下最后的一個(gè)功能,也就是 登錄鑒權(quán)

首先我們先去對 登錄鑒權(quán) 進(jìn)行一個(gè)定義,什么是 登錄鑒權(quán) 呢?

  • 當(dāng)用戶未登陸時(shí),不允許進(jìn)入除 login 之外的其他頁面。

  • 用戶登錄后,token 未過期之前,不允許進(jìn)入 login 頁面

而想要實(shí)現(xiàn)這個(gè)功能,那么最好的方式就是通過 路由守衛(wèi) 來進(jìn)行實(shí)現(xiàn)。

那么明確好了 登錄鑒權(quán) 的概念之后,接下來就可以去實(shí)現(xiàn)一下

main.js 平級,創(chuàng)建 permission 文件

import router from './router'
import store from './store'

// 白名單
const whiteList = ['/login']
/**
* 路由前置守衛(wèi)
*/
router.beforeEach(async (to, from, next) => {
// 存在 token ,進(jìn)入主頁
// if (store.state.user.token) {
// 快捷訪問
if (store.getters.token) {
 if (to.path === '/login') {
   next('/')
 } else {
   next()
 }
} else {
 // 沒有token的情況下,可以進(jìn)入白名單
 if (whiteList.indexOf(to.path) > -1) {
   next()
 } else {
   next('/login')
 }
}
})



在此處我們使用到了 vuex 中的 getters ,此時(shí)的 getters 被當(dāng)作 快捷訪問 的形式進(jìn)行訪問

所以我們需要聲明對應(yīng)的模塊,創(chuàng)建 store/getters

const getters = {
token: state => state.user.token
}
export default getters

store/index 中進(jìn)行導(dǎo)入:

import getters from './getters'
export default createStore({
getters,
})

那么到這里我們整個(gè)"通用后臺(tái)登錄方案解析"就算是全部講解完成了。
當(dāng)然,里面的一些代碼,例如網(wǎng)絡(luò)請求的解析,這些就需要你根據(jù)自己公司的接口自行去調(diào)整了,還有登錄鑒權(quán)的一些返回碼,都需要具體問題具體分析,我們這邊只是給出一個(gè)通用的方式,及其思路,但相信你看完整篇文章,這些也不是什么大問題的。加油吧!

  • 下一次,我們繼續(xù)介紹 “# 項(xiàng)目架構(gòu)之搭建Layout架構(gòu) 解決方案與實(shí)現(xiàn)”,下面給個(gè)預(yù)告圖,下次再見吧


    image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,998評論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內(nèi)容