# 登录模块

# 回顾

  • 关于参数传递参数props需要设置为true,才可以再组件中通过props得到路径参数
  • 接口路径传参和路由的参数传递
  • 熟悉项目的业务场景
  • 初始化项目
  • Vant基本用法
  • 移动端适配(原理:基于Rem)
  • 配置Vant相关适配插件
    • postcss-pxtorem 把px单位转换为rem单位
    • 插件二:根据屏幕的尺寸计算HTML的font-size基准值并进行设置(计算公式=屏幕尺寸/份数)
  • 整体路由配置
  • 首页基本布局
  • 全局样式设置
  • 封装token的操作
  • 封装并重构store的模块化
  • 封装通用的接口调用方法
    • 基本封装
    • 请求拦截器
    • 响应拦截器

# 封装请求API业务模块

目标:封装登录接口的业务模块

  • 封装登录接口业务模块
import request from '@/utils/request.js'

// 登录验证
export const login = (data) => {
  return request({
    method: 'post',
    url: 'app/v1_0/authorizations',
    data: data
  })
}
  • 组件中调用业务模块方法
  import { login } from '@/api/login.js'
  // 处理异步任务
  actions: {
    // 实现登录的action
    async login (context, payload) {
      try {
        // 调用登录接口
        const ret = await login(payload)
        // 调用mutation,更新用户信息
        context.commit('updateUserInfo', ret.data)
        if (ret && ret.data && ret.data.token) {
          // 登录成功
          return true
        }
      } catch (e) {
        console.log(e)
        // 告诉页面组件登录失败了
        return false
      }
    }
  },

# 页面基本布局

目标:实现登录页面基本布局

<div class="page-login">
  <van-nav-bar title="登 录"></van-nav-bar>
  <van-cell-group>
    <van-field  label="手机号" placeholder="请输入手机号" />
    <van-field label="验证码" placeholder="请输入验证码">
      <van-button class="p5" slot="button" size="mini" type="primary">
          发送验证码
      </van-button>
    </van-field>
  </van-cell-group>
  <div class="btn_box">
    <van-button type="info" block round>登 录</van-button>
  </div>
</div>

# 实现基本登录功能

目标:实现登录的基本功能

  • 表单数据绑定
  • 提交按钮事件绑定
  • 触发Action
import { mapActions } from 'vuex'

methods: {
  ...mapActions('login', ['toLogin']),
  async handleLogin () {
    // 处理登录流程
    const ret = await this.toLogin(this.loginForm)
    if (ret) {
      // 登录成功
      this.$router.push('/')
    } else {
      // 登录失败
      this.$toast.fail('手机号或验证码错误')
    }
  }
}

image-20210320100550675

# 表单验证

目标:能够自己实现表单验证(vant本身不支持表单验证)

  • 绑定错误消息 :error-message="errorMsg.mobile"
<van-cell-group>
  <van-field @blur='validate("mobile")' :error-message='mobileError' v-model='loginForm.mobile' label="手机号" placeholder="请输入手机号" />
  <van-field @blur='validate("code")' :error-message='codeError'  v-model='loginForm.code' label="验证码" placeholder="请输入验证码">
  <van-button class="p5" slot="button" size="mini" type="primary">
      发送验证码
  </van-button>
</van-field>
</van-cell-group>
<div class="btn_box">
  <van-button type="info" @click="handleLogin" block round>登 录</van-button>
</div>
  • @blur 失去焦点,做校验。
data () {
  return {
    mobileError: '',
    codeError: '',
    // 表单数据  
    loginForm: {
      mobile: '',
      code: ''
    }
  }
},
methods: {
  ...mapActions('login', ['toLogin']),
  validate (type) {
    // 获取手机号或者验证码的值
    const value = this.loginForm[type]
    if (type === 'mobile') {
      // 验证手机号
      if (!value) {
        // 手机号非空验证
        this.mobileError = '手机号不能为空'
      } else if (!/^1[3-9]\d{9}$/.test(value)) {
        // 手机号格式验证
        this.mobileError = '手机号格式错误'
      } else {
        // 验证通过
        this.mobileError = ''
      }
    } else if (type === 'code') {
      // 验证码
      if (!value) {
        // 手机号非空验证
        this.codeError = '验证码不能为空'
      } else if (!/^\d{6}$/.test(value)) {
        // 验证码格式验证
        this.codeError = '验证码格式错误'
      } else {
        // 验证通过
        this.codeError = ''
      }
    }
  },
  async handleLogin () {
    // 实现表单验证
    this.validate('mobile')
    this.validate('code')
    if (!(this.mobileError === '' && this.codeError === '')) {
      // 其中之一没有验证通过,终止请求动作
      return
    }
    // 处理登录流程
    const ret = await this.toLogin(this.loginForm)
    if (ret) {
      // 登录成功
      this.$router.push('/')
    } else {
      // 登录失败
      this.$toast.fail('手机号或验证码错误')
    }
  }
}

当然也可以使用插件,vee-validate http://vee-validate.logaretm.com/

# 登录按钮提示

目标:实现登录按钮提示效果(防止重复提交)

  • 通过vant按钮的loading属性实现提示效果
    • 点击按钮时开启加载状态
    • 完成操作后关闭加载状态,并且表单验证失败时也要关闭加载状态
