代码地址: https://github.com/yexia553/vue_study/tree/%E9%85%8D%E7%BD%AEvue-router/vue3-notes
这篇笔记的内容有点难,建议多看几遍
笔记中的代码一定要结合github仓库的内容和博客中提到的视频一起看,不然不太容易看懂
这篇笔记中新使用了好几个第三方包,建议新手在看博客内容之前现在项目根目录下执行以下cnpm install 安装这些依赖,以免后面报错
同步发表于个人站点: http://panzhixiang.cn/article/2022/11/15/61.html
很多网站都有登录功能,访问者在页面上输入账号密码之后页面会请求后端API进行认证,如果认证通过会跳转到首页。我们简单地来拆分一下这个过程中具体有那几件事情。
以上就是实现一个登录功能大致的过程。在这个过程中会用到axios来调用API,另外后端的认证用的jwt,还需要vuex来做状态管理,我会在下面分别介绍这三个知识点。
做前端开发的人肯定听说过axios,关于axios的介绍我就不写了,网上有很多相关的内容。
原生的axios在调用API的时候每一次都要写很多代码,我做了一点封装,代码放在src/api/request.js中,
import axios from 'axios'
import config from './config.js'
import Cookies from 'js-cookie'
import { ElMessage } from 'element-plus'
import store from '../store/index.js'
import { useRouter } from 'vue-router';
const NETWORK_ERROR = '网络请求错误,请稍后重试...'
const service = axios.create({
baseURL: config.baseApi,
})
service.interceptors.request.use((req) => {
//可以在请求之前做一些事情
//比如自定义header, jwt-token等等
return req
})
service.interceptors.response.use((res) => {
// 对请求得到的响应做一些处理
if (res.status === 200) {
// 状态码是200表明请求正常,可以返回请求到的数据也可以做一些其他事情
return res
} else {
// 状态码不是200说明请求可能出错
// ElMessage.error(NETWORK_ERROR)
// return Promise.reject(NETWORK_ERROR)
// 这里的封装不完善,直接抛出异常会导致页面不能正常提醒用户,先这样返回,以后修改
return res
}
})
let tokenRefresher = async () => {
let router = useRouter()
let now = new Date().getTime()
if (now - Cookies.get('last_token_refresh_time') > 1000 * 60 * 4) {
let res = await service({
url: '/api/token/refresh/',
method: 'post',
headers: {
'Authorization': `Bearer ${Cookies.get('access_token')}`
},
data: {
refresh: `${Cookies.get('refresh_token')}`
}
})
if (res.status === 200) {
store.commit('setAccessToken', res.data.access)
} else if (res.status === 403) {
// refresh token过期了,要求重新登录
store.commit('clearRefreshToken')
store.commit('clearAccessToken')
router.push({
name: 'login'
})
}
}
}
function request(options) {
options.method = options.method || 'get' // 如果没有传入method这个参数,就默认是get请求
if (options.method.toLowerCase() === 'get') {
// console.log(options)
options.params = options.data
}
// 如果可以从cookie中获取到access_token,就添加到header中
if (Cookies.get('access_token')) {
// 设置token之前先检查是否需要更新token
tokenRefresher()
service.defaults.headers.common['Authorization'] = `Bearer ${Cookies.get('access_token')}`
}
return service(options)
}
export default request
其实这一部分封装的不好,几乎没什么额外的功能,但是整个封装的思路都在里面了,强烈建议结合Vue3中如何封装axios 这个视频一起理解axios封装的部分,在实际工作中还是很有用的,在我接触axios封装之前就是在每一次调用的api的时候写一堆重复的代码,修改布置后确实方便很多。
上面这一段代码中的tokenRefresher这个函数是用来更新access token的,暂时可以先不管,等看完后面的jwt和api部分在回头理解这一部分。
在看下面的内容之前,推荐先完整仔细地看一下下面两篇博客:
可以不用细究里面的细节,了解运行过程即可。
我简单的概括一下jwt在登录这个场景中过程,以便理解后面的内容。
在前后端分离的情况下,前端一般都需要一个方法来管理被调用的API,这样比较便于后期的维护、更新和修改等工作,这里介绍一种我比较喜欢的方法,这个方式使用于中小型的前端项目,调用的API不是非常多的情况。
/**
* 项目的环境配置
*/
// 这是vite的一种使用方式:https://cn.vitejs.dev/guide/env-and-mode.html#env-files
const env = import.meta.env.MDOE || 'dev'
const envConfig = {
dev: {
baseApi: 'http://localhost:8000',
},
test: {
baseApi: 'test.example.com/api',
},
prod: {
baseApi: 'example.com/api',
}
}
export default {
env,
...envConfig[env]
}
不难看出,这个文件是用来做环境管理的,比如第一个dev表示本地的开发环境,test表示线上测试环境,prod表示生产环境,还可以按照自己的需要添加其他的配置。
这个config.js在前面axios的封装中也有用到,大家可以回到上面看一下代码。
import request from "./request.js";
export default {
login(params) {
return request({
url: '/api/token/',
method: 'post',
data: params,
mock: false
})
},
refreshToken(params) {
return request({
url: '/api/token/refresh',
method: 'post',
data: params,
mock: false
})
}
}
api.js里面目前只包含两个接口,一个是login,用于在登录的时候调用,一个是refreshtoken,用于刷新access token, 这个在上面jwt部分介绍过。
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router/index.js'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/main.css'
import api from './api/api.js'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(ElementPlus)
app.mount('#app')
app.config.globalProperties.$api = api
修改的部分就是在import里面引入src/api/api.js;然后在最后一行把api配置到vue的global properties,方便以后引用。
到这里vue中api的集中管理就结束了。
上面做了那么多准备工作,都是为了实现登录页面做的,我们现在来实现它。在src/views目录下创建login文件夹,然后在其中创建Login.vue文件,文件内容如下,主要看代码中的注释。
<template>
<el-form :model="formData" status-icon class="login-container" ref="formRef">
<h3 class="login-title">登陆h3>
<el-form-item label="用户名" prop="username" label-width="80px">
<el-input type="input" auto-complete="off" placeholder="请输入用户名" v-model="formData.username">el-input>
el-form-item>
<el-form-item label="密码" prop="password" label-width="80px">
<el-input type="password" auto-complete="off" placeholder="请输入密码" v-model="formData.password">el-input>
el-form-item>
<el-form-item class="login-submit">
<el-button type="primary" class="login-submit" @click="login()">登录el-button>
el-form-item>
el-form>
template>
<script>
import { getCurrentInstance, reactive } from 'vue';
import { defineComponent } from 'vue-demi';
import { useRouter } from 'vue-router';
import { ElMessageBox } from 'element-plus'; //这是用来在账号密码错误时弹窗提示的
import store from '../../store/index.js'; //这里引入vuex,暂时先忽略,后面会介绍
export default defineComponent({
setup() {
const { proxy } = getCurrentInstance() // 注意这里,下面login函数会用到
const router = useRouter()
// vue3中获取表单数据需要使用reactive
const formData = reactive({
username: '',
password: '',
});
// 使用异步的方式请求api
let login = async () => {
let res = await proxy.$api.login(formData) // 通过$api来调用login
if (res.status === 200) { // 如果返回码是200表明账号密码正确,校验通过
// 下面两行代码是获取后端返回的access token和refresh token并存储起来,方便后面使用
store.commit('setAccessToken', res.data.access)
store.commit('setRefreshToken', res.data.refresh)
store.commit('updateLastRefreshTime') // 更新最近一次刷新access token的时间,用于比较access token是否过期,这里要和jwt的内容联系起来看
router.push({ // 跳转到主页面,
name: 'main'
})
} else {
// 如果账号密码错误的话就要进行提示,并且重新回到登录页面
ElMessageBox.alert('账号密码错误,请重试!')
router.push({
name: 'login'
})
}
};
return {
formData,
login,
}
}
})
script>
<style lang="less" scoped>
.login-container {
border-radius: 15px;
background-clip: padding-box;
margin: 180px auto;
width: 350px;
padding: 35px 35px 15px 35px;
background-color: #fff;
border: 1px solid #eaeaea;
box-shadow: 0 0 25px #cac6c6;
}
.login-title {
margin: 0px auto 40px auto;
text-align: center;
color: #505458;
}
.login-submit {
margin: 10px auto 0 auto;
justify-content: center;
}
style>
上面这一段代码实现了页面登录的样式和功能,但是我们还缺少一个指向这个页面的路由,现在来配置,修改src/router/index.js的内容,修改后如下,
import { createRouter, createWebHashHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'main',
redirect: '/home',
component: () => import('../views/Main.vue'),
children: [
{
path: '/home',
name: 'home',
component: () => import('../views/home/Home.vue'),
},
{
path: '/other',
name: 'other',
component: () => import('../views/other/Other.vue'),
},
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/login/Login.vue')
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router
其实只修改了一个地方,就是在routes中添加了一个一级路由/login, 用于指向登录页面,这个应该也很好理解。
到这里登录页面相关的内容就完成了,但是整个登录功能还不能正常运行,还缺少一个重要部分,vuex。
上一步在Login.vue这个文件中,我们先是引入了import store from '../../store/index.js'
,然后又是存储access token、refresh token和最后更新access token的时间,但是具体是怎么实现的没有介绍,现在来说一下。
vuex网上也是有很多,我贴两篇感觉写的还不错的博客:
大家可以先浏览一下上面这两篇博客,对于vuex有一个整体的了解再继续看下面的内容。
假设大家已经看完了上了上面的两篇博客,我们开始介绍vuex在登录这个场景中应用。
在src/路径下创建store文件夹,然后在里面穿件index.js文件,文件内容如下,
import { createStore } from 'vuex' // 引入createStore, 这里跟vue2不一样
import Cookies from "js-cookie"
export default createStore({
state: {
// state里面定义了一些要使用的变量
access_token: '',
refresh_token: '',
last_token_refresh_time: new Date("October 01, 1975 00:00:00"), // 以1975-10-01 00:00:00 为初始值
},
mutations: {
// mutations里面主要是针对state里面的变量进行一些操作的函数,
// 在登录这个场景中,分别对access token和refresh token有设置(set)、清除(clear)和获取(get)3个操作,一共6个
// 额外还有一个更新access token的函数
// access token 和 refresh token 我们是存储在cookie中,这个很简单,看代码就能理解了,想要深入了解的需要自行搜索
setAccessToken(state, val) {
state.access_token = val
Cookies.set('access_token', val)
},
clearAccessToken(state) {
state.access_token = ''
Cookies.remove('access_token')
},
getAccessToken(state) {
state.access_token = state.access_token || Cookies.get('access_token')
},
setRefreshToken(state, val) {
state.refresh_token = val
Cookies.set('refresh_token', val)
},
clearRefreshToken(state) {
state.refresh_token = ''
Cookies.remove('refresh_token')
},
getRefreshToken(state) {
state.refresh_token = state.refresh_token || Cookies.get('refresh_token')
},
updateLastRefreshTime(state) {
state.last_token_refresh_time = new Date().getTime()
Cookies.set('last_token_refresh_time', state.last_token_refresh_time)
},
}
})
上面的代码展示了怎么样通过vuex来管理和操作token,但是配置好了之后应该怎么调用呢? 其实在Login.vue中已经使用过了,我在这里再解释一下。
import store from '../../store/index.js';
store.commit('setAccessToken', res.data.access)
好了,到这里基本就大功告成了,再项目的根目录下把项目运行起来,然后在浏览器中输入 http://localhost:5173/#/login 就会出现登录页面,账号和密码分别是admin和Pass1234,输入之后点击登录应该就能跳转到首页了。
记住在运行项目之前先安装依赖,执行cnpm install。
虽然到这里登录功能基本完成了,还是还缺一点。
大家有没有发现,到目前为止,虽然登录功能可以工作了,但是访问者依然可以在没有登录的情况下就访问所有的页面,也就是没有起到限制的作用,这个叫做路由守卫或者导航守卫,下面介绍。
路由守卫的内容上面介绍过了,实现起来其实比较简单,在main.js里面配置,main.js修改后内容如下,
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import router from './router/index.js'
import api from './api/api.js'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/main.css'
import store from './store/index.js'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
router.beforeEach((to, from, next) => {
store.commit('getAccessToken')
const token = store.state.access_token
if (!token && to.name !== 'login') {
next({ name: 'login' })
} else if (token && to.name === 'login') {
next({ name: 'home' })
} else (
next()
)
})
app.use(router)
app.use(ElementPlus)
app.mount('#app')
app.config.globalProperties.$api = api
修改了两部分,
import store from './store/index.js'
router.beforeEach((to, from, next) => {
store.commit('getAccessToken')
const token = store.state.access_token
if (!token && to.name !== 'login') {
next({ name: 'login' })
} else if (token && to.name === 'login') {
next({ name: 'home' })
} else (
next()
)
})
这一段代码的作用就是判断access_token是否存在,如果存在就放行,如果不存在就跳转到登录页面。
理解这一段代码的时候要结合Login.vue里面的login这个函数,这个函数在登录校验通过之后运行了 store.commit('setAccessToken', res.data.access)
和 store.commit('setRefreshToken', res.data.refresh)
,这两行代码的作用就是更新(设置)access token 和refresh token,换句话说,登录成功之后,access token一定存在,如果access token不存在就可以认为没有登录或者没有登录成功,所以在路由守卫中可以利用access token来作为判断条件。
到这里,等个登录模块就完全做好了。
其实登录功能本身很简单,但是这个笔记里面讲到了vue3中api的管理,axios的封装,vuex的使用,所以会有点难。
最后留三个思考题: