3.登录/退出功能
3.1登录概述
3.1-1.登录业务流程
①在登录页面输入用户名和密码
②调用后台接口进行验证
③通过验证之后,根据后台的响应状态跳转到项目主页
3.1-2.登录业务的相关技术点
- http 是无状态的(需要记录用户的登录状态)
- 通过 cookie 在客户端记录状态
- 通过 session 在服务器端记录状态
- 通过 token 方式维持状态
3.2登录–token原理分析
1.如果前端和后台接口之间不存在跨域问题,推荐使用Cookie或者session记录登录状态。
2.如果前端和后台接口存在跨域问题,则使用token。
分析:token原理
现在有客户端和服务器
服务器:专门提供API接口。如登录接口
客户端:需要通过ajax请求,访问服务器上的数据
目前:客户端与服务器之间存在跨域问题
token原理:
1.客户端在登录页面输入用户名和密码,把这次请求提交到服务器。
2.服务器根据用户的这次请求验证用户是否存在。
如果登录成功之后,服务器向客户端返回一个唯一的token值。
3.客户端需要把服务器返回的token值,记录到客户端本地。
4.今后如果客户端要请求服务器的API接口,必须携带token才能够正常的进行操作。
如果客户端在发起下一次请求的时候,携带了token。
5.服务器接收到token之后,验证token是否存在:如果存在证明用户已登录
然后会根据携带的token验证是哪个用户,从而根据用户的操作返回不同的结果。
注意事项:
1.token值是由服务器生成,而且每个不同的用户生成的token值也是不一样的。
2.token值记录了用户的登录状态
3.token:客户端与服务器端进行身份校验
3.3登录功能实现
1.登录页面的布局
通过Element–UI组件实现布局
- el-form
- el-form-item
- el-input
- el-button
- 字体图标
找到自己的项目 vue_shop
打开vscode,在项目根目录vue_shop运行:git status
注意:在开发中只有开发新功能了,尽量把功能放在新的分支上进行开发;分支开发完成以后再把这个分支合并到master分支上
1.创建分支命令:git checkout -b 分支名
2.在项目根目录,运行:git checkout -b login
3.查看所有分支,运行:git branch 分支前存在* 表示当前处于这个分支 *login
梳理vue项目的结构,删除不必要的内容
- 1.App.vue根组件
<template>
<div id="app">
APP根组件
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
</style>
- 2.路由 router.js
import Vue from 'vue' 指的是引入vue.js文件
import Router from 'vue-router'
Vue.use(Router) Vue.use() 明确地安装路由功能
export default new Router({
routes: [
]
})
- 3.删除views文件夹
- 4.删除components/HelloWorld.vue文件
功能:创建登录组件,通过路由的形式渲染到App根组件,添加路由重定向redirect
- 1.在components/新建登录组件:Login.vue
<template>
<div>
登录组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
</style>
注意:
1.lang="less" 支持less语法格式
2.scoped是vue的指令。控制组件样式生效的区间
如果加上scoped,样式只在当前组件内生效
如果去掉scoped,样式会全局生效
一个组件它的样式是来美化自己组件结构的,不应该影响其他的组件
所以:只要定义的是单文件组件,一定为style标签加上scoped指令
避免组件之间的样式冲突
- 2.在路由 router.js 导入路由–添加路由规则
import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login.vue'
Vue.use(Router)
export default new Router({
routes: [{
path: '/',
redirect: '/login'
}, {
path: '/login',
component: Login
}]
})
- 3.在根组件 App.vue,添加路由占位符
<template>
<div id="app">
<!-- 路由占位符 -->
<router-view></router-view>
</div>
</template>
功能:完成登录组件的基本页面布局:全屏背景色–登录的盒子
实现全屏背景色
- 1.在src/assets/新建css文件夹/新建global.css
html,
body,
#app {
height: 100%;
margin: 0;
padding: 0;
}
- 2.在入口文件main.js中导入全局样式表:import ‘./assets/css/global.css’
- 3.在Login.vue中,为style标签添加 height: 100%;
<style lang="less" scoped>
.login_container{
height: 100%;
background-color: #2b4b6b;
}
</style>
登录的盒子 login_box
在Login.vue文件中:
<template>
<div class="login_container">
<div class="login_box">
</div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
.login_container{
height: 100%;
background-color: #2b4b6b;
}
.login_box{
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%)
}
</style>
功能:绘制图像区域 avatar_box
<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 {
}
</script>
<style lang="less" scoped>
.login_container{
height: 100%;
background-color: #2b4b6b;
}
.login_box{
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
top: 50%;
left: 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>
功能:登录的表单区域
打开Element-UI官网,粘贴对应的html结构
因为我们的组件是按需导入的,所以如果用到form表单、input输入框等
- 1.需要先在src/plugins/element.js,引入相关的组件
// 按需导入
import { Button } from 'element-ui'
import { Form, FormItem } from 'element-ui'
import { Input } from 'element-ui'
// 注册全局组件
Vue.use(Button)
Vue.use(Form)
Vue.use(FormItemon)
Vue.use(Input)
- 2.在Login.vue文件中: Form表单–Button按钮–Input输入框
<template>
<div class="login_container">
<div class="login_box">
<!-- 头像区域 -->
<div class="avatar_box">
<img src="../assets/logo.png" alt="">
</div>
<!--登录表单区域 -->
<el-form class="login_form">
<!-- 用户名区域 -->
<el-form-item>
<el-input></el-input>
</el-form-item>
<!-- 密码区域 -->
<el-form-item>
<el-input></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 {
}
</script>
<style lang="less" scoped>
.login_container{
height: 100%;
background-color: #2b4b6b;
}
.login_box{
width: 450px;
height: 300px;
background-color: #fff;
border-radius: 3px;
position: absolute;
top: 50%;
left: 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>
功能:绘制密码和小图标
- 1.在Element-UI官网查找:输入框–带 icon 的输入框
<el-input prefix-icon="el-icon-search"></el-input> el-icon-search 需要被替换为 iconfont icon-user
因为Element-UI官网找不到需要的图标,所以借助第三方图标库。
下载好之后,将字体图标文件fonts,放在assets文件夹里面 assets/fonts
- 2.然后在main.js导入
// 导入字体图标
import './assets/fonts/iconfont.css'
- 3.在Login.vue中,iconfont 图标名
<el-input prefix-icon="iconfont icon-user"></el-input> 用户头像
<el-input prefix-icon="iconfont icon-3702mima"></el-input> 用户密码锁
功能:实现表单的数据绑定(element文档提供的做法)
- 1.为el-form标签添加model 属性绑定到数据对象form :model = ‘form’
- 2.为每一个表单项,通过v-model绑定到数据对象上的属性中
<template>
<div class="login_container">
<div class="login_box">
<!-- 头像区域 -->
<div class="avatar_box">
<img src="../assets/logo.png" alt="">
</div>
<!--登录表单区域 -->
<el-form :model="loginForm" class="login_form">
<!-- 用户名区域 -->
<el-form-item>
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user">
</el-input>
</el-form-item>
<!-- 密码区域 -->
<el-form-item>
<el-input v-model="loginForm.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 {
data(){
return {
// 这是登录表单的数据绑定对象
loginForm: {
username: 'wuddong',
password: '123'
}
}
}
}
</script>
功能:表单的数据验证(这是element官网文档提供的做法)
- 为el-form标签通过
绑定rules属性
,指定校验对象(自定义):rules="loginFormRules"
- 在data数据中定义校验对象:
loginFormRules
,其中每一个属性就是一个验证规则(数组) - 为不同的表单el-form-item项,通过
prop指定不同的验证规则
进行表单验证
在Login.vue文件中:
第一步:<el-form :model="loginForm" :rules="loginFormRules" class="login_form"></el-form>
第二步:在data数据中
// 这是表单的验证规则对象
(required: true 表示必填项;trigger: 'blur' 表示鼠标离开进行验证)
loginFormRules:{
// 验证用户名是否合法
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
// 验证密码是否合法
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
]
}
第三步:
用户名区域 <el-form-item prop="username"></el-form-item>
密码区域 <el-form-item prop="password"></el-form-item>
功能:表单的重置—点击重置按钮,重置表单的效验–resetFields
Vue直接操作DOM
1.通过ref标注DOM元素
// 在DOM元素上通过ref属性标注,属性名称自定义 <div ref="info">hello</div>
2.通过$refs获取DOM元素
// 通过Vue实例的$refs获取标记ref属性的元素
let info = this.$refs.info.innerHTML
console.log(info) // hello
方法:查询官方文档form–Form Methods 拿到表单的实例对象Form,调用resetFields函数
。
-
为表单el-form添加
ref引用
,ref引用指定的值就是表单的引用对象loginFormRef
-
为重置按钮添加事件 @click=“resetLoginForm”,触发resetLoginForm事件处理函数
-
在resetLoginForm事件处理函数中,通过
this.$refs.loginFormRef.resetFields()
在Login.vue文件中
第一步:登录表单区域 <el-form ref="loginFormRef"> </el-form>
第二步:<el-button type="info" @click="resetLoginForm">重置</el-button>
第三步:在script标签定义methods方法
methods:{
// 点击重置按钮,重置登录表单
resetLoginForm(){
console.log(this);
//结果是组件的实例对象,里面有个属性$refs
this.$refs.loginFormRef.resetFields();
}
}
功能:实现登录前表单数据的预验证–validate
方法:查询官方文档form–Form Methods 拿到表单的引用对象,调用validate函数
。
-
获取表单的引用对象
loginFormRef
-
拿着引用对象
loginFormRef
,调用validate函数进行表单预校验;在validate中它接收一个回调函数,第一个形参是验证的结果:布尔值
第一步:为登录按钮添加点击事件 <el-button type="primary" @click="login">登录</el-button> 第二步:在script标签的methods方法中 login(){ this.$refs.loginFormRef.validate(valid=>{ console.log(valid); }) }
功能:配置axios发起登录请求
1.如果要请求服务器:
- 启动MySql服务器(Phpstudy服务器)
- 找到素材/vue-api-server/app.js,在窗口启动:node app.js
2.在main.js中:配置axios (重点)
```js
// 导入axios包
import axios from 'axios'
// 配置请求的根路径
axios.defaults.baseURL = 'http://127.0.0.1:8888/api/private/v1/'
// 把axios包挂载到vue原型对象上:这样每一个vue组件都可以通过this直接访问到$http,从而去发起ajax请求
Vue.prototype.$http = axios
3.根据验证的结果valid,判断是否发起登录请求
如果请求成功,Login.vue
- 请求方式:post
- 请求地址:login
- 请求参数:this.loginForm
login(){
this.$refs.loginFormRef.validate(async valid=>{
console.log(valid);
//valid是布尔值:true 或者 false
//******************内容如下:
if(!valid) return; 不发起请求
const {data: res} = await this.$http.post("login",this.loginForm);
console.log(res);
// {data: null, meta: {…}}
if(res.meta.status !== 200) return console.log('登录失败');
console.log('登录成功');
})
}
}
因为post请求的结果是promise实例对象,所以在函数使用 await async
这样结果是:服务器返回的数据是一个对象:{data: {…}, status: 200, statusText: "OK", headers: {…}, config: {…}, …}
**es6语法:取对象里面的属性值**
我们只需要里面的data对象,所以结构复制出data属性重命名为res对象{data: res} res就是data对应的属性值
然后根据res的meta属性,判断是否登录成功 res.meta.status !== 200
功能:配置Message全局弹框组件
需求:登录成功或者失败,给用户友好提示
配置方法:
- 从element-ui中导入弹框组件
import { Message } from 'element-ui'
- 把弹框组件挂载到vue的原型对象上
Vue.prototype.$message = Message
- 调用$message提供的方法
查看官方文档:Message消息提示
在plugins/element.js
中,导入弹框提示组件:
// 导入弹框提示组件
import { Message } from 'element-ui'
Vue.prototype.$message = Message
注意:
1.$message是自定义属性
2.Vue.prototype.$message = Message
表示把弹框组件挂载到vue的原型对象上,这样每一个组件都可以通过this访问到$message
然后就可以弹框提示了
同时$message身上提供了一些弹框方法error('失败')、success('成功')
我们只需要调用这些方法,提示需要的信息就可以了。
在Login.vue
中,调用这些方法:
登录成功:this.$message.error('登录失败')
登录失败:this.$message.success('登录成功')
2.实现登录
①通过 axios 调用登录验证接口
②登录成功之后保持用户 token 信息
③跳转到项目主页
在Login.vue中:
this.$message.success('登录成功')
// 1.将登录成功之后的token,保存到客户端的sessionStorage中
// localstorage 是持久化的存储机制
// sessionStorage 是会话期间的存储机制
// 1.1 项目中除了登录之外的其他API接口,必须在登录之后才能访问
// 1.2 token只应在当前网站打开期间生效,所以将token保存在sessionStorage中
第一步: window.sessionStorage.setItem("token",res.data.token);
// 2.通过编程式导航跳转到后台主页,路由地址是 /home
第二步: this.$router.push('/home');
然后在components文件夹下,新建Home.vue
<template>
<div>
Home 组件
</div>
</template>
<script>
export default {
}
</script>
<style lang="less" scoped>
</style>
接着在router.js导入home组件,添加路由规则
// 导入home组件
import Home from './components/Home.vue'
// 添加路由规则
{ path: '/home',component: Home }
3.路由导航守卫控制访问权限
需求:/home页面属于有权限的页面
- 只有用户在登录的情况下才允许访问,如果是未登录状态是不允许看到home页面的
- 如果用户没有登录,希望用户从home路径直接跳转到登录页
如果用户没有登录,但是直接通过URL访问特定页面,需要重新导航到登录页面
**在router.js中,添加路由导航守卫 router.beforeEach **
// 为路由对象,添加 beforeEach 函数(导航守卫)
to表示将要访问的页面路径;
from表示从哪个路径跳转到哪个路径
next表示放行的函数
const router = new Router({})
router.beforeEach((to, from, next) => {
// 如果用户访问的登录页,直接放行允许访问登录页
if (to.path === '/login') return next()
// 如果访问的不是登录页,判断sessionStorage 中是否存在 token 值
const tokenStr = window.sessionStorage.getItem('token')
// 没有token,证明你没有登录,强制跳转到登录页
if (!tokenStr) return next('/login')
// 经过判断用户有token值,直接放行
next()
})
export default router
**通过用户访问的地址以及用户是否有token值,来决定用户具体访问的是哪个页面。**
退出功能实现原理
基于 token 的方式实现退出比较简单,只需要销毁本地的 token ,利用编程式导航–重新返回登录页面。
这样,后续的请求就不会携带 token ,必须重新登录生成一个新的 token 之后才可以访问页面。
在Home.vue组件中:
<template>
<div>
第一步:<el-button type="info" @click="logout">退出</el-button>
</div>
</template>
<script>
export default {
第二步:methods:{
logout(){
window.sessionStorage.clear();
this.$router.push("/login");
}
}
}
</script>
<style lang="less" scoped>
</style>
登录页面 login.vue
完整代码示例:
<template>
<dir class="login_container">
<div class="login_box">
<!-- 头部图片区域 -->
<div class="avator_box">
<img src="../assets/logo.png" alt />
</div>
<!-- 表单区域 -->
<el-form class="login_form" :model="loginForm" :rules="loginFormRules" ref="loginFormRef">
<!-- 用户名 -->
<el-form-item prop="username">
<el-input v-model="loginForm.username" prefix-icon="iconfont icon-user"></el-input>
</el-form-item>
<!-- 密码 -->
<el-form-item prop="username">
<el-input
type="password"
v-model="loginForm.password"
prefix-icon="iconfont icon-3702mima"
></el-input>
</el-form-item>
<!-- 按钮 -->
<el-form-item class="btns">
<el-button type="primary" @click="login">登录</el-button>
<el-button type="info" @click="resetLoginForm">重置</el-button>
</el-form-item>
</el-form>
</div>
</dir>
</template>
<script>
export default {
data () {
return {
// 表单的数据绑定
loginForm: {
username: 'admin',
password: '123456'
},
// 表单的数据验证
loginFormRules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
]
}
}
},
methods: {
// 重置按钮
resetLoginForm () {
console.log(this)
this.$refs.loginFormRef.resetFields()
},
// 登录按钮
login () {
// 实现登录前表单的预验证
this.$refs.loginFormRef.validate(async valid => {
console.log(valid)
if (!valid) return
// 验证通过--执行登录操作
const { data: res } = await this.$http.post('/login', this.loginForm)
console.log(res)
if (res.meta.status !== 200) return this.$message.error('登录失败')
this.$message.success('登录成功')
// 登录成功:保存token、跳转主页
window.sessionStorage.setItem('token', res.data.token)
this.$router.push('/home')
})
}
}
}
</script>
<style lang="less" scoped>
.login_container {
height: 100%;
background-color: #006695;
.login_box {
width: 450px;
height: 300px;
background-color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.avator_box {
width: 130px;
height: 130px;
border: 1px solid #ccc;
border-radius: 50%;
padding: 10px;
box-shadow: 0 0 10px #ddd;
background-color: #fff;
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
img {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #eee;
}
}
}
.login_form {
position: absolute;
bottom: 0;
width: 100%;
padding: 0 10px;
box-sizing: border-box;
.btns {
display: flex;
justify-content: flex-end;
}
}
}
</style>