我们要实现的登录界面
目前的登录界面
复制下面的代码到views/login/index.vue页面下
<template>
<div class="login-container">
<div class="logo"/>
<div class="form">
<h1>登录</h1>
<el-card shadow="never" class="login-card">
<!--登录表单-->
</el-card>
</div>
</div>
</template>
<script>
export default {
name: 'Login'
}
</script>
<style lang="scss">
.login-container {
display: flex;
align-items: stretch;
height: 100vh;
.logo {
flex: 3;
background: rgba(38, 72, 176) url(../../assets/common/login_back.png) no-repeat center / cover;
border-top-right-radius: 60px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
padding: 0 100px;
.icon {
background: url(../../assets/common/logo.png) no-repeat 70px center /
contain;
width: 300px;
height: 50px;
margin-bottom: 50px;
}
p {
color: #fff;
font-size: 18px;
margin-top: 20px;
width: 300px;
text-align: center;
}
}
.form {
flex: 2;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 176px;
.el-card {
border: none;
padding: 0;
}
h1 {
padding-left: 20px;
font-size: 24px;
}
.el-input {
width: 350px;
height: 44px;
.el-input__inner {
background: #f4f5fb;
}
}
.el-checkbox {
color: #606266;
}
}
}
</style>
保存并看一下页面结构,并没有登录表单,然后下面写一下登录表单
<template>
<div class="login-container">
<div class="logo"/>
<div class="form">
<h1>登录</h1>
<el-card shadow="never" class="login-card">
<!--登录表单-->
<!--el-form element-ui的表单-->
<!--el-form-item 表单中的一项-->
<!--el-input 输入框-->
<el-form>
<el-form-item>
<el-input placeholder="请输入手机号"></el-input>
</el-form-item>
<el-form-item>
<el-input placeholder="请输入密码"></el-input>
</el-form-item>
<!--勾选框-->
<el-form-item>
<el-checkbox>用户平台使用协议</el-checkbox>
</el-form-item>
<!--登录按钮
设置一下宽度 style="width: 350px"
-->
<el-form-item>
<el-button type="primary" style="width: 350px">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</div>
</template>
<script>
export default {
name: 'Login'
}
</script>
<style lang="scss">
.login-container {
display: flex;
align-items: stretch;
height: 100vh;
.logo {
flex: 3;
background: rgba(38, 72, 176) url(../../assets/common/login_back.png) no-repeat center / cover;
border-top-right-radius: 60px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: center;
padding: 0 100px;
.icon {
background: url(../../assets/common/logo.png) no-repeat 70px center /
contain;
width: 300px;
height: 50px;
margin-bottom: 50px;
}
p {
color: #fff;
font-size: 18px;
margin-top: 20px;
width: 300px;
text-align: center;
}
}
.form {
flex: 2;
display: flex;
flex-direction: column;
justify-content: center;
padding-left: 176px;
.el-card {
border: none;
padding: 0;
}
h1 {
padding-left: 20px;
font-size: 24px;
}
.el-input {
width: 350px;
height: 44px;
.el-input__inner {
background: #f4f5fb;
}
}
.el-checkbox {
color: #606266;
}
}
}
</style>
我们使用了很多Element-ui的组件
官网:Element-ui组件官网
接下来我们的登录流程将会按照下图进行设计
比如校验手机号的格式是否正确,密码是否符合位数
13.Vue - ref属性、props配置、mixin混入、插件、scoped样式
在element-ui中其实就是如下所示的组件,进行表单验证
我们可以通过el-form标签的ref属性获取form表单的整体实例,进而进行整体的校验
el-form标签中的:rules="rulesFrom"属性其实就是我们的校验规则,并且校验规则都会写在rulesFrom里
el-form标签中的:model="ruleForm"属性其实就是我们所绑定的数据从ruleForm数据结构中来
el-form-item标签汇总的prop="test1"属性,并且test1字段是ruleForm数据结构中的一个属性
el-input标签做一个v-model双向绑定,这样的话就能做回显了
<template>
<div class="login-container">
<div class="logo"/>
<div class="form">
<h1>登录</h1>
<el-card shadow="never" class="login-card">
<!--登录表单-->
<!--el-form element-ui的表单-->
<!--el-form-item 表单中的一项-->
<!--el-input 输入框-->
<el-form ref="loginFormRef" :model="loginFromModel" :rules="loginFromRules">
<el-form-item prop="mobile">
<el-input placeholder="请输入手机号" v-model="loginFromModel.mobile"></el-input>
</el-form-item>
<el-form-item prop="password">
<!-- show-password是el-input属性,表示不展示input框中信息-->
<el-input show-password placeholder="请输入密码" v-model="loginFromModel.password"></el-input>
</el-form-item>
<!--勾选框-->
<el-form-item prop="isAgree">
<el-checkbox v-model="loginFromModel.isAgree">用户平台使用协议</el-checkbox>
</el-form-item>
<!--登录按钮
设置一下宽度 style="width: 350px"
-->
<el-form-item prop="isAgree">
<el-button @click="login" type="primary" style="width: 350px">登录</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</div>
</template>
export default {
name: 'Login',
data() {
return {
// 登录表单数据结构
loginFromModel: {
mobile: '',
password: '',
isAgree: false
},
// 登录表单规则,数据字段和loginFromModel一样,只不过值是数组的模式
// 数组中的每一个对象值就是一个校验规则
loginFromRules: {
// 手机号校验规则
mobile: [
/**
* required: true 必填
* trigger触发模式
* change表示只要我们不断的写就会触发校验
* blur 失去焦点才会触发校验
*/
{ required: true, message: '请输入手机号信息', trigger: 'blur' },
// 正则表达式检验规则
{ pattern: /^1[3-9]\d{9}$/, message: '手机号输入格式不合规范', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
// 密码长度校验规则
{ min: 6, max: 16, message: '密码长度在 6 到 16 个字符', trigger: 'blur' }
],
/**
* 说明:required命令检测不了false,只能检测null/undefine/空字符串
* 所以我们要自定义校验方式,其实是自定义校验方式是一个函数
* 需要三个参数rule,value,callback
* rule:校验规则
* value:要校验的值
* callback: 一个必须要执行的函数(无论校验成功还是失败都要执行)
* 如果校验成功的话直接执行callback()函数,如果失败的话执行callback(new Error(错误信息))
*/
isAgree: [
{
validator: (rule, value, callback) => {
value ? callback() : callback(new Error('您必须勾选用户平台使用协议'))
}
}
]
}
}
},
methods: {
login() {
/**
* 通过表单的ref属性获取表单实例对象,并且执行此表单实例对象的validate方法校验一下
* validate方法可以传入一个回调函数,并且会传入一个参数,我们可以命名为isOK
*/
this.$refs.loginFormRef.validate((isOK) => {
if (!isOK) {
// 此时isOK为false,表示校验没有通过
alert('校验没有通过,请检查您的登录信息')
return
}
// 此时isOK为true,表示校验通过了
})
}
}
}
</script>
效果展示
我们在登录表单校验之后并不是直接调用请求到后台验证用户名和密码是否正确
而是前端又调用了VuexAction,之后会再去调用登录接口并返回一个token,我们要把此token进行缓存并使用vuex进行共享,共享之后相当于我们的登录成功了,然后跳转到主页
使用了vuex表示一定有数据要去做共享,在这里我们要共享用户的token,之后在每个页面我们都能拿到用户的token了
Vuex基础知识
vuex中的用户模块如下图所示:
// State理解为数据源,像极了我们之前学的data,用来存放数据
const state = {
token: null
}
// Mutations类似java中的数据层,只对数据进行操作,不对业务操作(比如数据加减乘除)
const mutations = {
// 修改token
setToken(state, token) {
state.token = token
// console.log(state.token)
}
}
/**
* actions似java中的业务逻辑层,对逻辑操作,然后向mutations发送数据,在这个业务逻辑中也可以互相调用
* actions可以做异步操作
*/
const actions = {
/**
* 参数1: context上下文对象,有commit和dispatch方法
* 参数2:我们要传入的参数
*/
login(context, data) {
console.log(context)
console.log(data)
// TODO 调用登录接口(代办)
// 假设登录成功之后就会返回一个token,我们要将此token实现共享就要存储在state中
// vuex中想修改state中数据必须通过mutations,不能直接修改state中属性
context.commit('setToken', '123456')
}
}
// 对三个变量进行一个默认的导出
export default {
// 开启命名空间。表示之后调用state/mutations/actions时必须带上模块名称
namespaced: true,
state,
mutations,
actions
}
但是现在有一个问题,当我们刷新页面之后,state.token的值会消失,所以我们要实现vuex持久化
这样就算我们给state.token赋值之后,但刷新页面后仍然是null,所以此时永远不能取到我们缓存的token值
// State理解为数据源,像极了我们之前学的data,用来存放数据
const state = {
token: null
}
所以我们要实现vuex的数据持久化,我们要从缓存中读取state.token的初始值
编写一个工具类
// 我们使用了一个cookie的包,其实和localStorage都可以实现前端缓存数据
import Cookies from 'js-cookie'
const TokenKey = 'vue_admin_template_token'
// 按需导出了三个函数
// 获取token
export function getToken() {
return Cookies.get(TokenKey)
}
// 设置token
export function setToken(token) {
return Cookies.set(TokenKey, token)
}
// 移除token
export function removeToken() {
return Cookies.remove(TokenKey)
}
在vuex用户模块user.js中进行引用
// @符号表示根路径
import { getToken, setToken, removeToken } from '@/utils/auth'
const state = {
// 这样就算我们给state.token赋值之后,但刷新页面后仍然是null,所以此时永远不能取到我们缓存的token值
// token: null
// 从缓存中读取初始值
token: getToken()
}
const mutations = {
// 修改token
setToken(state, token) {
state.token = token
// console.log(state.token)
// 将token值同步到缓存
setToken(token)
},
removeToken() {
// 删除vuex的token
state.token = null
// 删除缓存中的token
removeToken()
}
}
我们要在登录组件中进行调用vuex
methods: {
login() {
/**
* 通过表单的ref属性获取表单实例对象,并且执行此表单实例对象的validate方法校验一下
* validate方法可以传入一个回调函数,并且会传入一个参数,我们可以命名为isOK
*/
this.$refs.loginFormRef.validate((isOK) => {
if (!isOK) {
// 此时isOK为false,表示校验没有通过
alert('校验没有通过,请检查您的登录信息')
return
}
/**
* 此时isOK为true,表示校验通过了,调用vuex中的用户模块
* 此时因为我们开启了命名空间,传参时要带上模块名,即模块名/actions中方法名
* dispatch方法是调用vuex中的actions
*/
this.$store.dispatch('user/login', this.loginFromModel)
})
}
}
效果如下图所示
编写登录接口
// 引入封装的axios工具
import request from '@/utils/request'
// 按需暴露一个登录方法
export function login(Data) {
// request发送登录请求,会得到一个promise结果并将其返回
return request({
// 请求地址
url: '/sys/login',
// 请求方式
method: 'POST',
// 请求参数
data: Data
// 在ES6中上面data: Data可以简写为 data
})
}
methods: {
login() {
/**
* 通过表单的ref属性获取表单实例对象,并且执行此表单实例对象的validate方法校验一下
* validate方法可以传入一个回调函数,并且会传入一个参数,我们可以命名为isOK
*/
this.$refs.loginFormRef.validate((isOK) => {
if (!isOK) {
// 此时isOK为false,表示校验没有通过
alert('校验没有通过,请检查您的登录信息')
return
}
/**
* 此时isOK为true,表示校验通过了,调用vuex中的用户模块
* 此时因为我们开启了命名空间,传参时要带上模块名,即模块名/actions中方法名
* dispatch方法是调用vuex中的actions
*/
this.$store.dispatch('user/login', this.loginFromModel)
})
}
}
超级管理员电话号:138 0000 0002
超级管理员密码:hm#qd@23!
登录密码:网络安全需要,密码以由原来的123456 变更为hm#qd@23! , 管理员不可修改密码,新建用户的密码仍为123456
点击登录接口
请求信息
响应信息
成功存储了浏览器缓存:cookie
刷新之后进入了主页面
补充:为了方便,登录表单的数据我们可以做出如下所示的修改
data() {
return {
// 登录表单数据结构
loginFromModel: {
mobile: process.env.NODE_ENV === 'development' ? '13800000002' : '',
password: process.env.NODE_ENV === 'development' ? 'hm#qd@23!' : '',
isAgree: isAgree: process.env.NODE_ENV === 'development'
},
}
当用户登录成功后直接跳转到主页面,而不是等到用户手动刷新之后才跳转到主页面
此功能是登录的最后一个功能
修改登录页面的代码
methods: {
login() {
/**
* 通过表单的ref属性获取表单实例对象,并且执行此表单实例对象的validate方法校验一下
* validate方法可以传入一个回调函数,并且会传入一个参数,我们可以命名为isOK
*/
this.$refs.loginFormRef.validate(async(isOK) => {
if (!isOK) {
// 此时isOK为false,表示校验没有通过
alert('校验没有通过,请检查您的登录信息')
return
}
/**
* 此时isOK为true,表示校验通过了,调用vuex中的用户模块
* 此时因为我们开启了命名空间,传参时要带上模块名,即模块名/actions中方法名
* dispatch方法是调用vuex中的actions
* Vuex中的action返回的是一个promise,说明是一个异步,我们必须等到promise执行成功才能继续跳转到主页
* 只要是用了await表示下方的代码一定是promise执行成功了
* await 一定要写,因为我们要等待登录成功,如果失败的话就走axios中的reject
*/
await this.$store.dispatch('user/login', this.loginFromModel)
// 登录成功后跳转到主页面
// this.$router获取到路由的实例对象,push表示我们要跳转到哪里,'/'表示我们的主页
this.$router.push('/')
})
}
}
我们可以看一下路由的配置
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/**
* Layout @/在vue中代表路径别名
* @ 符号表示当前目录的src
* @/ 表示src下的layout,而layout又是一个目录,所以会拉取index.vue文件
* 即index.vue组件就是我们的路由组件,会实现二级路由
* */
import Layout from '@/layout'
export const constantRoutes = [
{
path: '/login',
component: () => import('@/views/login/index'),
hidden: true
},
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index'),
meta: { title: 'Dashboard', icon: 'dashboard' }
}]
},
{
path: '/example',
component: Layout,
redirect: '/example/table',
name: 'Example',
meta: { title: 'Example', icon: 'el-icon-s-help' },
children: [
{
path: 'table',
name: 'Table',
component: () => import('@/views/table/index'),
meta: { title: 'Table', icon: 'table' }
},
{
path: 'tree',
name: 'Tree',
component: () => import('@/views/tree/index'),
meta: { title: 'Tree', icon: 'tree' }
}
]
},
{
path: '/form',
component: Layout,
children: [
{
path: 'index',
name: 'Form',
component: () => import('@/views/form/index'),
meta: { title: 'Form', icon: 'form' }
}
]
},
{
path: '/nested',
component: Layout,
redirect: '/nested/menu1',
name: 'Nested',
meta: {
title: 'Nested',
icon: 'nested'
},
children: [
{
path: 'menu1',
component: () => import('@/views/nested/menu1/index'), // Parent router-view
name: 'Menu1',
meta: { title: 'Menu1' },
children: [
{
path: 'menu1-1',
component: () => import('@/views/nested/menu1/menu1-1'),
name: 'Menu1-1',
meta: { title: 'Menu1-1' }
},
{
path: 'menu1-2',
component: () => import('@/views/nested/menu1/menu1-2'),
name: 'Menu1-2',
meta: { title: 'Menu1-2' },
children: [
{
path: 'menu1-2-1',
component: () => import('@/views/nested/menu1/menu1-2/menu1-2-1'),
name: 'Menu1-2-1',
meta: { title: 'Menu1-2-1' }
},
{
path: 'menu1-2-2',
component: () => import('@/views/nested/menu1/menu1-2/menu1-2-2'),
name: 'Menu1-2-2',
meta: { title: 'Menu1-2-2' }
}
]
},
{
path: 'menu1-3',
component: () => import('@/views/nested/menu1/menu1-3'),
name: 'Menu1-3',
meta: { title: 'Menu1-3' }
}
]
},
{
path: 'menu2',
component: () => import('@/views/nested/menu2/index'),
name: 'Menu2',
meta: { title: 'menu2' }
}
]
},
{
path: 'external-link',
component: Layout,
children: [
{
path: 'https://panjiachen.github.io/vue-element-admin-site/#/',
meta: { title: 'External Link', icon: 'link' }
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router
跳转后如下如图所示
下面仅仅是解决开发环境中的跨域问题,生产环境的跨域问题后面会详细解析
如果跨域问题不解决的话,我们的请求是发送不过去的
Vue - 前端代理服务器解决跨域问题
我们前端页面的地址是http://localhost:9528,后端项目的地址是https://heimahr.itheima.net/,一眼可以看出前后端地址不同源
浏览器同源策略:协议、主机、端口三者一致。目前来说都不一致,如果后端不开启cors的话,前端是请求不到后端去的
解决方案:配置vue-cli代理服务器
如果是node向后端接口服务发送请求的话不会发生跨域问题,因为同源策略是浏览器的策略,并不是node的策略
相当于前端向后端请求的中间新加了一个vue-cli(node)
修改vue.config.js文件,并且修改后记得重启项目
module.exports = {
.....
// 此属性中存放项目的一些配置,可以在这个位置写代理的一些属性
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
// before: require('./mock/mock-server.js'), 基础模板做的模拟数据
// 代理属性
proxy: {
// path:目标服务器,想要代理到哪里
'/api': {
// 要代理到的目标服务器
target: 'https://heimahr.itheima.net/'
}
}
},
....
}
基地址:axios中有一个baseurl的属性,表示我痛序偶axios设置了一个基础的地址,剩下的所有请求都不要再拼接这个地址了
超时时间:请求超时时间,比如设置为10s,但是我们前端发起请求而服务器10s内没有做出响应,则表示请求失败
axios中还有两个比较重要的拦截器:请求拦截器与响应拦截器
一般来说我们的请求拦截器是往请求中注入一个token,后面的请求就不用一个一个的往请求数据中添加token数据了
响应拦截器一般是负责解析一些数据结构及处理异常
拦截器的基础知识
axios——难点语法使用、拦截器的使用、取消请求功能演示
配置一下axios的基地址和超时时间
我们补充一下其他文件的内容
vuex中的index.js文件
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
import user from './modules/user'
Vue.use(Vuex)
const store = new Vuex.Store({
// 模块
modules: {
app,
settings,
user
},
// Vuex中的计算属性
getters
})
export default store
vuex中的getters.js文件
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name
}
export default getters
如下所示的代码我们配置了基地址、超时时间、请求拦截器
// 引入axios
import axios from 'axios'
// 引入vuex可以获取vuex实例对象,
// 我们在vuex实例对象找那个又添加了一个getters.js,其实是为了获取token数据
import store from '@/store/index.js'
// 创建一个axios实例
// 根据指定配置创建一个新的axios,也就是每个新的axios都有自己的配置
var service = axios.create({
// 基地址。因为后端的所有地址都有一个共同路径/api
baseURL: '/api',
// 超时时间,单位为毫秒,10000毫秒为10秒
timeout: 10000
})
// 创建请求拦截器,并且两个参数都是回调函数
// 请求成功的时候执行第一个成功回调函数
// 请求失败的时候执行第二个失败回调函数,error就是错误对象
service.interceptors.request.use(
// 成功时执行
(config) => {
// 将vuex中的token值放入到请求头里面
if (store.getters.token) {
// 存在token,才放入请求头中
config.headers.Authorization = `Bearer ${store.getters.token}`
}
return config
},
// 失败时执行
(error) => {
// 默认支持promise的,下面语句相当于终止了当前promise的执行
return Promise.reject(error)
})
// 默认导出axios实例对象
export default service
仍然是修改如下所示文件
// 创建响应拦截器,并且两个参数都是回调函数
service.interceptors.response.use(
// 请求成功时响应,此时的响应默认包裹了一层data,即response.data才是后台服务返回的内容
(response) => {
// 一次性解析出response.data中的三个属性
const { data, message, success } = response.data
if (success) {
// 此时响应正常
return data
} else {
Message({ type: 'error', message: message })
return Promise.reject(new Error(message))
}
},
// 请求失败时响应
(error) => {
// this.$message.warning 不能这么使用,因为此时的this不是组件实例对象
Message({ type: 'error', message: error.message })
// 默认支持promise的,下面语句相当于终止了当前promise的执行
return Promise.reject(error)
}
)
开发环境:开发人员开发代码、测试的代码环境。此环境对代码的要求不是很高,可以随意更改,因为还不是面向真正的用户
生产环境:也叫做正式环境,面向真实的用户
下面我们就要实现下图流程中的“区分环境”模块
怎么区分不同的环境?
使用环境变量,有如下两个位置
.env.development中设置开发环境变量 默认 NODE_ENV 值为 development
.env.production中设置生产环境变量默认 NODE_ENV 值为 production
执行不同的命令,NODE_ENV的值是不同的,我们需要node命令来取出NODE_ENV的值
process.env.属性的方式获取
process.env是我们node.js的全局变量,可以通过它来取到环境变量的名称
测试一下
开发环境下:在登录界面写一个created钩子函数
created(){
alert(process.env.NODE_ENV)
}
生产环境下:
所以为什么要区分环境?
开发环境下我们的数据随意更改,但是生产环境下数据是不能随意更改的,并且开发环境下和生产环境下的请求路径也是有所差别的
比如开发环境下请求/api路径,生产环境下走/prod-api路径
vue-cli给我们做了一个变量规范:
Vue代码中NODE_ENV之外,所有的变量必须以VUE_APP_开头
所以接下来我们要在axios中区分不同的环境,将VUE_APP_BASE_API的值赋值给axios的基地址
// 创建一个axios实例
// 根据指定配置创建一个新的axios,也就是每个新的axios都有自己的配置
var service = axios.create({
// 基地址。因为后端的所有地址都有一个共同路径/api
// 并且这个地方不能写死,否则生产环境和开发环境都走的一个地址
baseURL: process.env.VUE_APP_BASE_API,
// 超时时间,单位为毫秒,10000毫秒为10秒
timeout: 10000
})