登录功能
布局处理
页面分为上下两个部分,顶部为导航,底部为登录表单
首先我们创建登录组件,然后配置路由
// views/login/index.vue
<template>
<div class="login">登录功能</div>
</template>
<script>
export default {
name: 'Login'
}
</script>
<style lang="scss" scoped></style>
配置路由,跳转以后再设置
const routes = [
{
path: '/login',
name: 'login',
component: () => import(/* webpackChunkName: 'login' */'@/views/login/index'),
},
...
]
导航栏部分可以使用Vant的NavBar导航栏组件,经过一番处理:
<template>
<div class="login">
<!-- 登录导航栏 -->
<van-nav-bar
title="登录"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
</div>
</template>
<script>
export default {
name: 'Login',
methods: {
onClickLeft () {
// 返回登录之前的那个页面
this.$router.go(-1)
}
}
}
</script>
<style lang="scss" scoped>
</style>
登录表单
使用Vant的form表单组件设置
- van-field为Field输入框组件,代表一个表单项,可以通过rules属性设置方法进行校验
- submit事件仅仅在提交表单并且验证通过之后才能触发
<template>
<div class="login">
<!-- 登录导航栏 -->
<van-nav-bar
title="登录"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<!-- 登录表单 -->
<van-form @submit="onSubmit">
<van-field
v-model="form.phone"
name="phone"
label="手机号"
placeholder="请输入手机号"
:rules="[
{ required: true, message: '请填写手机号' },
{ validator: phoneCheck, message: '格式有误,请重新输入' }
]"
/>
<van-field
v-model="form.password"
type="password"
name="password"
label="密码"
placeholder="6-12位密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^[a-zA-Z0-9]{6,12}$/, message: '格式有误,请重新输入' }
]"
/>
<div style="margin: 16px;">
<van-button round block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
</div>
</template>
<script>
export default {
name: 'Login',
data () {
return {
form: {
phone: '',
password: ''
}
}
},
methods: {
onClickLeft () {
// 返回登录之前的那个页面
this.$router.go(-1)
},
onSubmit () {
console.log('submit')
},
phoneCheck (value) {
return /^1\d{10}$/.test(value)
}
}
}
</script>
<style lang="scss" scoped>
</style>
登录请求
封装接口
创建services/user.js,封装登录接口:地址
- 注意请求参数为urlencoded格式,可以通过qs模块进行处理
- 除了qs模块之外,也可以通过URLSearchParams对象来进行处理
在浏览器中打印一下功能,确认无误
import request from '@/utils/request'
// 用户登录
export const login = data => {
return request({
method: 'POST',
url: '/front/user/login',
data: new URLSearchParams(data).toString()
})
}
引入,登陆成功,提示:
import { login } from '@/services/user'
...
async onSubmit () {
const { data } = await login(this.form)
if (data.state === 1) {
this.$toast.success('登录成功')
} else {
this.$toast.fail('登录失败')
}
},
...
避免重复请求
可以使用Button按钮组件的loading属性进行加载设置
通过loading属性设置按钮为加载状态,加载状态下默认会隐藏按钮文字,可以通过loading-text设置加载状态下的文字
设置给按钮,根据请求状态进行加载,但是光是设置loading请求依然会持续发送,建议再加一个disabled
// login/index.vue
...
<van-button
...
:loading="isLoading"
:disabled="isLoading"
>登 录</van-button>
...
<script>
data () {
return {
...
// 登录按钮加载中状态
isLoading: false
}
},
methods: {
async onSubmit () {
this.isLoading = true
const ...
if (data.state === 1) {
...
}
this.isLoading = false
},
}
</script>
登录状态存储
学习页面和用户页面在登陆前后的访问结果是不一样的,为了能在其他组件中访问登录状态,应该使用Vuex统一管理,同时呢,也要注意将数据设置到本地存储,避免刷新导致重新登录
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 存储登录用户的数据
user: JSON.parse(window.localStorage.getItem('eduMobileUser') || null)
},
mutations: {
// 用于修改user
setUser (state, payload) {
// payload为请求到的用户数据,JSON格式,不方便操作
// 转换为对象存储
state.user = JSON.parse(payload)
// 本地存储
window.localStorage.setItem('eduMobileUser', payload)
}
},
actions: {
},
modules: {
}
})
登录成功后我们要提交mutation存储用户数据
// login/index.vue
...
if (data.state === 1) {
// 将用户信息存储到vuex的state中
this.$store.commit('setUser', data.content)
this.$toast.success('登录成功')
} else {
this.$toast.fail('登录失败')
}
...
身份认证
登录状态检测
用户功能与学习功能是需要登录才能访问的,如果没有登录,就应该跳转到登录页面。这里通过Vue Router的导航守卫处理
// router/index.js
...
import store from '@/store'
...
{
path: '/learn',
name: 'learn',
component: () => import(/* webpackChunkName: 'learn' */'@/views/learn/index'),
meta: { requiresAuth: true }
},
{
path: '/user',
name: 'user',
component: () => import(/* webpackChunkName: 'user' */'@/views/user/index'),
meta: { requiresAuth: true }
}
...
// 导航守卫进行登录检测与跳转
router.beforeEach((to, from, next) => {
// 验证 to 路由是否需要进行身份认证
if (to.matched.some(record => record.meta.requiresAuth)) {
// 验证 Vuex 的 store 中的登录用户信息是否存储
if (!store.state.user) {
// 未登录,跳转到登录页
return next({
name: 'login'
})
}
// 已经登录,允许通过
next()
} else {
next()
}
})
export default router
登录成功跳转到上一次访问的页面
登录后我们应该跳回到上次访问的页面,处理方式:
- 导航守卫跳转login同时传递跳转页的信息
- 登陆成功后根据信息跳转到对应的页面就可以了
// router/index.js
...
router.beforeEach((to, from, next) => {
...
if (!store.state.user) {
return next({
name: 'login',
query: {
// 将本次路由的 fullPath 传递给 login 页面
redirect: to.fullPath
}
})
}
...
login登录成功之后的跳转需要取决于redirect
// login/index.vue
...
async onSubmit () {
...
if (data.state === 1) {
...
// 跳转
this.$router.push(this.$route.query.redirect || '/')
}
...
通过这种方式设置的路由会出现
诸如此类的报错,这个报错并不会影响程序的进行,就是告诉你重定向了,不用刻意去管理
接口鉴权
用户功能需要用户登录才能访问,接口也是一样的,如果不登录就去请求用户功能接口是很不合理,也很不安全的,这类接口进行请求时需要进行接口访问的权限认证,称之为,接口鉴权
在项目中,通Token方式进行接口鉴权,Token数据由服务器产生,在登录成功之后就要响应到客户端
在接口文档中得知,客户端请求接口时,通过请求头的Authorization字段发送access_token来进行身份认证,失败时的状态码为401
通过请求拦截器统一设置Token
由于要进行鉴权的接口有很多,所以我们在request.js中通过Axios的请求拦截器统一处理
步骤:
- 引入store,读取其中的user信息
- 如果user存在并且数据有access_token,就设置给请求头的Authorization字段
// 设置请求拦截器,进行接口鉴权
request.interceptors.request.use(config => {
const { user } = store.state
// 检测user是否存在
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
return config
})
export default request
尝试请求用户信息接口(随便找一个组件测试一下就成),验证之后请求里有access_token就没有问题啦
刷新Token
Token是具有过期时间的,过期之后的token就不能再继续使用,这时候就有两种方法来解决这个问题
- 用户重新登录,获取新的access_token就可以
- 当Token已经过期时,自动刷新Token减少用户登录次数(体验更好)
刷新Token的方式:
- 将登陆成功后响应的refresh_token发送给刷新Token接口获取到新的access_token,再利用新的access_token进行接口鉴权
通过响应拦截器刷新Token
Token过期可能会发生在任意接口操作时,可以通过Axios响应拦截器进行统一的处理。步骤如下:
- 判断是否为Token过期导致状态码为401的
- 获取refresh_token
- 请求刷新Token接口
- 记录刷新状态,避免多个接口重复刷新Token
代码:
import axios from 'axios'
import store from '@/store'
import router from '@/router'
const request = axios.create({
baseURL: 'http://edufront.lagou.com'
})
// 设置请求拦截器,进行接口鉴权
request.interceptors.request.use(config => {
const { user } = store.state
// 检测user是否存在
if (user && user.access_token) {
config.headers.Authorization = user.access_token
}
return config
})
// 封装函数用于跳转登录页
function redirectLogin () {
router.push({
name: 'login',
query: {
redirect: router.currentRoute.fullPath
}
})
}
// 标记token刷新状态
let isRefreshing = false
// 存储刷新时等待的请求
let requests = []
// 响应拦截器
request.interceptors.response.use(response => {
return response
// 接下来处理失败的情况
}, async error => {
// 确认是否存在响应的内容(注意:这里是出错的响应
if (error.response) {
// 检测状态码是不是401
if (error.response.status === 401) {
// 检测是否存在用户的登录信息
if (!store.state.user) {
redirectLogin()
// 如果不存在直接结束,把错误向后抛出
return Promise.reject(error)
}
// 发送请求前检测,是否已经存在刷新 token 的请求了
if (isRefreshing) {
return requests.push(() => {
request(error.config)
})
}
isRefreshing = true
// 这里是存在登录信息,发送请求,尝试刷新token
const { data } = await request({
method: 'POST',
url: '/front/user/refresh_token',
data: new window.URLSearchParams({
refreshtoken: store.state.user.refresh_token
}).toString()
})
if (data.state !== 1) {
// 刷新token失败
store.commit('setUser', null)
redirectLogin()
return Promise.reject(error)
}
// 刷新token成功
store.commit('setUser', data.content)
// 重新发送所有的挂起请求 requests
requests.forEach(callback => callback())
requests = []
isRefreshing = false
return request(error.config)
}
}
return Promise.reject(error)
})
export default request