Vue3入门笔记七----登录功能

这个系列的笔记重点会放在怎么样利用Vue3把项目架设起来并跟后端API互动,不会介绍Vue的基础特性,关于Vue的基础特性可以参考这个视频四个小时带你快速入门Vue,我是看这个入门的,觉得还不错。

代码地址: https://github.com/yexia553/vue_study/tree/%E9%85%8D%E7%BD%AEvue-router/vue3-notes

这篇笔记的内容有点难,建议多看几遍

笔记中的代码一定要结合github仓库的内容和博客中提到的视频一起看,不然不太容易看懂

这篇笔记中新使用了好几个第三方包,建议新手在看博客内容之前现在项目根目录下执行以下cnpm install 安装这些依赖,以免后面报错

登录功能介绍

很多网站都有登录功能,访问者在页面上输入账号密码之后页面会请求后端API进行认证,如果认证通过会跳转到首页。我们简单地来拆分一下这个过程中具体有那几件事情。

  1. 首先要有一个登录页面让访问者输入账号密码,并且要有登录按钮
  2. 后端要有一个认证相关的API用来给页面调用以检查访问者提供的账号密码是否正确
  3. 上一步的认证检查通过之后,前端会获得一个token,这个token标志着访问者是合法的,需要把这个token存储起来,以便在后面请求后端的时候使用

以上就是实现一个登录功能大致的过程。在这个过程中会用到axios来调用API,另外后端的认证用的jwt,还需要vuex来做状态管理,我会在下面分别介绍这三个知识点。

axios请求API和axios的封装

做前端开发的人肯定听说过axios,关于axios的介绍我就不写了,网上有很多相关的内容。

原生的axios在调用API的时候每一次都要写很多代码,我做了一点封装,代码放在src/api/request.js中,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

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的介绍和在登录功能中的应用

在看下面的内容之前,推荐先完整仔细地看一下下面两篇博客:

  1. JSON Web Token 入门教程 —- 阮一峰
  2. JWT 介绍 - Step by Step

可以不用细究里面的细节,了解运行过程即可。

我简单的概括一下jwt在登录这个场景中过程,以便理解后面的内容。

  1. 访问者在页面上输入账号密码并点击登录之后前端会请求后端的/api/token/这个api,如果认证通过,后端会给前端返回一个access token和一个refresh token
  2. access token是用来访问后端api的,所以在之后的请求中都要携带这个token才能正常访问api;refresh token是用来刷新access token的,因为一般来说,access token的有效期很多,比如django 的DRF框架中,默认情况下,access token的有效期只有5分钟,但是refresh token的有效期是8个小时
  3. 前端拿到access token和refresh token 需要找一个地方把它们存储起来,以便后面使用,比如可以存在cookie中,或者是localstorage中等
  4. 要有一个检查机制,检查access token有没有过期,如果过期了就要及时用refresh token进行更新,获取新的access token,并且还要更新在上一步存存储的access token

vue3中api的集中管理

在前后端分离的情况下,前端一般都需要一个方法来管理被调用的API,这样比较便于后期的维护、更新和修改等工作,这里介绍一种我比较喜欢的方法,这个方式使用于中小型的前端项目,调用的API不是非常多的情况。

  1. 首先在src/api目录下新建一个config.js文件,内容如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * 项目的环境配置
 */
// 这是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的封装中也有用到,大家可以回到上面看一下代码。

  1. 然后在src/api目录下创建一个api.js文件,这个文件就是用来放置所有会被调用的api的地方,代码如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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部分介绍过。

  1. 创建了api.js之后,还需要把它跟vue绑定起来,这样才能调用。
    修改src/main.js这个文件,修改后如下,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

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文件,文件内容如下,主要看代码中的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<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">
<!-- 这里通过@click绑定login函数,用于点击的时候触发登录功能 -->
            <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的内容,修改后如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

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。

通过vuex管理token(状态管理)

上一步在Login.vue这个文件中,我们先是引入了import store from '../../store/index.js' ,然后又是存储access token、refresh token和最后更新access token的时间,但是具体是怎么实现的没有介绍,现在来说一下。

vuex网上也是有很多,我贴两篇感觉写的还不错的博客:

  1. vuex-介绍
  2. vuex是什么

大家可以先浏览一下上面这两篇博客,对于vuex有一个整体的了解再继续看下面的内容。

假设大家已经看完了上了上面的两篇博客,我们开始介绍vuex在登录这个场景中应用。

在src/路径下创建store文件夹,然后在里面穿件index.js文件,文件内容如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

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中已经使用过了,我在这里再解释一下。

  1. 首先要引入,可以看Login.vue里面import部分的最后一行,也就是 import store from '../../store/index.js';
  2. 通过store.commit(函数名,参数)的方法调用,比如 store.commit('setAccessToken', res.data.access)

好了,到这里基本就大功告成了,再项目的根目录下把项目运行起来,然后在浏览器中输入 http://localhost:5173/#/login 就会出现登录页面,账号和密码分别是admin和Pass1234,输入之后点击登录应该就能跳转到首页了。

记住在运行项目之前先安装依赖,执行cnpm install。

虽然到这里登录功能基本完成了,还是还缺一点。
大家有没有发现,到目前为止,虽然登录功能可以工作了,但是访问者依然可以在没有登录的情况下就访问所有的页面,也就是没有起到限制的作用,这个叫做路由守卫或者导航守卫,下面介绍。

路由守卫

路由守卫的内容上面介绍过了,实现起来其实比较简单,在main.js里面配置,main.js修改后内容如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

修改了两部分,

  1. 引入store
    1
    import store from './store/index.js'
  2. 配置路由守卫
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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的使用,所以会有点难。

思考题

最后留三个思考题:

  1. 现在的登录页面在输入账号密码之后一定要鼠标点击登录按钮才能登录,在输入框内按回车键是不行的,可以考虑一下怎么实现
  2. 加上登录模块之后,访问者一定要登录之后才能访问,但是也有的系统是允许匿名访问的,只有在做一些特定的操作的时候才需要登录,这个要怎么实现 ?
  3. 当前系统,所有访问者在登录之后能够访问的页面是一样的,但是项目中经常会需要根据访问者的权限不同开放不同页面,这个要怎么实现 ?