Vue黑马头条移动端-Day1-源码分析
------------恢复内容开始------------
1、任何放置在 public 文件夹的静态资源都会被简单的复制,而不经过 webpack。
===所以在public下的文件其实可以直接通过比如 html/index.html直接访问,不用再通过复杂的相对定位这些东西来搞的
2、App.vue 根组件,最终被替换渲染到 index.html 页面中 #app 入口节点
3、main.js 整个项目的启动入口模块
移动端 REM 适配
Vant 中的样式默认使用 px
作为单位,如果需要使用 rem
单位,推荐使用以下两个工具:
- postcss-pxtorem 是一款 postcss 插件,用于将 px 单位转化为 rem
- lib-flexible 用于设置 rem 基准值
下面我们分别将这两个工具配置到项目中完成 REM 适配。
4、
然后在 src/styles/index.less
中加载 icon.less
。
/** * 全局样式 */ @import "./icon.less";
英语index。less已经引入了main.js。所以icon。less直接引入index。less中即可了
最后就是如何使用:使用 i
标签,给两个类名,一个是字体类名 toutiao
,一个是图标类名 toutiao-xxx
。
<!-- 手机图标 --> <i class="toutiao toutiao-shouji"></i> <!-- 收藏图标 --> <i class="toutiao toutiao-shoucang"></i>
==也就是iconfont主要用到的标签可以是i标签这样的
而vant中图标的使用的话
使用 Vant 中的图标
Vant 组件库内置了一套非常精致的字体图标,除基本功能之外还支持徽标提示等功能。
<!-- 基本展示 --> <van-icon name="chat-o" /> <!-- 设置dot属性后,会在图标右上角展示一个小红点 --> <van-icon name="chat-o" dot /> <!-- 设置badge属性后,会在图标右上角展示相应的徽标 --> <van-icon name="chat-o" badge="9" />
也就是说一个是通过class文件来搞的,一个就是通过在van-icon中通过name来搞的
也就是
import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) // 路由表 const routes = [ { path: '/login', name: 'login', component: () => import('@/views/login/') }, { path: '/', component: () => import('@/views/layout/'), children: [ { path: '', // 默认子路由 name: 'home', component: () => import('@/views/home/') }, { path: '/qa', name: 'qa', component: () => import('@/views/qa/') }, { path: '/video', name: 'video', component: () => import('@/views/video/') }, { path: '/my', name: 'my', component: () => import('@/views/my/') } ] } ] const router = new VueRouter({ routes }) export default router
也就是说tabbar页面都是布局组件的子组件,作为默认子路由就直接path:' '直接为空就行了
拓展:vuerouter
路由中有三个基本的概念 route, routes, router。
1, route,它是一条路由,由这个英文单词也可以看出来,它是单数, Home按钮 => home内容, 这是一条route, about按钮 => about 内容, 这是另一条路由。
2, routes 是一组路由,把上面的每一条路由组合起来,形成一个数组。[{home 按钮 =>home内容 }, { about按钮 => about 内容}]
3, router 是一个机制,相当于一个管理者,它来管理路由。因为routes 只是定义了一组路由,它放在哪里是静止的,当真正来了请求,怎么办? 就是当用户点击home 按钮的时候,怎么办?这时router 就起作用了,它到routes 中去查找,去找到对应的 home 内容,所以页面中就显示了 home 内容。
4,客户端中的路由,实际上就是dom 元素的显示和隐藏。当页面中显示home 内容的时候,about 中的内容全部隐藏,反之也是一样。客户端路由有两种实现方式:基于hash 和基于html5 history api.
1, 页面实现(html模版中)
在vue-router中, 我们看到它定义了两个标签<router-link> 和<router-view>来对应点击和显示部分。<router-link> 就是定义页面中点击的部分,<router-view> 定义显示部分,就是点击后,区配的内容显示在什么地方。所以 <router-link> 还有一个非常重要的属性 to,定义点击之后,要到哪里去, 如:<router-link to="/home">Home</router-link>
2, js 中配置路由
结:也就是根组件App.vue。通过在 App.vue中 定义<router-link > 和 </router-view> ,如何通过src-》router文件来配置路由,在router中把router export出来,如何在App。vue中展现
就可以把各个页面都在APP.vue里面呈现了,然后把App。vue的东西直接挂载到public中的index。html中即可
封装请求模块
和之前项目一样,这里我们还是使用 axios 作为我们项目中的请求库,为了方便使用,我们把它封装为一个请求模块,在需要的时候直接加载即可。
安装 axios:
npm i axios
创建 src/utils/request.js
:
/** * 封装 axios 请求模块 */ import axios from "axios" const request = axios.create({ baseURL: "http://ttapi.research.itcast.cn/" // 基础路径 }) export default request
这里就是通过baseurl把我们要请求接口的根目录赋值为了request,什么时候想要调用里面的接口的话就可以直接通过request来搞了
哪里使用,哪里加载:
import request from '@/utils/request' request({ method: 'xxx', url: 'xxx',
二、登陆注册
①顶部标题栏,设置title和左箭头来实现返回到上一页面中去
<van-nav-bar title="登录" left-arrow @click-left="$router.back()" />
②登陆的整体表单外面需要通过一个van-cell-group,可以使得内部是有序的排列的(由于我们这个登陆页面的nav-bar写完之后,可以不配置样式,会直接使用全局中队nav-bar配置的样式的
注意:van-cell-group 仅仅是提供了一个上下外边框,能看到包裹的区域
<van-cell-group> <van-field v-model="user.mobile" //这里就是输入的东西了通过v-model,v-model里面的东西都要在下面的return data数据域里面进行定义的 left-icon="smile-o" //这里就是左边的图标 placeholder="请输入手机号" /> <van-field v-model="user.code" clearable //是否是否启用清除图标,点击清除图标后会清空输入框
left-icon="music-o" placeholder="请输入验证码" > <template #button> //在这个van-field中再嵌套一个button按钮来“发送验证码”,也就是可以通过这样的格式 template #button 这里#后面是slot插槽的类别 <van-button size="small" round >发送验证码</van-button> </template> </van-field> </van-cell-group>
##拓展:
写样式的原理:将公共样式写到全局(src/styles/index.less
),将局部样式写到组件内部。
然后再把全局的演示index。less导入到main。js里面,就可以给作为各个页面的公共样式了
/** * 全局样式 */ @import "./icon.less"; body { background-color: #f5f7f9; } .app-nav-bar { background-color: #3296fa !important; .van-nav-bar__title { color: #fff; } .van-icon { color: #fff !important; } }
这里就是把iconfont里面的引入到全局了,然后把顶部van-nav-bar的样式和整个body的背景颜色都定义了
③实现基本登陆功能
思路:
- 注册登录按钮点击事件
- 获取表单数据(根据接口要求使用 v-model 绑定)
- 表单验证
- 请求提交
- 处理响应结果
一般我们会根据接口传入数据类型的要求,会直接提前在return data数据域中把这个数据类型就定义好了
比如‘:
... data () { return { user: { mobile: '', code: '' } } }
请求登录
创建 src/api/user.js
模块,封装登录请求方法:
/**
* 用户相关的请求模块
*/
import request from "@/utils/request"
/**
* 用户登录
*/
export const login = data => {
return request({
method: 'POST',
url: '/app/v1_0/authorizations',
data
})
}
给登录按钮注册点击事件。==也就是我们的button添加上一个点击事件即可了
登录处理函数如下。
import { login } from '@/api/user' async onLogin () { try { const res = await login(this.user) console.log('登录成功', res) } catch (err) { if (err.response.status === 400) { console.log('登录失败', err) } } }
这里通过try catch 异步的来调用接口获取到数据
登录状态提示
Vant 中内置了Toast 轻提示组件,可以实现移动端常见的提示效果。
===提示:在组件中可以直接通过 this.$toast
调用。
==为我们的登陆功能增加弹窗提醒的功能
async onLogin () { // 开始转圈圈 this.$toast.loading({ duration: 0, // 持续时间,0表示持续展示不停止 forbidClick: true, // 是否禁止背景点击 message: '登录中...' // 提示消息 }) try { const res = await request({ method: 'POST', url: '/app/v1_0/authorizations', data: this.user }) console.log('登录成功', res) // 提示 success 或者 fail 的时候,会先把其它的 toast 先清除 this.$toast.success('登录成功') } catch (err) { console.log('登录失败', err) this.$toast.fail('登录失败,手机号或验证码错误') } }
===如果想要进行测试的话:如果请求非常快的话就看不到 loading 效果了,这里可以在调试工具中将网络设置为慢速模式模拟低速网络。测试结束,再把网络设置恢复为 Online
正常网络。
表单验证
1)基本使用
- 使用 van-form 组件包裹所有的表单项 van-field
- 监听 form 的 submit 事件
- 当表单提交的时候会触发 submit 事件
- 只有表单验证通过,它才会被触发调用
- 使用 Field 的 rules 属性定义校验规则
===field中rules一般都是定义这个输入框一定要填写上东西
<van-form @submit="onSubmit"> <van-field v-model="username" name="用户名" label="用户名" placeholder="用户名" :rules="[{ required: true, message: '请填写用户名' }]" /> <van-field v-model="password" type="password" name="密码" label="密码" placeholder="密码" :rules="[{ required: true, message: '请填写密码' }]" /> <div style="margin: 16px;"> <van-button round block type="info" native-type="submit"> 提交 </van-button> </div> </van-form> export default { data() { return { username: '', password: '', }; }, methods: { onSubmit(values) { console.log('submit', values); }, }, };
这里的form通过@submit绑定了监听事件,也就是监听submit函数的触发,所以我们就可以把下面的button定义为一个submit上交的按钮类型
====校验规则
并且就可以想到我们通过每一个field都会进行校验,那么如果校验错误了要怎么提醒用户了,就可以使用外层的form来监听这个错误的事件
在vue里面很多组件都是这样的,都是通过布尔来控制提示的文字要不要发,然后通过一个String来定义提示的内容,然后通过@来监听某一个事件并且定义事件监听触发函数
这里就是通过show-error来判断要不要提示,通过show-error-message来说明提示什么文字,然后监听failed事件,定义onfailed函数
这个onfailed是可以直接拿到错误error并且作为参数值的
验证码处理
在验证码处理之前,也就是把这个验证码发送给用户之前的话,要对用户输入的手机号进行验证才行的,所以就是要验证手机号而不用验证整个表单
这个时候就用部了form的failed事件监听了
通过查阅文档可以看到 Form 组件的实例方法 validate
可以用来手动触发表单验证,并且支持传入 name
来验证单个表单项。
- 给 Form 添加 ref 属性用来获取组件实例
- 给 Field 添加 name 属性用来验证单个表单项
- 监听发送按钮的点击事件
- 由于 Form 中的任何按钮点击都会触发表单的默认提交行为,所以这里使用
.prevent
修饰符阻止默认行为
这里获取单个元素的话可以类比我们写css那种,id用#,class用。但是如果要在js里面访问元素dom节点的把,可能会想到通过getclassby这种函数
但是vue组件里面就提供了一个更加方便的,通过ref来先找到我们的form表单结构,然后就可以指定表单内部的某一个name属性的dom节点了
async onSendSms () { try { // 校验手机号码 await this.$refs['login-form'].validate('mobile') // 验证通过 -> 请求发送验证码 -> 用户接收短信 -> 输入验证码 -> 请求登录 // 请求发送验证码 -> 隐藏发送按钮,显示倒计时 // 倒计时结束 -> 隐藏倒计时,显示发送按钮 } catch (err) { this.$toast({ message: err.message, position: 'top' }) } }
请求发送短信
/** * 发送短信验证码 */ export const sendSms = mobile => { return request({ method: 'GET', url: `/app/v1_0/sms/codes/${mobile}` }) }
就是加入要在请求接口中url里面要添加上我们的数据的话,外层就要通过 ` ` 来保住,然后后面通过${ }把我们的数据拼接到url链接的后面即可了
async onSendSms () { try { // 校验手机号码 await this.$refs['login-form'].validate('mobile') // 验证通过,请求发送验证码 await sendSms(this.user.mobile) } catch (err) { // try 里面任何代码的错误都会进入 catch // 不同的错误需要有不同的提示,那就需要判断了 let message = '' if (err && err.response && err.response.status === 429) { // 发送短信失败的错误提示 message = '发送太频繁了,请稍后重试' } else if (err.name === 'mobile') { // 表单验证失败的错误提示 message = err.message } else { // 未知错误 message = '发送失败,请稍后重试' } // 提示用户 this.$toast({ message, position: 'top' }) } }
如果是把验证手机号还有发送验证码到用户写在一起的把,通过try catch,catch中进入的错误可能是不同的
所以就可以定义一个message来分不同情况下,赋值不同的值,然后在最后的时候就可以通过this.$toast提示给用户l
await this.$refs['login-form'].validate('mobile')
可见通过这种方法获取到DOM节点的话也是要使用await异步来实现的
async onSendSms () { try { // 校验手机号码 await this.$refs['login-form'].validate('mobile') // 验证通过,请求发送验证码 await sendSms(this.user.mobile) } catch (err) { // try 里面任何代码的错误都会进入 catch // 不同的错误需要有不同的提示,那就需要判断了 let message = '' if (err && err.response && err.response.status === 429) { // 发送短信失败的错误提示 message = '发送太频繁了,请稍后重试' } else if (err.name === 'mobile') { // 表单验证失败的错误提示 message = err.message } else { // 未知错误 message = '发送失败,请稍后重试' } // 提示用户 this.$toast({ message, position: 'top' }) } }
也就是说如果是未知错误的话,就可以直接不负责任的“发送失败,重试就行”
请求期间禁用按钮点击
记住一个原则:任何和网络交互有关的视图都应该在网络请求期间禁用,防止请求过慢导致多次触发请求行为。
拓展:click.prevent
@click.prevent="onSendSms"
这个放在van-button上面的话,其实就是因为form表单框架一个,只要有一个button点击了就会立马执行他的submit触发事件
就会验证整个表单输入数据了,但是我们不希望是这样的,就要阻止form这种静默的行为,这个代码的意思就是,阻止form触发submit只运行onSendSms这个函数
async onSendSms () { try { // 校验手机号码 await this.$refs['login-form'].validate('mobile') // 验证通过,请求发送验证码 + this.isSendSmsLoading = true // 展示按钮的 loading 状态,防止网络慢用户多次点击触发发送行为 await sendSms(this.user.mobile) // 短信发出去了,隐藏发送按钮,显示倒计时 // 倒计时结束 -> 隐藏倒计时,显示发送按钮(监视倒计时的 finish 事件处理) } catch (err) { // try 里面任何代码的错误都会进入 catch // 不同的错误需要有不同的提示,那就需要判断了 let message = '' if (err && err.response && err.response.status === 429) { // 发送短信失败的错误提示 message = '发送太频繁了,请稍后重试' } else if (err.name === 'mobile') { // 表单验证失败的错误提示 message = err.message } else { // 未知错误 message = '发送失败,请稍后重试' } // 提示用户 this.$toast({ message, position: 'top' }) } // 无论发送验证码成功还是失败,最后都要关闭发送按钮的 loading 状态 + this.isSendSmsLoading = false }
处理倒计时
我们可以使用 Vant 中的 CountDown 倒计时 轻松的实现这个功能。下面是具体的处理流程。
在 data 中添加数据用来控制倒计时的显示和隐藏:
这里的time是通过毫秒来算的,这里的1000其实就是1秒,再乘以60的话,其实就是进行了60秒的倒计时了
format="ss s"这种形式的话,其实就是因为从0到60的话,如果是从0到9的话,就是通过format“s“这个个位数的格式了
如果是从10开始一直到60的话就是通过format("ss”)这种格式了
async onSendSms () { try { // 校验手机号码 await this.$refs['login-form'].validate('mobile') // 验证通过,请求发送验证码 this.isSendSmsLoading = true // 展示按钮的 loading 状态,防止网络慢用户多次点击触发发送行为 await sendSms(this.user.mobile) // 短信发出去了,显示倒计时,关闭发送按钮 + this.isCountDownShow = true // 倒计时结束 -> 隐藏倒计时,显示发送按钮(监视倒计时的 finish 事件处理) } catch (err) { // try 里面任何代码的错误都会进入 catch // 不同的错误需要有不同的提示,那就需要判断了 let message = '' if (err && err.response && err.response.status === 429) { // 发送短信失败的错误提示 message = '发送太频繁了,请稍后重试' } else if (err.name === 'mobile') { // 表单验证失败的错误提示 message = err.message } else { // 未知错误 message = '发送失败,请稍后重试' } // 提示用户 this.$toast({ message, position: 'top' }) } // 无论发送验证码成功还是失败,最后都要关闭发送按钮的 loading 状态 this.isSendSmsLoading = false // 发送失败,显示发送按钮,关闭倒计时 + this.isCountDownShow = false }
用户如果成功的登陆之后,接口会返回一个token值,这个值就是进入到小程序的一个令牌了
但是我们只有在第一次用户登录成功之后才能拿到 Token。
所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。
往哪儿存?
- 本地存储(不推荐)
- 获取麻烦
- 数据不是响应式
- Vuex 容器(推荐)
- 获取方便
- 响应式的
使用容器存储 Token 的思路:
- 登录成功,将 Token 存储到 Vuex 容器中
- 获取方便
- 响应式
- 为了持久化,还需要把 Token 放到本地存储
- 持久化
async onLogin () { // Toast.loading({ this.$toast.loading({ message: '登录中...', // 提示文本 forbidClick: true, // 禁止背景点击 duration: 0 // 展示时长(ms),值为 0 时,toast 不会消失 }) // 1. 找到数据接口 // 2. 封装请求方法 // 3. 请求调用登录 try { const { data } = await login(this.user) // 4. 处理响应结果 this.$toast.success('登录成功') // 将后端返回的用户登录状态(token等数据)放到 Vuex 容器中 + this.$store.commit('setUser', data.data) } catch (err) { console.log(err) this.$toast.fail('登录失败,手机号或验证码错误') } },
这里的
this.$store.commit('setUser', data.data)
因为此时的this表示的是src这个目录
store是src目录下的一个子目录,可以就进入到这个文件里面,请求调用setUser这个函数
import Vue from 'vue' import Vuex from 'vuex' import { getItem, setItem } from '@/utils/storage' Vue.use(Vuex) // 这样做的目的可以避免访问和获取数据的名字不一致导致的问题 const USER_KEY = 'TOUTIAO_USER' export default new Vuex.Store({ state: { user: getItem(USER_KEY) // 当前登录用户的登录状态(token等数据) // user: JSON.parse(window.localStorage.getItem('user')) // 当前登录用户的登录状态(token等数据) }, mutations: { setUser (state, data) { state.user = data // 为了防止页面刷新数据丢失,我们还需要把数据放到本地存储中,这里仅仅是为了持久化数据 setItem(USER_KEY, state.user) // window.localStorage.setItem('user', JSON.stringify(state.user)) } }, actions: { }, modules: { } })
mutations里面的state参数其实是上面的那个state他表示一种全局的状态
关于 Token 过期问题
登录成功之后后端会返回两个 Token:
token
:访问令牌,有效期2小时
refresh_token
:刷新令牌,有效期14天,用于访问令牌过期之后重新获取新的访问令牌
我们的项目接口中设定的 Token
有效期是 2 小时
,超过有效期服务端会返回 401
表示 Token 无效或过期了。
为什么过期时间这么短?
- 为了安全,例如 Token 被别人盗用
过期了怎么办?
- 让用户重新登录,用户体验太差了
- 使用
refresh_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;①这里面导入了router的原因就是,因为如果是token和刷新token都失效的话,只要让用户跳转到登陆页面中去的,这个时候就要路由导航了
②导入的store就是为了获取vuex中存放的token数据
③request.defaults.transformResponse的意思:
配置的默认值/defaults
你可以指定将被用在各个请求的配置默认值
这里的transformresponse的意思就是,改变后端返回数据
配置处理后端返回数据中超出 js 安全整数范围问题