登录功能

登录流程

  • 登录业务流程
    • 在登录页面输入用户名和密码
    • 调用后台接口进行验证
    • 通过验证之后,根据后台的响应状态跳转到项目主页
  • 登录业务的相关技术点
    • http是无状态的
    • 通过cookie在客户端记录状态
    • 通过session在服务器端记录状态
    • 通过token方式维持状态【存在跨域推荐使用token】
  • token原理分析

启动项目

  • 进入vue_shop项目目录,查看项目状态
1
git status

  • 我们开发一个新功能时,最后好创建一个分支,然后合并到主分支master
1
2
3
4
# 创建login分支并切换
git checkout -b login
# 查看所有分支
git branch

  • 在Vue可视化界面中【任务】可以运行【serve】查看当前项目

  • 修改App.vue和路由规则。最好关掉代码校验,创建vue.congig.js文件
1
2
3
module.exports = {
lintOnSave: false
}

登录路由

  • components下创建Login.vue
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>登录组件</div>
</template>

<script>
export default {
name: "Login",
};
</script>

<style scoped lang="less">
</style>
  • 然后在router/index.js中配置路由规则,/也重定向到/login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "../components/Login";

Vue.use(VueRouter);

const router = new VueRouter({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login }
]
});

export default router;
  • App.vue中使用路由占位符
1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div id="app">
<!--路由占位符-->
<router-view></router-view>
</div>
</template>

<script>
export default {
name: "app",
};
</script>

绘制视图

  • 首先创建assets/css/global.css,编写全局样式表
1
2
3
4
5
6
7
8
/* 全局样式表 */
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
  • main.js中引入即可生效
1
2
// 引入全局样式
import './assets/css/global.css'
  • 使用到less预编译语言需要下载less-loader
1
2
npm uninstall less-loader
npm i less-loader@7.3.0
  • 编写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
<template>
<div class="login_container">
<div class="login_box">
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
</div>
</div>
</template>

<script>
export default {
name: "Login",
};
</script>

<style scoped lang="less">
.login_container {
background-color: #2b4b6b;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
/* 居中展示 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
position: absolute;
left: 50%;
transform:translate(-50%, -50%);
background-color: #fff;
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color:#eee;
}
}
</style>

组件库

1
2
3
4
5
6
7
import Vue from 'vue'
import { Button } from 'element-ui'
import {Form,FormItem,Input} from 'element-ui'
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItem)
Vue.use(Input)
  • 引入第三方图标库,下载资源存放在assets目录下,然后在main.js引入
1
import './assets/fonts/iconfont.css'
  • 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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form label-width="0px" class="login_form">
<!--用户名-->
<el-form-item>
<el-input prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!--密码-->
<el-form-item>
<el-input prefix-icon="iconfont icon-3702mima"></el-input>
</el-form-item>
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
};
</script>

<style scoped lang="less">
.login_container {
background-color: #2b4b6b;
height: 100%;
}
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
/* 居中展示 */
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.avatar_box {
height: 130px;
width: 130px;
border: 1px solid #eee;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
}
.login_form {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 20px;
box-sizing: border-box;
}
.btns {
display: flex;
justify-content: flex-end;
}
</style>

功能实现

表单数据绑定

  • 首先需要对表单数据进行绑定,绑定步骤如下
    • 给表单绑定数据对象:model="form"
    • data中定义form数据对象
    • 每一个输入框双向绑定数据v-model="form.password"
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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form :model="form" label-width="0px" class="login_form">
<!--用户名-->
<el-form-item>
<el-input v-model="form.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!--密码-->
<el-form-item>
<el-input v-model="form.password" prefix-icon="iconfont icon-3702mima" type="password"></el-input>
</el-form-item>
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
// 绑定表单数据
form: {
username:'',
password:'',
}
}
}
};
</script>

表单数据校验

  • 表单数据校验功能实现步骤
    • 给表单绑定数据规则对象:rules="rules"
    • data中定义rules验证规则
    • 给每个表单项绑定prop="xxx"指定规则
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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form :model="form" :rules="rules" ref="form" label-width="0px" class="login_form">
<!--用户名-->
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!--密码-->
<el-form-item prop="password">
<el-input v-model="form.password" prefix-icon="iconfont icon-3702mima" type="password"></el-input>
</el-form-item>
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
// 绑定表单数据
form: {
username:'',
password:'',
},
// 表单验证规则对象
rules: {
username:[
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 10, message: '长度在 3 到 10 个字符', trigger: 'blur' }
],
password:[
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
],
}
}
}
};
</script>

表单数据重置

  • 通过ref="xxx",拿到表单的实例对象,然后调用resetFields函数即可
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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form :model="form" :rules="rules" ref="formRef" label-width="0px" class="login_form">
<!--用户名-->
<!--密码-->
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary">登录</el-button>
<el-button type="info" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
///
},
methods: {
// 点击重置按钮重置表单
resetForm(){
this.$refs.formRef.resetFields()
}
}
};
</script>

表单数据预校验

  • 表单数据预校验即表单项全部校验通过才能提交表单,发送请求
  • 通过ref="xxx",拿到表单的实例对象,然后调用validate函数即可
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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form :model="form" :rules="rules" ref="formRef" label-width="0px" class="login_form">
<!--用户名-->
<!--密码-->
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary" @click="submitForm">登录</el-button>
<el-button type="info" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
///
}
},
methods: {
// 点击重置按钮重置表单
resetForm(){
this.$refs.formRef.resetFields()
},
submitForm(){
// 接收一个回调函数
this.$refs.formRef.validate(valid =>{
console.log(valid) // 布尔值
})
}
}
};
</script>

