二、登录注册 
目标
能实现登录页面的布局 
能实现基本登录功能 
能掌握 Vant 中 Toast 提示组件的使用 
能理解 API 请求模块的封装 
能理解发送验证码的实现思路 
能理解 Vant Form 实现表单验证的使用 
 
 
 
准备 创建组件并配置路由 1、创建 src/views/login/index.vue 并写入以下内容
<template >   <div  class ="login-container" > 登录页面</div >  </template > <script > export  default  {  name : 'LoginPage' ,   components : {},   props : {},   data () {     return  {}   },   computed : {},   watch : {},   created () {},   mounted () {},   methods : {} } </script > <style  scoped  lang ="less" > </style > 
 
2、然后在 src/router/index.js 中配置登录页的路由表
{   path : '/login' ,   name : 'login' ,   component : () =>  import ('@/views/login' ) } 
 
最后,访问 /login 查看是否能访问到登录页面。
 
布局结构 这里主要使用到三个 Vant 组件:
一个经验:使用组件库中的现有组件快速布局,再慢慢调整细节,效率更高(刚开始可能会感觉有点麻烦,越用越熟,慢慢的就有了自己的思想)。
 
布局样式 
写样式的原则:将公共样式写到全局(src/styles/index.less),将局部样式写到组件内部。
 
