02-HR-登录模块(登录页面基本布局,登录表单的校验,封装单独的登录接口,axios响应拦截器,基于vuex管理token,token缓存封装,导航守卫处理)

140 阅读6分钟

登录模块

登录页面基本布局

**目标**完成登录页面的基础布局

  • 基本布局
<!-- 放置标题图片 @是设置的src目录别名-->
<div class="title-container">
  <h3 class="title">
    <img src="@/assets/common/login-logo.png" alt="">
  </h3>
</div>
  • 样式处理
.login-container {
  // 设置背景图片
  background-image: url('~@/assets/common/login.jpg'); 
  // 将图片位置设置为充满整个屏幕
  background-position: center; 
}

注意: 如需要在样式表中使用**@别名的时候,需要在@前面加上一个~**符号,否则不识别

  • 设置手机号和密码的字体颜色
// 修改输入框字体颜色
$light_gray: ##68b0fe; 
  • 设置输入表单整体背景色
.el-form-item {
  border: 1px solid rgba(255, 255, 255, 0.1);
  background: rgba(255, 255, 255, 0.9); // 输入登录表单的背景色
  border-radius: 5px;
  color: #454545;
}
  • 设置错误信息的颜色
.el-form-item__error {
  color: #fff;
  font-size: 14px;
}
  • 设置登录按钮的样式
.login-btn {
  background: #407ffe;
  height: 64px;
  line-height: 32px;
  font-size: 24px;
}

需要给el-button 增加一个login-btn的class样式

  • 修改显示的提示文本和登录文本
<div class="tips">
  <span style="margin-right:20px;">账号: 13800000002</span>
  <span> 密码: 123456</span>
</div>

总结:实现登录页面的布局样式

注意:定义element组件样式,需要找到对应元素的类名,进行覆盖

登录表单的校验

**目标**对登录表单进行规则校验

基础模板已经有了基础校验的代码,我们只需在此基础上修改即可

  • el-form表单校验的必要条件
    • el-form标签上面绑定model和rules熟悉
      • model指的是绑定到输入域上的表单数据
      • rules表示验证的规则配置
    • el-form-item标签上绑定prop属性,值是要验证的表单的输入域的name
    • el-input上面要双向绑定输入域的name

image.png

  • 登录表单验证
    • 手机号必填,并且进行格式校验
    • 密码必填,长度6-16位之间
// @/utils/validate.js
// 校验手机号
export function validMobile(str) {
  // 正则的方法test用于判断str是否符合正则的规则
  return /^1[3-9]\d{9}$/.test(str) 
}
// -------------------------------------
import { validMobile } from '@/utils/validate'
data() {
  // 自定义校验函数
  const validateMobile = (rule, value, callback) => {
    if (!validMobile(value)) {
      callback(new Error('手机格式有误'))
    } else {
      callback()
    }
  }

  return {
    loginForm: {
      mobile: '13800000002',
      password: '123456'
    },
    loginRules: {
      mobile: [
        { required: true, message: '请输入手机号', trigger: ['blur', 'change'] },
        { validator: validateMobile, trigger: ['blur', 'change'] }
      ],
      password: [
        { required: true, message: '请输入密码', trigger: ['blur', 'change'] },
        { min: 6, max: 16, message: '密码必须是6-16位', trigger: ['blur', 'change'] }
      ]
    },
    loading: false,
    passwordType: 'password',
    redirect: undefined
  }
},

总结:遵循ElementUI的表单验证规则;自定义验证规则要熟悉

  1. 内置表单验证规则
  2. 自定义表单验证规则

注意:1、el-form绑定model和rules;2、el-form-item绑定prop;3、表单输入域el-input需要v-model绑定name属性

  • 手动进行表单验证
handleLogin () {
  // 点击登录按钮时,触发表单验证
  // validate函数属于谁?el-form组件的实例对象
  this.$refs.loginForm.validate(valid => {
    if (valid) {
      console.log('表单验证通过')
      // 调用接口实现登录
    } else {
      console.log('表单验证失败')
    }
  })
}

总结:

  1. 手动表单验证
  2. ref直接操作组件实例
    1. 通过ref属性绑定组件
    2. 通过this.$refs.名称,访问组件实例

