笔记来源:拉勾教育 - 大前端就业集训营
文章内容:学习过程中的笔记、感悟、和经验
登录用户功能
登录模块
未登录点击学习和我时会跳转到登录页面让用户登录之后才能访问
导航布局
顶部导航栏使用Vant中的导航栏组件
这里返回按钮可以使用this.$router.go(-1)方法处理
src/views/login/index.vue 添加导航栏模块,处理返回按钮
<template>
<div class="login">
<!-- 顶部导航栏 -->
<van-nav-bar
title="标题"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
</div>
</template>
<script>
export default {
name: 'login',
methods: {
// 导航栏返回事件函数
onClickLeft () {
// 使用 go 方法实现返回上一页
this.$router.go(-1)
}
}
}
</script>
表单布局
登录表单可以使用Vant的表单组件
可以直接把校验规则写在结构内部
src/views/login/index.vue 添加登陆表单,设置校验规则
<template>
<div class="login">
<!-- 顶部导航栏 -->
<van-nav-bar
title="标题"
left-text="返回"
left-arrow
@click-left="onClickLeft"
/>
<!-- 登陆表单 rules为校验规则,只有全部规则满足才能触发 submit -->
<van-form @submit="onSubmit">
<!-- 手机号 -->
<van-field
v-model="form.username"
name="手机号"
label="手机号"
placeholder="手机号"
:rules="[
{ required: true, message: '请填写手机号码' },
{ pattern: /^1\d{10}$/, message: '手机号格式错误,请重新输入' }
]"
/>
<!-- 密码框 -->
<van-field
v-model="form.password"
type="password"
name="密码"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^[0-9a-zA-Z]{6,12}$/, message: '密码格式错误,请输入6-12位密码' }
]"
/>
<!-- 登陆按钮 -->
<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: {}
}
},
methods: {
// 导航栏返回事件函数
onClickLeft () {
// 使用 go 方法实现返回上一页
this.$router.go(-1)
},
// 表单登陆事件函数
onSubmit () {
console.log('登陆啦')
}
}
}
</script>
接口封装
使用登录接口
移动端可以使用new URLSearchParams(data).toString()将对象参数转换为url格式
URLSearchParams可以处理URL字符串,新功能
添加提示信息提示是否登陆成功
// src/api/user.js 新建文件封装用户相关接口
// 引入axios实例
import axios from './axios'
// 用户登录
export const login = data => {
return axios({
method: 'post',
url: '/front/user/login',
data: new URLSearchParams(data).toString()
})
}
src/views/login/index.vue 使用用户登陆接口
..................
<script>
// 引入接口
import { login } from '@/api/user'
export default {
...................
methods: {
....................
// 表单登陆事件函数
async onSubmit () {
// 调用接口
const { data } = await login(this.form)
if (data.state === 1) {
// 如果接口请求成功弹出提示(暂时先不存储数据)
this.$toast.success('登陆成功')
} else {
this.$toast.fail('登录失败')
}
}
}
}
</script>
避免重复请求
利用按钮组件额的加载状态,避免短时间重复请求
src/views/login/index.vue 给登录按钮添加loading属性,绑定数据
<van-button :loading="loading" round block type="info" native-type="submit">提交</van-button>
登录请求接口之前设置为true,登录无论是否成功都设置回false
存储登录状态
使用VueX功能存储登录状态
登陆成功后将返回的数据利用vuex的功能添加进去(注意转换为对象)
将数据存在本地,避免刷新后丢失数据
- 写入:window.localStorage.setItem(名字,数据)
- 读取:window.localStorage.getItem('名字')
// src/views/login/index.vue 存储用户登录信息
// 表单登陆事件函数
async onSubmit () {
// 请求接口之前让登录按钮处于加载状态
this.loading = true
// 调用接口
const { data } = await login(this.form)
if (data.state === 1) {
// 登录成功调用vuex方法存储登录信息!!!!!!!!!!!!!!!!!!
this.$store.commit('setUser', data.content)
// 如果接口请求成功弹出提示(暂时先不存储数据)
this.$toast.success('登陆成功')
} else {
this.$toast.fail('登录失败')
}
// 请求完成后不管成功还是失败,都需要将按钮取消加载状态
this.loading = false
}
// src/store/index.js vuex添加用户信息属性,设置方法作本地数据持久化
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
// 用户登录信息,读取本地持久化数据,并且转换为对象格式
user: JSON.parse(window.localStorage.getItem('lagou-edu-mobile'))
},
mutations: {
// 存储用户登录信息
setUser (state, data) {
// 设置user,否则user不刷新不会自动读取localStorage
state.user = JSON.parse(data)
// 将登录心心做本地持久化
window.localStorage.setItem('lagou-edu-mobile', data)
}
},
actions: {
},
modules: {
}
})
登录状态监测
访问需要登录的页面需要校验是否登录,使用导航守卫,添加路由元信息meta: {requiresAuth: true}
判断访问页面是否需要要验证to.marched.some(record => record.meta.requiresAuth)判断元数据属性是否有这个属性
需要验证判断是否已经登陆,否则跳转login
在跳转登陆之前记录一下信息,登录又跳转回指定页面
src/router/index.js 给路由增加元信息,并且设置导航守卫
................
{ // 学习页面
name: 'study',
path: '/study',
component: () => import(/* webpackChunkName: 'study' */'@/views/study'),
// 添加路由元信息,设置需要判断是否登录的标记
meta: { login: true }
},
{ // 用户页面
name: 'user',
path: '/user',
component: () => import(/* webpackChunkName: 'user' */'@/views/user'),
// 添加路由元信息,设置需要判断是否登录的标记
meta: { login: true }
},
...........
// 导航守卫
router.beforeEach((to, from, next) => {
// 判断路由的元信息中是否有需要校验是否登录的标记
if (to.matched.some(record => record.meta.login)) {
// 如果需要判断再判断是否登录
if (!Store.state.user) {
// 未登录跳转到登陆页,并且把原本要访问的路由带上
next({
path: '/login',
query: { path: to.fullPath }
})
} else {
// 已经登陆直接放行
next()
}
} else {
// 不需要判断的直接放行
next()
}
})
通过请求拦截器设置token
在发送请求之前添加token,使用vuex数据
如果有user并且有token则将token设置给请求头
// src/api/axios.js 设置请求拦截器,再请求之前添加请求头
// 引入axios插件
import axiosPlug from 'axios'
// 引入vuex
import Store from '@/store'
// 创建axios实例
const axios = axiosPlug.create({
// 设置url前缀
baseURL: 'http://edufront.lagou.com'
})
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 设置请求头,判断是否登录,登录就带上请求头
if (Store.state.user) config.headers.Authorization = Store.state.user.access_token || ''
// 返回新的配置信息
return config
}, function (error) {
// 抛出错误
return Promise.reject(error)
})
// 导出实例
export default axios
刷新token
判断当前token是否已经过期,所以我们需要刷新token
设置相应拦截器,成功不需要处理,所以我们需要设置失败的处理函数
判断是否返回了401,如果返回401说明未授权,而未授权又分为两种情况
- 根本没有登录:直接抛出错误
- token过期:更新token - 使用刷新token接口,判断是否能刷新,刷新成功把新数据更新,如果请求失败直接清除本地存储
存在多个请求的情况需要(添加刷新标记)将多个请求存储起来,刷新token后一起发送,请求信息存在error.config中,别忘了触发token刷新的请求
未登录和无法刷新token可以直接跳转回login,记得带参数
// src/api/axios.js 设置请求拦截器,刷新token或者返回登录
// 引入axios插件
import axiosPlug from 'axios'
// 引入vuex
import Store from '@/store'
// 引入router,后面会有跳转
import router from '@/router'
// 创建axios实例
const axios = axiosPlug.create({
// 设置url前缀
baseURL: 'http://edufront.lagou.com'
})
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 设置请求头,判断是否登录,登录就带上请求头
if (Store.state.user) config.headers.Authorization = Store.state.user.access_token || ''
// 返回新的配置信息
return config
}, function (error) {
// 抛出错误
return Promise.reject(error)
})
// 信号值,是否正在刷新token
let uploading = false
// 除刷新token的请求外,其他请求全部暂时存储起来
let ajax = []
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 响应成功什么都不做
return response
}, async error => {
// 如果有响应
if (error.response) {
// 判断返回状态码是否是401
if (error.response.status === 401) {
// 判断用户是否登录
if (!Store.state.user) {
// 用户未登录,直接带参数返回登录
router.push({
name: 'login',
query: {
path: router.currentRoute.fullPath
}
})
console.log('用户未登录,返回登陆')
return Promise.reject(error)
}
// 判断是否正在刷新token
if (uploading) {
// 正在刷新token将剩下的请求全部挂起保存起来
return ajax.push(() => {
axios(error.config)
})
}
// 没刷新token则更改状态为刷新
uploading = true
// 尝试刷新token
const { data } = await axios({
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)
// 带参数返回登录
router.push({
name: 'login',
query: {
path: router.currentRoute.fullPath
}
})
console.log('更新失败')
return Promise.reject(error)
}
// 刷新token成功
console.log('更新成功')
// 保存新的登录信息
Store.commit('setUser', data.content)
// 将之前挂起的请求重新发送
ajax.forEach(fn => fn())
// 清空请求
ajax = []
// 修改状态为未刷新
uploading = false
// 最后重新发送本次请求
return axios(error.config)
}
}
return Promise.reject(error)
})
// 导出实例
export default axios
用户模块
用户信息处理
使用公共导航栏组建
顶部整体可以 使用Vant的单元格组件,只需要修改内部内容即可
// src/api/user.js 添加获取用户信息接口
// 获取用户基本信息
export const getInfo = () => {
return axios({
method: 'get',
url: '/front/user/getInfo'
})
}
src/views/user/index.vue 使用公共组件,设置顶部用户信息
<template>
<div>
<van-cell-group>
<!-- 顶部用户信息单元格,绑定数据 -->
<van-cell class="user-info">
<!-- 头像 -->
<van-image
round
width="50px"
height="50px"
:src="info.portrait"
/>
<!-- 文字信息 -->
<div class="user-info-text">
<h3>{{info.userName}}</h3>
<p><van-icon name="edit"/> 编辑个人资料</p>
</div>
</van-cell>
</van-cell-group>
<!-- 使用子组件 -->
<foot-bar></foot-bar>
</div>
</template>
<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
name: 'User',
data () {
return {
// 用户信息数据
info: {}
}
},
created () {
// 初始化数据
this.getUserInfo()
},
methods: {
// 调用接口获取用户数据
async getUserInfo () {
const { data } = await getInfo()
// 把获取到的数据写进data
this.info = data.content
}
},
// 组件注册
components: {
FootBar
}
}
</script>
<style lang="scss" scoped>
// 顶部用户信息
.user-info{
padding: 0;
.van-cell__value {
display: flex;
padding: 30px 20px 0;
background: #f89704;
.user-info-text {
flex: 1;
margin-left: 15px;
h3 {
margin: 5px;
font-size: 18px;
}
p {
margin: 5px;
}
}
}
}
</style>
账户信息处理
使用Vant的宫格组件,依然写在vant单元格里面
src/views/user/index.vue 添加账户信息
<template>
<div>
<van-cell-group>
<!-- 顶部用户信息单元格,绑定数据 -->
<van-cell class="user-info">
<!-- 头像 -->
<van-image
round
width="50px"
height="50px"
:src="info.portrait"
/>
<!-- 文字信息 -->
<div class="user-info-text">
<h3>{{info.userName}}</h3>
<p><van-icon name="edit"/> 编辑个人资料</p>
</div>
</van-cell>
<van-cell class="user-account">
<!-- 账户信息 - 宫格组件 -->
<van-grid>
<!-- 使用v-for创建结构 -->
<van-grid-item v-for="item in account" :key="item.id">
<p class="num">{{item.num}}</p>
<p>{{item.title}}</p>
</van-grid-item>
</van-grid>
</van-cell>
</van-cell-group>
<!-- 使用子组件 -->
<foot-bar></foot-bar>
</div>
</template>
<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
name: 'User',
data () {
return {
// 用户信息数据
info: {},
// 账户信息数据
account: [
{ id: 0, num: 14.05, title: '学习时长' },
{ id: 1, num: 200, title: '钱包/勾豆' },
{ id: 2, num: 1, title: '优惠券' },
{ id: 3, num: 213, title: '学分' }
]
}
},
created () {
// 初始化数据
this.getUserInfo()
},
methods: {
// 调用接口获取用户数据
async getUserInfo () {
const { data } = await getInfo()
// const { data2 } = await getInfo()
// const { data3 } = await getInfo()
// const { data4 } = await getInfo()
// return data + data2 + data3 + data4
// 把获取到的数据写进data
this.info = data.content
}
},
// 组件注册
components: {
FootBar
}
}
</script>
<style lang="scss" scoped>
// 顶部用户信息
.user-info{
padding: 0;
.van-cell__value {
display: flex;
padding: 30px 20px 0;
background: #f89704;
.user-info-text {
flex: 1;
margin-left: 15px;
h3 {
margin: 5px;
font-size: 18px;
}
p {
margin: 5px;
}
}
}
}
// 账户信息样式
.user-account {
margin-top: -1px;
padding: 10px 16px;
background: #f89704;
.van-grid {
border-radius: 10px;
overflow: hidden;
p {
margin: 0;
}
.num {
font-size: 22px;
font-weight: 700;
}
}
}
</style>
底部菜单列表
直接使用单元格组件罗列起来即可
src/views/user/index.vue 书写底部列表结构,使用v-for
<template>
<div>
<van-cell-group>
<!-- 顶部用户信息单元格,绑定数据 -->
<van-cell class="user-info">
<!-- 头像 -->
<van-image
round
width="50px"
height="50px"
:src="info.portrait"
/>
<!-- 文字信息 -->
<div class="user-info-text">
<h3>{{info.userName}}</h3>
<p><van-icon name="edit"/> 编辑个人资料</p>
</div>
</van-cell>
<van-cell class="user-account">
<!-- 账户信息 - 宫格组件 -->
<van-grid>
<!-- 使用v-for创建结构 -->
<van-grid-item v-for="item in account" :key="item.id">
<p class="num">{{item.num}}</p>
<p>{{item.title}}</p>
</van-grid-item>
</van-grid>
</van-cell>
<!-- 底部列表,使用v-for创建结构 -->
<van-cell v-for="item in list" :key="item.id" :icon="item.icon" :value="item.value" :title="item.title" is-link />
</van-cell-group>
<!-- 使用子组件 -->
<foot-bar></foot-bar>
</div>
</template>
<script>
// 引入接口
import { getInfo } from '@/api/user'
// 引入底部导航栏组件
import FootBar from '../../common/foot-bar.vue'
export default {
name: 'User',
data () {
return {
// 用户信息数据
info: {},
// 账户信息数据
account: [
{ id: 0, num: 14.05, title: '学习时长' },
{ id: 1, num: 200, title: '钱包/勾豆' },
{ id: 2, num: 1, title: '优惠券' },
{ id: 3, num: 213, title: '学分' }
],
// 底部列表数据
list: [
{ id: 0, icon: 'location-o', value: '收益200元', title: '分销中心' },
{ id: 1, icon: 'location-o', value: '', title: '个性化设置' },
{ id: 2, icon: 'location-o', value: '', title: '我的下载' },
{ id: 3, icon: 'location-o', value: '', title: '帮助与反馈' },
{ id: 4, icon: 'location-o', value: 'v2.0.0', title: '关于拉勾教育' }
]
}
},
created () {
// 初始化数据
this.getUserInfo()
},
methods: {
// 调用接口获取用户数据
async getUserInfo () {
const { data } = await getInfo()
// const { data2 } = await getInfo()
// const { data3 } = await getInfo()
// const { data4 } = await getInfo()
// return data + data2 + data3 + data4
// 把获取到的数据写进data
this.info = data.content
}
},
// 组件注册
components: {
FootBar
}
}
</script>
<style lang="scss" scoped>
// 顶部用户信息
.user-info{
padding: 0;
.van-cell__value {
display: flex;
padding: 30px 20px 0;
background: #f89704;
.user-info-text {
flex: 1;
margin-left: 15px;
h3 {
margin: 5px;
font-size: 18px;
}
p {
margin: 5px;
}
}
}
}
// 账户信息样式
.user-account {
margin-top: -1px;
padding: 10px 16px;
background: #f89704;
.van-grid {
border-radius: 10px;
overflow: hidden;
p {
margin: 0;
}
.num {
font-size: 22px;
font-weight: 700;
}
}
}
</style>
封装接口与数据绑定
使用用户基本信息接口,获取然后绑定数据
上面已经进行了绑定,这里不用再写了