1、src/styles/index.less
body  {  background-color : #f5f7f9 ; } .page-nav-bar  {  background-color : #3296fa ;   .van-nav-bar__title  {     color : #fff ;   } } 
 
2、src/views/login/index.vue
<template >   <div  class ="login-container" >           <van-nav-bar  class ="page-nav-bar"  title ="登录"  />                <van-form  @submit ="onSubmit" >        <van-field           name ="用户名"          placeholder ="请输入手机号"        >         <i  slot ="left-icon"  class ="toutiao toutiao-shouji" > </i >        </van-field >        <van-field           type ="password"          name ="验证码"          placeholder ="请输入验证码"        >         <i  slot ="left-icon"  class ="toutiao toutiao-yanzhengma" > </i >          <template  #button >            <van-button  class ="send-sms-btn"  round  size ="small"  type ="default" > 发送验证码</van-button >          </template >        </van-field >        <div  class ="login-btn-wrap" >          <van-button  class ="login-btn"  block  type ="info"  native-type ="submit" >            登录         </van-button >        </div >      </van-form >         </div >  </template > <script > export  default  {  name : 'LoginIndex' ,   components : {},   props : {},   data () {     return  {     }   },   computed : {},   watch : {},   created () {},   mounted () {},   methods : {     onSubmit (values) {       console .log('submit' , values)     }   } } </script > <style  scoped  lang ="less" > .login-container  {  .toutiao  {     font-size : 37px ;   }   .send-sms-btn  {     width : 152px ;     height : 46px ;     line-height : 46px ;     background-color : #ededed ;     font-size : 22px ;     color : #666 ;   }   .login-btn-wrap  {     padding : 53px  33px ;     .login-btn  {       background-color : #6db4fb ;       border : none;     }   } } </style > 
 
实现基本登录功能 思路:
注册点击登录的事件 
获取表单数据(根据接口要求使用 v-model 绑定) 
表单验证 
发请求提交 
根据请求结果做下一步处理 
 
一、根据接口要求绑定获取表单数据 
1、在登录页面组件的实例选项 data 中添加 user 数据字段
... data () {   return  {     user : {       mobile : '' ,       code : ''      }   } } 
 
2、在表单中使用 v-model 绑定对应数据
<van-cell-group >   <van-field       v-model ="user.mobile"      required      clearable      label ="手机号"      placeholder ="请输入手机号"    />   <van-field       v-model ="user.code"      type ="number"      label ="验证码"      placeholder ="请输入验证码"      required    /> </van-cell-group > 
 
最后测试。
一个小技巧:使用 VueDevtools 调试工具查看是否绑定成功。
 
二、请求登录 
1、创建 src/api/user.js 封装请求方法
import  request from  "@/utils/request" export  const  login = data  =>  {  return  request({     method : 'POST' ,     url : '/app/v1_0/authorizations' ,     data   }) } 
 
2、给登录按钮注册点击事件
async  onLogin () {  try  {     const  res = await  login(this .user)     console .log('登录成功' , res)   } catch  (err) {     if  (err.response.status === 400 ) {      	console .log('登录失败' , err)     }   } } 
 
最后测试。
登录状态提示 Vant 中内置了Toast 轻提示 组件,可以实现移动端常见的提示效果。
Toast("提示内容" ); Toast.loading({   duration : 0 ,    message : "加载中..." ,   forbidClick : true   }); Ttoast.success("成功文案" ); Toast.fail("失败文案" ); 
 
提示:在组件中可以直接通过 this.$toast 调用。
 
另外需要注意的是:Toast 默认采用单例模式,即同一时间只会存在一个 Toast,如果需要在同一时间弹出多个 Toast,可以参考下面的示例
Toast.allowMultiple(); const  toast1 = Toast('第一个 Toast' );const  toast2 = Toast.success('第二个 Toast' );toast1.clear(); toast2.clear(); 
 
下面是为我们的登录功能增加 toast 交互提示。
async  onLogin () {     this .$toast.loading({     duration : 0 ,      forbidClick : true ,      message : '登录中...'     })   try  {     const  res = await  request({       method : 'POST' ,       url : '/app/v1_0/authorizations' ,       data : this .user     })     console .log('登录成功' , res)          this .$toast.success('登录成功' )   } catch  (err) {     console .log('登录失败' , err)     this .$toast.fail('登录失败,手机号或验证码错误' )   } } 
 
假如请求非常快的话就看不到 loading 效果了,这里可以手动将调试工具中的网络设置为慢速网络。
测试结束,再把网络设置恢复为 Online 正常网络。
表单验证 
参考文档:Form 表单验证 
 
<template >   <div  class ="login-container" >           <van-nav-bar  class ="page-nav-bar"  title ="登录"  />                     <van-form  @submit ="onSubmit" >        <van-field           v-model ="user.mobile"          name ="手机号"          placeholder ="请输入手机号"          :rules ="userFormRules.mobile"          type ="number"          maxlength ="11"        >         <i  slot ="left-icon"  class ="toutiao toutiao-shouji" > </i >        </van-field >        <van-field           v-model ="user.code"          name ="验证码"          placeholder ="请输入验证码"          :rules ="userFormRules.code"          type ="number"          maxlength ="6"        >         <i  slot ="left-icon"  class ="toutiao toutiao-yanzhengma" > </i >          <template  #button >            <van-button  class ="send-sms-btn"  round  size ="small"  type ="default" > 发送验证码</van-button >          </template >        </van-field >        <div  class ="login-btn-wrap" >          <van-button  class ="login-btn"  block  type ="info"  native-type ="submit" >            登录         </van-button >        </div >      </van-form >         </div >  </template > <script > import  { login } from  '@/api/user' export  default  {  name : 'LoginIndex' ,   components : {},   props : {},   data () {     return  {       user : {         mobile : '' ,          code : ''         },       userFormRules : {         mobile : [{           required : true ,           message : '手机号不能为空'          }, {           pattern : /^1[3|5|7|8]\d{9}$/ ,           message: '手机号格式错误'          }],         code : [{           required : true ,           message : '验证码不能为空'          }, {           pattern : /^\d{6}$/ ,           message: '验证码格式错误'          }]       }     }   },   computed : {},   watch : {},   created () {},   mounted () {},   methods : {     async  onSubmit () {              const  user = this .user                     this .$toast.loading({         message : '登录中...' ,         forbidClick : true ,          duration : 0         })       try  {         const  res = await  login(user)         console .log('登录成功' , res)         this .$toast.success('登录成功' )       } catch  (err) {         if  (err.response.status === 400 ) {           this .$toast.fail('手机号或验证码错误' )         } else  {           this .$toast.fail('登录失败,请稍后重试' )         }       }            }   } } </script > <style  scoped  lang ="less" > .login-container  {  .toutiao  {     font-size : 37px ;   }   .send-sms-btn  {     width : 152px ;     height : 46px ;     line-height : 46px ;     background-color : #ededed ;     font-size : 22px ;     color : #666 ;   }   .login-btn-wrap  {     padding : 53px  33px ;     .login-btn  {       background-color : #6db4fb ;       border : none;     }   } } </style > 
 
验证码处理 验证手机号 async  onSendSms () {  console .log('onSendSms' )      try  {     await  this .$refs.loginForm.validate('mobile' )   } catch  (err) {     return  console .log('验证失败' , err)   }       } 
 
使用倒计时组件 1、在 data 中添加数据用来控制倒计时的显示和隐藏
data () {   return  {     ...     isCountDownShow : false    } } 
 
2、使用倒计时组件
<van-field    v-model ="user.code"    placeholder ="请输入验证码"  >   <i  class ="icon icon-mima"  slot ="left-icon" > </i >    <van-count-down       v-if ="isCountDownShow"      slot ="button"      :time ="1000 * 5"      format ="ss s"      @finish ="isCountDownShow = false"    />   <van-button       v-else      slot ="button"      size ="small"      type ="primary"      round      @click ="onSendSmsCode"    > 发送验证码</van-button > </van-field > 
 
发送验证码 1、在 api/user.js 中添加封装数据接口
export  const  getSmsCode = mobile  =>  {  return  request({     method : 'GET' ,     url : `/app/v1_0/sms/codes/${mobile} `    }) } 
 
2、给发送验证码按钮注册点击事件
3、发送处理
async  onSendSms () {     try  {     await  this .$refs.loginForm.validate('mobile' )   } catch  (err) {     return  console .log('验证失败' , err)   }      this .isCountDownShow = true       try  {     await  sendSms(this .user.mobile)     this .$toast('发送成功' )   } catch  (err) {          this .isCountDownShow = false      if  (err.response.status === 429 ) {       this .$toast('发送太频繁了,请稍后重试' )     } else  {       this .$toast('发送失败,请稍后重试' )     }   } } 
 
处理用户 Token  
Token 是用户登录成功之后服务端返回的一个身份令牌,在项目中的多个业务中需要使用到:
访问需要授权的 API 接口 
校验页面的访问权限 
… 
 
但是我们只有在第一次用户登录成功之后才能拿到 Token。
所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。
往哪儿存?
使用容器存储 Token 的思路:
登录成功,将 Token 存储到 Vuex 容器中
 
为了持久化,还需要把 Token 放到本地存储
 
 
下面是具体实现。
1、在 src/store/index.js 中
import  Vue from  'vue' import  Vuex from  'vuex' Vue.use(Vuex) export  default  new  Vuex.Store({  state : {          user : JSON .parse(window .localStorage.getItem('TOUTIAO_USER' ))        },   mutations : {     setUser (state, user) {       state.user = user       window .localStorage.setItem('TOUTIAO_USER' , JSON .stringify(user))     }   },   actions : {   },   modules : {   } }) 
 
2、登录成功以后将后端返回的 token 相关数据存储到容器中
async  onLogin () {     this .$toast.loading({     duration : 0 ,      forbidClick : true ,      message : '登录中...'     })   try  {     const  res = await  login(this .user)      +    this .$store.commit('setUser' , res.data.data)          this .$toast.success('登录成功' )   } catch  (err) {     console .log('登录失败' , err)     this .$toast.fail('登录失败,手机号或验证码错误' )   }       } 
 
优化封装本地存储操作模块 创建 src/utils/storage.js 模块。
export  const  getItem = name  =>  {  const  data = window .localStorage.getItem(name)   try  {     return  JSON .parse(data)   } catch  (err) {     return  data   } } export  const  setItem = (name, value ) =>  {  if  (typeof  value === 'object' ) {     value = JSON .stringify(value)   }   window .localStorage.setItem(name, value) } export  const  removeItem = name  =>  {  window .localStorage.removeItem(name) } 
 
关于 Token 过期问题 登录成功之后后端会返回两个 Token:
token:访问令牌,有效期2小时 
refresh_token:刷新令牌,有效期14天,用于访问令牌过期之后重新获取新的访问令牌 
 
我们的项目接口中设定的 Token 有效期是 2 小时,超过有效期服务端会返回 401 表示 Token 无效或过期了。
为什么过期时间这么短?
过期了怎么办?
让用户重新登录,用户体验太差了 
使用 refresh_token 解决 token 过期 
 
如何使用 refresh_token 解决 token 过期?
到课程的后面我们开发的业务功能丰富起来之后,再给大家讲解 Token 过期处理。
大家需要注意的是在学习测试的时候如果收到 401 响应码,请重新登录再测试 。
 
概述:服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token 过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401。
处理流程:
在axios的拦截器中加入token刷新逻辑 
当用户token过期时,去向服务器请求新的 token 
把旧的token替换为新的token 
然后继续用户当前的请求 
 
在请求的响应拦截器中统一处理 token 过期:
import  axios from  "axios" ;import  jsonBig from  "json-bigint" ;import  store from  "@/store" ;import  router from  "@/router" ;const  request = axios.create({  baseURL : "http://ttapi.research.itcast.cn/"   }); request.defaults.transformResponse = [   function (data )  {     try  {       return  jsonBig.parse(data);     } catch  (err) {       return  {};     }   } ]; request.interceptors.request.use(   function (config )  {     const  user = store.state.user;     if  (user) {       config.headers.Authorization = `Bearer ${user.token} ` ;     }          return  config;   },   function (error )  {          return  Promise .reject(error);   } ); request.interceptors.response.use(         function (response )  {               return  response;   },      async  function (error )  {                         console .dir(error);     if  (error.response && error.response.status === 401 ) {              const  user = store.state.user;       if  (!user || !user.refresh_token) {         router.push("/login" );                  return ;       }              try  {         const  res = await  axios({           method : "PUT" ,           url : "http://ttapi.research.itcast.cn/app/v1_0/authorizations" ,           headers : {             Authorization : `Bearer ${user.refresh_token} `            }         });                  console .log("刷新 token  成功" , res);         store.commit("setUser" , {           token : res.data.data.token,            refresh_token : user.refresh_token          });                                    return  request(error.config);       } catch  (err) {                  console .log("请求刷线 token 失败" , err);         router.push("/login" );       }     }     return  Promise .reject(error);   } ); export  default  request;