封装单独的登录接口

目标 在单独请求模块中,单独封装登录接口

基准地址: ihrm-java.itheima.net/api

完成登录模块之后,我们需要对登录接口进行封装, 登录请求应该单独封装到一个模块中, 便于维护和管理

  • 封装基本的接口模块 api/user.js
import request from '@/utils/request'

// 登录功能 
// data = { mobile: '', password: '' }
export function login (data) {
  return request({
    method: 'post',
    url: '/sys/login',
    data: data
  })
}
  • 封装一个Action方法实现登录
// 定义action
async login({ commit }, userInfo) {
  const { username, password } = userInfo
  const ret = await login({ mobile: username.trim(), password: password })
  if (ret.data.code === 10000) {
    // 登录成功
    commit('setToken', ret.data.data)
    return true
  } else {
    // 登录失败
    return false
  }
}
// 触发action
handleLogin () {
  this.$refs.loginForm.validate(async valid => {
    if (valid) {
      // 表单验证通过,触发action
      this.$store.dispatch('user/login', {
        mobile: this.loginForm.username,
        password: this.loginForm.password
      }).then(ret => {
        if (ret) {
          // 登录成功,跳转到主页面
          this.$router.push('/')
        } else {
          // 登录失败
          alert('用户名或者密码错误')
        }
      })
    }
  })
}
// --------------------------------------------
handleLogin () {
  // 登录触发方法
  this.$refs.loginForm.validate(async valid => {
    if (valid) {
      // 表单验证成功,调用接口登录
      // action返回的结果默认就是Promise实例对象
      const ret = await this.$store.dispatch('user/login', {
        mobile: this.loginForm.username,
        password: this.loginForm.password
      })
      if (ret) {
        // 登录成功,跳转到主页面
        this.$router.push('/')
      } else {
        // 登录失败,提示错误
        this.$message.error('用户名或者密码错误')
      }
    } else {
      console.log('error submit!!')
      return false
    }
  })
}

总结

  1. 封装登录接口api
  2. 封装调用接口的Action方法
  3. 组件中把Action映射为函数并且调用方法实现登录
  • 基于async函数和mapActions优化代码
handleLogin () {
  // 手动校验表单
  // ref可以操作原生DOM;也可以操作组件实例
  this.$refs.loginForm.validate(async valid => {
    // 验证如果不通过,就终止后续代码
    if (!valid) return
    // 验证通过,继续执行
    this.loading = true
    // 触发action实现登录(action的返回值是Promise实例对象)
    try {
      const res = await this.$store.dispatch('user/login', {
          mobile: this.loginForm.username,
          password: this.loginForm.password
      })
      if (res) this.$message.success('登录成功')
    } catch (err) {
      if (!err) this.$message.error('登录失败')
    }
    this.loading = false
  })
}

总结:

  1. Promise的方式可以基于Async函数优化
  2. 原始的dispatch方法触发Action可以基于映射函数进行优化
import { mapActions } from 'vuex'
methods: {
  ...mapActions('user', ['login'])
},
handleLogin () {
  // 手动校验表单
  // ref可以操作原生DOM;也可以操作组件实例
  this.$refs.loginForm.validate(async valid => {
    // 验证如果不通过,就终止后续代码
    if (!valid) return
    // 验证通过,继续执行
    // 防止按钮多次点击
    this.loading = true
    // 触发action实现登录(action的返回值是Promise实例对象)
    const ret = await this.login({
        mobile: this.loginForm.username,
        password: this.loginForm.password
    })
    if (ret) {
        this.$router.push('/')
    } else {
        this.$message.error('登录失败')
    }
    // 让按钮可以继续点击
    this.loading = false
  })
}

总结

  1. 基于mapActions优化触发action的代码
  2. 处理表单的重复提交问题(给按钮添加loading状态控制)

axios响应拦截器

目标: 处理响应拦截器

axios返回的数据中默认增加了一层**data的包裹**, 每次我们都要 res.data.data, 太麻烦, 所以我们需要在这里处理下。

// 响应拦截器
instance.interceptors.response.use(function(response) {
  // 对响应数据做点什么
  return response.data
}, function(error) {
  // 对响应错误做点什么
  return Promise.reject(error)
})

