1. 布局结构
目标
能实现登录页面的布局
能实现基本登录功能
能掌握 Vant 中 Toast 提示组件的使用
能理解 API 请求模块的封装
能理解发送验证码的实现思路
能理解 Vant Form 实现表单验证的使用
这里主要使用到三个 Vant 组件:
1.1 布局样式
写样式的原则:将公共样式写到全局(src/styles/index.less
),将局部样式写到组件内部。对于需要更改样式,自己添加 class 类名。
1.2 自定义图标的使用
官方文档实现:
实现效果:
最左侧显示的图标无法满足需求的情况下,可以通过 slot 实现,插入自定义的图标。可以插入的slot如下所示:
具体实现,通过插入 i 标签,设置 iconfont 类名。
1.2 插入按钮
官方文档:
实现效果:
但是在表单中,除了提交按钮外,可能还有一些其他的功能性按钮,如发送验证码按钮。在使用这些按钮时,要注意将 native-type设置为button,否则会触发表单提交。
2. 实现基本登录功能
功能需求:
注册点击登录的事件
获取表单数据(根据接口要求使用 v-model 绑定)
表单验证
发请求提交
根据请求结果做下一步处理
2.1 登录状态提示
2.2 表单验证
- 给 vant-field 组件配置 rules 验证规则
- 可以在 data 里自定义校验规则校验手机号和验证码
- 当给表单提交的时候会自动触发表单验证
- 如果验证通过,则触发 submit 事件
- 如果验证不通过,则不会触发 submit 事件
- 验证规则 参考vant组件的文档
2.3 验证码处理
点击发送验证码功能
功能需求:
- 点击发送验证码按钮后,对用户输入的手机号进行验证
- 验证通过显示倒计时
- 发送用户手机号给后端,后台给用户手机下发验证码
- 发送失败关闭倒计时
注意:
- 这里的 ref 是绑在表单 Form上
- 获取表单实例之后,通过 .validate('校验表单或输入框的name属性值')
- 返回的是一个 promise 对象
3. 处理用户 Token
Token 是用户登录成功之后服务端返回的一个身份令牌,在项目中的多个业务中需要使用到:
访问需要授权的 API 接口
校验页面的访问权限
...
但是我们只有在第一次用户登录成功之后才能拿到 Token。
所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。
往哪儿存?
本地存储
获取麻烦
数据不是响应式
Vuex 容器(推荐)
获取方便
响应式的
3.1 使用容器存储 Token 的思路
登录成功,将 Token 存储到 Vuex 容器中
获取方便
响应式
为了持久化,还需要把 Token 放到本地存储
持久化
登录成功以后将后端返回的 token 相关数据存储到容器中
3.2 优化封装本地存储操作模块
创建 src/utils/storage.js
模块
3.3 关于 Token 过期问题
登录成功之后后端会返回两个 Token:
token
:访问令牌,有效期2小时refresh_token
:刷新令牌,有效期14天,用于访问令牌过期之后重新获取新的访问令牌
我们的项目接口中设定的 Token
有效期是 2 小时
,超过有效期服务端会返回 401
表示 Token 无效或过期了。
为什么过期时间这么短?
为了安全,例如 Token 被别人盗用
过期了怎么办?
让用户重新登录,用户体验太差了
使用
refresh_token
解决token
过期
后续拓展Token 过期处理 ,在学习测试的时候如果收到 401 响应码,请重新登录再测试。
概述:服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token
过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401。
处理流程:
在axios的拦截器中加入token刷新逻辑
当用户token过期时,去向服务器请求新的 token
把旧的token替换为新的token
然后继续用户当前的请求
在请求的响应拦截器中统一处理 token 过期(后续拓展)。
/** * 封装 axios 请求模块 */ import axios from "axios"; import jsonBig from "json-bigint"; import store from "@/store"; import router from "@/router"; // axios.create 方法:复制一个 axios const request = axios.create({ baseURL: "http://ttapi.research.itcast.cn/" // 基础路径 }); /** * 配置处理后端返回数据中超出 js 安全整数范围问题 */ 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}`; } // Do something before request is sent return config; }, function(error) { // Do something with request error return Promise.reject(error); } ); // 响应拦截器 request.interceptors.response.use( // 响应成功进入第1个函数 // 该函数的参数是响应对象 function(response) { // Any status code that lie within the range of 2xx cause this function to trigger // Do something with response data return response; }, // 响应失败进入第2个函数,该函数的参数是错误对象 async function(error) { // Any status codes that falls outside the range of 2xx cause this function to trigger // Do something with response error // 如果响应码是 401 ,则请求获取新的 token // 响应拦截器中的 error 就是那个响应的错误对象 console.dir(error); if (error.response && error.response.status === 401) { // 校验是否有 refresh_token const user = store.state.user; if (!user || !user.refresh_token) { router.push("/login"); // 代码不要往后执行了 return; } // 如果有refresh_token,则请求获取新的 token try { const res = await axios({ method: "PUT", url: "http://ttapi.research.itcast.cn/app/v1_0/authorizations", headers: { Authorization: `Bearer ${user.refresh_token}` } }); // 如果获取成功,则把新的 token 更新到容器中 console.log("刷新 token 成功", res); store.commit("setUser", { token: res.data.data.token, // 最新获取的可用 token refresh_token: user.refresh_token // 还是原来的 refresh_token }); // 把之前失败的用户请求继续发出去 // config 是一个对象,其中包含本次失败请求相关的那些配置信息,例如 url、method 都有 // return 把 request 的请求结果继续返回给发请求的具体位置 return request(error.config); } catch (err) { // 如果获取失败,直接跳转 登录页 console.log("请求刷新 token 失败", err); router.push("/login"); } } return Promise.reject(error); } ); export default request;