在 views
中 login
文件夾,創(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搭建完成后,效果就是這樣的
處理完了基本UI表單操作之后,接下來就是登錄操作的實(shí)現(xiàn)了。
對于登錄操作在后臺(tái)項(xiàng)目中是一個(gè)通用的解決方案,具體可以分為以下幾點(diǎn):
- 封裝
axios
模塊 - 封裝 接口請求 模塊
- 封裝登錄請求動(dòng)作
- 保存服務(wù)端返回的
token
- 登錄鑒權(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
來說,它具備三種不同的模式:
development
test
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è)文件:
.env.development
.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)作我們期望把它封裝到 vuex
的 action
中
在 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ì)分為兩種:
- 本地緩存:
LocalStorage
- 全局狀態(tài)管理:
Vuex
保存在 LocalStorage
是為了方便實(shí)現(xiàn) 自動(dòng)登錄功能
保存在 vuex
中是為了后面在其他位置進(jìn)行使用
那么下面我們就分別來實(shí)現(xiàn)對應(yīng)的緩存方案:
LocalStorage:
-
創(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() }
-
在
vuex
的user
模塊下,處理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() }) ... }) } } }
-
處理保存的過程中,需要?jiǎng)?chuàng)建
constant
常量目錄constant/index.js
export const TOKEN = 'token'
此時(shí),當(dāng)點(diǎn)擊登陸時(shí),即可把 token
保存至 vuex
與 localStorage
中
響應(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è)登錄后的頁面,也就是我們所說的登錄后操作。
-
創(chuàng)建
layout/index.vue
,寫入以下代碼:<template> <div class="">Layout 頁面</div> </template> <script setup> import {} from 'vue' </script> <style lang="scss" scoped></style>
-
在
router/index
中,指定對應(yīng)路由表:const publicRoutes = [ ... { path: '/', component: () => import('@/layout/index') } ]
-
在登錄成功后,完成跳轉(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