发送Axios

  • 首先需要在main.js中引入并挂载
1
2
3
4
5
import axios from 'axios'
// 配置请求根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
// 挂载到VM的原型对象上,通过$http就可以发送请求
Vue.prototype.$http = axios
  • 使用到了Message弹窗组件,在element.js中导入
1
2
3
import {Message} from 'element-ui'
// 挂载到VM原型对象上
Vue.prototype.$message = Message
  • 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
<template>
<div class="login_container">
<div class="login_box">
<!--头像区域-->
<div class="avatar_box">
<img src="../assets/logo.png" alt="" />
</div>
<!--登录表单区域-->
<el-form :model="form" :rules="rules" ref="formRef" label-width="0px" class="login_form">
<!--用户名-->
<!--密码-->
<!--按钮区域-->
<el-form-item class="btns">
<el-button type="primary" @click="submitForm">登录</el-button>
<el-button type="info" @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>

<script>
export default {
name: "Login",
data(){
return{
//
}
},
methods: {
// 点击重置按钮重置表单
resetForm(){
this.$refs.formRef.resetFields()
},
submitForm(){
// 接收一个回调函数
this.$refs.formRef.validate(async valid =>{
// 为false 不发送请求
if(!valid) return;
// 传入表单数据对象form,因为响应数据是Promise对象,需要await修饰成普通对象
// 且await只适用于async修饰的方法
// 只使用里面的data数据,使用解构赋值获取data,并别名为res
const {data: res} = await this.$http.post('login',this.form)
if(res.meta.status !== 200 ) return this.$message.error('登录失败')
this.$message.success('登录成功')
})
}
}
};
</script>

权限拦截

  • 将登录成功之后响应数据里的token,保存到客户端的sessionStorage
    • 项目中除了登录之外的其他API接口,必须在登录之后才能访问
    • token 只应在当前网站打开期间生效,所以将token保存在sessionStorage
  • 通过编程式导航跳转到后台主页,路由地址是/home
1
2
3
4
5
6
7
8
9
10
11
12
13
14
submitForm(){
// 接收一个回调函数
this.$refs.formRef.validate(async valid =>{
// 为false 不发送请求
if(!valid) return;
const {data: res} = await this.$http.post('login',this.form)
if(res.meta.status !== 200 ) return this.$message.error('登录失败')
this.$message.success('登录成功')
// 1. 将登录成功之后的`token`,保存到客户端的`sessionStorage` 中
window.sessionStorage.setItem('token',res.data.token)
// 2. 通过编程式导航跳转到后台主页,路由地址是`/home`
this.$router.push('/home')
})
}
  • 创建Home.vue组件,配置路由
1
2
3
4
5
6
7
8
9
10
11
import Home from "../components/Home";
Vue.use(VueRouter);

const router = new VueRouter({
routes: [
//...
{ path: "/home", component: Home }
]
});

export default router;
  • 未登录状态,即sessionStorage 中没有token时,主页不能访问。需要使用路由守卫进行权限控制。在路由配置文件中配置
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
import Vue from "vue";
import VueRouter from "vue-router";
import Login from "../components/Login";
import Home from "../components/Home";
Vue.use(VueRouter);

const router = new VueRouter({
routes: [
{ path: "/", redirect: "/login" },
{ path: "/login", component: Login },
{ path: "/home", component: Home }
]
});
// 挂载路由导航守卫
router.beforeEach((to,from,next) => {
// to 将要访问的路径
// from 从哪个路径跳转而来
// next 函数,表示放行
if(to.path === '/login') return next();
// 获取token
const tokenStr = window.sessionStorage.getItem('token')
// 未登录,强制跳转到登录页
if(!tokenStr) return next('/login')
// 已登录,放行
next()
})
export default router;

注销功能

注销实现

  • 基于token的方式实现退出比较简单,只需要销毁本地的token即可。这样,后续的请求就不会携带token,必须重新登录生成一个新的token之后才可以访问页面
  • 在主页组件中编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div>
<el-button type="info" @click="logout">退出</el-button>
</div>
</template>

<script>
export default {
name:'Home',
methods:{
logout(){
// 清空token
window.sessionStorage.clear();
// 跳转到登录页
this.$router.push("/login")
}
}
}
</script>

ESLint问题

  • ESLint语法检查会由于不合法出现语法警告。我们可以创建配置文件,在编辑器格式化后会自动修改语法,比如双引号格式化为单引号
  • 根目录下创建.prettierrc文件,内容如下
1
2
3
4
5
6
{
// 格式化后不额外添加分号
"semi": false,
// 单引号替换双引号
"singleQuote": true
}

提交到码云

  • 我们登录注销功能已经实现了,需要上传到码云中,首先查看状态
1
git status
  • 然后添加到缓存区并提交
1
2
3
git add .
git status
git commit -m "完成了登录注销功能"
  • 刚刚提交的都到了login分支,需要把分支合并,推送到远程仓库
1
2
3
4
5
6
7
8
# 查看分支 *表示当前分支
git branch
# 切换到master主分支
git checkout master
# 合并login分支
git merge login
# 远程推送
git push
  • 其实云端只有一个master分支,那怎么把本地的·login分支也推送到云端呢
1
2
3
4
5
6
# 切换到login分支
git checkout login
# 查看分支
git branch
# 推送到云端login分支
git push -u origin login