<van-button :loading='loading' @click='handleLogin' type="info" block>登录</van-button>

# 路由导航守卫

目标:通过导航守卫控制登录权限

  • 配置路由的导航守卫,统一验证路由权限
// 导航守卫
// 白名单列表(没有登录也可以访问)
const whiteList = ['/home', '/video']
router.beforeEach((to, from, next) => {
  const userInfo = store.state.login.userInfo
  if (to.path === '/login') {
    // 访问登录页面
    next()
  } else if (userInfo && userInfo.token && userInfo.refresh_token) {
    // 访问非登录页面,并且有token
    next()
  } else if (whiteList.includes(to.path)) {
    // 判断访问的路由路径是否在白名单里面
    next()
  } else {
    // 访问非登录页面,但是没有token
    next('/login')
  }
})

# token续签问题

目标:处理token失效问题

token是什么时候生成的?第一次登陆成功后服务器生成并返回给客户端,客户端需要缓存token(服务器不存储普通的token)

如果服务器不存储token,那么服务器如何验证token是否有效?服务器可以解析token进行验证

  1. 如果token的有效期是1个小时,服务器是否可以再30分钟的时候,作废客户端的token?做不到

  2. 但是,现在我们希望随时可以作废客户端的token

  3. 所以引入了refresh_token 的机制(双token)

  4. 现在需要理解refresh_token的运行机制:

    • 普通token有效期设置的比较短(5分钟)
      • 为什么这个token的有效性设置这么短?保证安全
    • refresh_token有效期比较长(14天)
      • 如果这个token丢失了,别人是否可以无限刷新有效的普通token?理论上是可以的
      • 那么,服务器是否应该有一种机制:可以随时作废refresh_token?有
      • 服务器其实会把refresh_token存在数据库中,当客户端通过refresh_token申请普通token时,服务器会查询数据库中是否有这个有效的refresh_token,如果有,就返回正常的普通token,如果没有,那么就不会返回有效的普通token。
    • 如果普通token过期了,那么可以通过refresh_token申请一个新的普通token
    • 如果没有refresh_token,那么当普通token过期后,客户端就会跳转到登录页,那么频繁让用户去登录,体验不好,所以可以通过refresh_token自动更新一个新的有效的普通token,从而可以正常获取业务数据。
  • 通过频道接口调用营造一个测试环境 src/api/api-channel.js
/*
  封装频道获取接口
*/
import request from '@/utils/request.js'

export const getAllChannels = () => {
  return request({
    method: 'get',
    url: 'user/channels'
  })
}
// 文件的路径 src/views/home/Index.vue
<script>
import { getAllChannels } from '@/api/api-channel.js'
export default {
  name: 'Home',
  created () {
    getAllChannels()
  }
}
</script>
  • token有效期分析
    • token 用于访问需要身份认证的普通接口,有效期2小时
    • refresh_token 用于在token过期后,获取新的用户token,有效期14天
  • 功能实现
/*
  通用的接口调用方法
*/
import axios from 'axios'
import store from '@/store/index.js'
import router from '@/router/index.js'
import JSONBIGINT from 'json-bigint'
// import { createNamespacedHelpers } from 'vuex'
// createNamespacedHelpers的参数是模块的名称
// const { mapState } = createNamespacedHelpers('login')
// mapState(['userInfo']).userInfo

const baseURL = 'http://api-toutiao-web.itheima.net/'

// 创建专门的axios请求实例对象
const instance = axios.create({
  baseURL: baseURL,
  // 配置返回数据的格式
  transformResponse: [(data) => {
    // data参数表示后端返回的原始数据
    try {
      return JSONBIGINT.parse(data)
    } catch (e) {
      console.log(e)
      // 这里表示如果转换错误,那么就返回原始数据(不做转换)
      return data
    }
  }]
})

// 请求拦截器
instance.interceptors.request.use((config) => {
  // 统一添加请求头(登录成功后添加)
  // store中的状态 -> 模块名称 --> 状态属性
  // store.state.login.userInfo
  const userInfo = store.state.login.userInfo
  if (userInfo && userInfo.token) {
    // 登录成功,添加请求头
    config.headers.authorization = 'Bearer ' + userInfo.token
  }
  return config
}, (err) => {
  return Promise.reject(err)
})

// 响应拦截器
instance.interceptors.response.use((response) => {
  return response.data
}, async (err) => {
  if (err.response.status === 401) {
    try {
      // 获取用户的token信息
      const userInfo = store.state.login.userInfo
      // token失效了,通过refreshtoken申请一个新的token
      const ret = await axios.put(baseURL + 'app/v1_0/authorizations', null, {
        headers: {
          authorization: 'Bearer ' + userInfo.refresh_token
        }
      })
      // 替换之前的token,触发mutation
      userInfo.token = ret.data.data.token
      store.commit('login/updateUserInfo', userInfo)
      // 把刚才失败的请求用新的token再重新发送
      return instance(err.config)
    } catch (e) {
      console.log(e)
      // 跳转到登录页面,删除用户信息
      store.commit('login/deleteUserInfo')
      router.push('/login')
    }
  }
  return Promise.reject(err)
})

// 封装通用的接口调用方法
export default (options) => {
  return instance({
    method: options.method || 'get',
    url: options.url || '#',
    data: options.data,
    params: options.params,
    headers: options.headers
  })
}