总结:通过响应拦截器优化数据的获取,去掉一层data属性。

基于vuex管理token

目标 封装获取token的登录action, 在vuex中存储token

  • 准备状态store/modules/user.js
state: () => ({
   token: ''
})
  • 封装 action 和 对应的 mutation store/modules/user.js
mutations: {
    setToken (state, token) {
        state.token = token
    }
},
actions: {
    async login({ commit }, userInfo) {
      const { username, password } = userInfo
      const { code, data } = await login({
          mobile: this.loginForm.username,
          password: this.loginForm.password
      })
      if (code === 10000) {
        // 登录成功
        commit('setToken', data)
        return true
      } else {
        // 登录失败
        return false
      }
    }
}

总结:token存储在Vuex的状态里面(JWT )

image.png

token缓存封装

目标 封装通用的token操作模块

特点:发送请求时会自动携带Cookie;没有浏览器的兼容问题;

Cookie的类型:会话Cookie(浏览器一旦关闭就失效了);持久Cookie(可以设置有效期)

  • 模块封装**utils/auth.js**
import Cookies from 'js-cookie'

const TokenKey = 'vue_admin_template_token_128'

// 获取指定的cookie
export function getToken () {
  return Cookies.get(TokenKey)
}
// 设置指定的cookie
export function setToken (token) {
  return Cookies.set(TokenKey, token)
}
// 删除指定的cookie
export function removeToken () {
  return Cookies.remove(TokenKey)
}

总结:

  1. 原生js可以操作Cookie,但是很麻烦
  2. 所以可以基于第三方包 js-cookie操作cookie,比较简单
  • 初始化获取缓存信息
const state = {
  // 一进来优先从缓存中取
  token: getToken() || null // token字符串
}
  • vuex中存token时, 也同步存到cookie中
async login (context, payload) {
  // context 相当于 this.$store
  // payload 表示触发action是传递的参数
  // 调用接口实现登录
  try {
    const { code, data } = await login(payload)
    if (code === 10000) {
      // 获取后端返回的成功的信息:关键是token
      context.commit('setToken', data)
      // 缓存token到cookie里面
      setToken(data)
      return true
      // return new Promise((resolve) => {
      //   resolve(true)
      // })
    } else {
      // 登录失败
      return false
    }
  } catch {
    // 登录失败
    return false
  }
},

总结:封装token的基本操作方法,本地缓存采用cookie

  1. 首次登陆时,缓存token
  2. 页面打开时,从缓存读取token并初始化state中的token

注意:cookie查看的位置

image.png

导航守卫处理

目标:根据token处理路由组件的访问权限问题 (没有token的用户, 不让你进系统的)

  • 权限拦截的流程图

image.png

  • 功能代码实现**src/permission.js**
import router from './router'
import store from './store'

// 白名单
const whiteList = ['/login', '/404', '/abc']

router.beforeEach((to, from, next) => {
  // 判断token的存在性
  // 从vuex中获取token信息
  const token = store.getters.token
  if (token) {
    // 存在:判断是否为登录页面
    if (to.path === '/login') {
      // 是登录页面,跳转到主页面
      next('/')
    } else {
      // 不是登录页面,直接放行通过
      next()
    }
  } else {
    // 不存在,判断是否请求的路径在白名单里面
    if (whiteList.includes(to.path)) {
      // 在白名单里面,放行通过即可
      next()
    } else {
      // 不在白名单里面,跳转到登录页面
      router.push('/login')
    }
  }
})

总结:导航守卫要拦截所有请求的路由链接

  1. 判断是否登录(token的存在性判断)
  2. 判断是否为白名单(根据需求规定一个白名单)

关于基准路径的配置

注意:.env.development.env.production 文件用于配置开发环境和生产环境的基准url地址

  • .env.development 开发环境
  • .env.production 生产环境
# base api
VUE_APP_BASE_API = 'http://ihrm-java.itheima.net/api/'
// 通过process.env.环境变量名称(名称自定义)
// process.env是Node.js的API
export const baseURL = process.env.VUE_APP_BASE_API

总结:为了灵活配置接口的基准路径,可以基于配置文件的方式,在开发和生产环境分别切换到不同的地址。这种方式好处就是代码在开发和上线时修改比较容易