赞
踩
目标
- 能使用 Vue CLI 创建项目
- 了解 Vant 组件库的导入方式
- 掌握制作使用字体图标的方式
- 掌握如何在 Vue 项目中处理 REM 适配
- 理解 axios 请求模块的封装
如果你还没有安装 VueCLI,请执行下面的命令安装或是升级:
npm install --global @vue/cli
- 1
在命令行中输入以下命令创建 Vue 项目:
vue create toutiao-m
Vue CLI v4.2.3
? Please pick a preset:
default (babel, eslint)
> Manually select features
default:默认勾选 babel、eslint,回车之后直接进入装包
manually:自定义勾选特性配置,选择完毕之后,才会进入装包
选择第 2 种:手动选择特性,支持更多自定义选项
? Please pick a preset: Manually select features
? Check the features needed for your project:
(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
(*) Router
(*) Vuex
(*) CSS Pre-processors
>(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing
分别选择:
Babel:es6 转 es5
Router:路由
Vuex:数据容器,存储共享数据
CSS Pre-processors:CSS 预处理器,后面会提示你选择 less、sass、stylus 等
Linter / Formatter:代码格式校验
? Use history mode for router? (Requires proper server setup for index fallback in production) (Y/n) n
是否使用 history 路由模式,这里输入 n 不使用
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default):
Sass/SCSS (with dart-sass)
Sass/SCSS (with node-sass)
> Less
Stylus
选择 CSS 预处理器,这里选择我们熟悉的 Less
? Pick a linter / formatter config:
ESLint with error prevention only
ESLint + Airbnb config
> ESLint + Standard config
ESLint + Prettier
选择校验工具,这里选择 ESLint + Standard config
? Pick additional lint features:
(*) Lint on save
>(*) Lint and fix on commit
选择在什么时机下触发代码格式校验:
- Lint on save:每当保存文件的时候
- Lint and fix on commit:每当执行
git commit
提交的时候
这里建议两个都选上,更严谨。
? Where do you prefer placing config for Babel, ESLint, etc.? (Use arrow keys)
> In dedicated config files
In package.json
Babel、ESLint 等工具会有一些额外的配置文件,这里的意思是问你将这些工具相关的配置文件写到哪里:
- In dedicated config files:分别保存到单独的配置文件
- In package.json:保存到 package.json 文件中
这里建议选择第 1 个,保存到单独的配置文件,这样方便我们做自定义配置。
? Save this as a preset for future projects? (y/N) N
这里里是问你是否需要将刚才选择的一系列配置保存起来,然后它可以帮你记住上面的一系列选择,以便下次直接重用。
这里根据自己需要输入 y 或者 n,我这里输入 n 不需要。
✨ Creating project in C:\Users\LPZ\Desktop\topline-m-fe89\topline-m-89.
� Initializing git repository...
⚙ Installing CLI plugins. This might take a while...
[ ........] - extract:object-keys: sill extract json5@2.1.1
向导配置结束,开始装包。
安装包的时间可能较长,请耐心等待…
⚓ Running completion hooks...
� Generating README.md...
� Successfully created project topline-m-89.
� Get started with the following commands:
$ cd topline-m
$ npm run serve
安装结束,命令提示你项目创建成功,按照命令行的提示在终端中分别输入:
# 进入你的项目目录
cd toutiao-webapp
# 启动开发服务
npm run serve
DONE Compiled successfully in 7527ms
App running at:
- Local: http://localhost:8080/
- Network: http://192.168.10.216:8080/
Note that the development build is not optimized.
To create a production build, run npm run build.
启动成功,命令行中输出项目的 http 访问地址。
打开浏览器,输入其中任何一个地址进行访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UynFzUoP-1654274042910)(assets/1582010300149-9932febe-2625-4ee7-986f-9190ab449fec.png)]
如果能看到该页面,恭喜你,项目创建成功了。
几个好处:
(1)创建远程仓库
(2)将本地仓库推到线上
如果没有本地仓库。
# 删除项目中的 `.git` 文件夹 # 创建本地仓库 git init # 将文件添加到暂存区 git add 文件 # 提交历史记录 git commit "提交日志" # master分支创建release分支 git branch release # master分支创建develop分支 git branch develop # 添加远端仓库地址 git remote add origin 你的远程仓库地址 # 推送提交 git push -u origin master
如果已有本地仓库(Vue CLI 已经帮我们初始化好了)。
# 添加远端仓库地址
git remote add origin 你的远程仓库地址
# 推送提交
git push -u origin master
如果之后项目代码有了变动需要提交:
git add
git commit
git push
默认生成的目录结构不满足我们的开发需求,所以这里需要做一些自定义改动。
这里主要就是下面的两个工作:
1、将 App.vue
修改为
<template>
<div id="app">
<h1>黑马头条</h1>
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style scoped lang="less"></style>
2、将 router/index.js
修改为
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
routes
})
export default router
3、删除
4、创建以下几个目录
main.js
中加载全局样式 import './styles/index.less'
调整之后的目录结构如下。
. ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ └── index.html └── src ├── api ├── App.vue ├── assets ├── components ├── main.js ├── router ├── utils ├── styles ├── store └── views
设计师为我们单独提供了设计稿中的图标,为了方便使用,我们在这里把它制作为字体图标。
制作字体图标的工具有很多,在这里我们推荐大家使用:https://www.iconfont.cn/。
一、注册账户
直接选择第三方登录即可
二、创建项目
三、上传图标到项目
四、生成链接
五、配置到项目中使用
一种方式是将 SVG 图标 包装为 Vue 组件来使用。
一种方式是将 SVG 制作为字体图标来使用:
Vant 是有赞商城前端开发团队开发的一个基于 Vue.js 的移动端组件库,它提供了非常丰富的移动端功能组件,简单易用。
下面是在 Vant 官网中列出的一些优点:
在我们的项目中主要使用 Vant 作为核心组件库,下面我们根据官方文档将 Vant 导入项目中。
将 Vant 引入项目一共有四种方式:
方式一:自动按需引入组件
方式二:手动按需引入组件
方式三:导入所有组件
方式四:通过 CDN 引入
vant
访问到所有组件。这里建议为了前期开发的便利性我们选择方式三:导入所有组件,在最后做打包优化的时候根据需求配置按需加载以降低打包体积大小。
1、安装 Vant
npm i vant
2、在 main.js
中加载注册 Vant 组件
import Vue from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'
Vue.use(Vant)
3、查阅文档使用组件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t2dwTpvr-1654274042918)(assets/1582017539392-6c48b63f-8e8b-4ef2-b4fa-ddeb059cec04.png)]
Vant 的文档非常清晰,左侧是组件目录导航,中间是效果代码,右边是效果预览。
例如我们在根组件使用 Vant 中的组件:
<van-button type="default">默认按钮</van-button>
<van-button type="primary">主要按钮</van-button>
<van-button type="info">信息按钮</van-button>
<van-button type="warning">警告按钮</van-button>
<van-button type="danger">危险按钮</van-button>
<van-cell-group>
<van-cell title="单元格" value="内容" />
<van-cell title="单元格" value="内容" label="描述信息" />
</van-cell-group>
如果在页面中能够正常的看到下面的效果,则说明 Vant 导入成功了。
Vant 中的样式默认使用 px
作为单位,如果需要使用 rem
单位,推荐使用以下两个工具:
下面我们分别将这两个工具配置到项目中完成 REM 适配。
一、使用 lib-flexible 动态设置 REM 基准值(html 标签的字体大小)
1、安装
# yarn add amfe-flexible
npm i amfe-flexible
2、然后在 main.js
中加载执行该模块
import 'amfe-flexible'
最后测试:在浏览器中切换不同的手机设备尺寸,观察 html 标签 font-size
的变化。
例如在 iPhone 6/7/8 设备下,html 标签字体大小为 37.5 px
例如在 iPhone 6/7/8 Plus 设备下,html 标签字体大小为 41.4 px
二、使用 postcss-pxtorem 将 px
转为 rem
1、安装
# yarn add -D postcss-pxtorem
# -D 是 --save-dev 的简写
npm install postcss-pxtorem -D
2、然后在项目根目录中创建 .postcssrc.js
文件
module.exports = {
plugins: {
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 8']
},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
3、配置完毕,重新启动服务
最后测试:刷新浏览器页面,审查元素的样式查看是否已将 px
转换为 rem
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZXfjWkV-1654274042919)(assets/1582035408807-1adb02e6-4576-48b6-8fb9-b3a0c57ead0d.png)]
这是没有配置转换之前的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RVDEAyG7-1654274042919)(assets/1582035305177-d13c0a83-65bf-4fe5-a509-83bbb3bbf627.png)]
这是转换之后的,可以看到 px 都被转换为了 rem。
需要注意的是:
px
,例如 <div style="width: 200px;"></div>
.postcssrc.js
配置文件module.exports = {
plugins: {
'autoprefixer': {
browsers: ['Android >= 4.0', 'iOS >= 8']
},
'postcss-pxtorem': {
rootValue: 37.5,
propList: ['*']
}
}
}
.postcssrc.js
是 PostCSS 的配置文件。
(1)PostCSS 介绍
PostCSS 是一个处理 CSS 的处理工具,本身功能比较单一,它主要负责解析 CSS 代码,再交由插件来进行处理,它的插件体系非常强大,所能进行的操作是多种多样的,例如:
目前 PostCSS 已经有 200 多个功能各异的插件。开发人员也可以根据项目的需要,开发出自己的 PostCSS 插件。
PostCSS 一般不单独使用,而是与已有的构建工具进行集成。
Vue CLI 默认集成了 PostCSS,并且默认开启了 autoprefixer 插件。
Vue CLI 内部使用了 PostCSS。
你可以通过
.postcssrc
或任何 postcss-load-config 支持的配置源来配置 PostCSS。也可以通过vue.config.js
中的css.loaderOptions.postcss
配置 postcss-loader。我们默认开启了 autoprefixer。如果要配置目标浏览器,可使用
package.json
的 browserslist 字段。
(2)Autoprefixer 插件的配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-glIW8z3U-1654274042920)(assets/image-20200319104557718.png)]
autoprefixer 是一个自动添加浏览器前缀的 PostCss 插件,browsers
用来配置兼容的浏览器版本信息,但是写在这里的话会引起编译器警告。
Replace Autoprefixer browsers option to Browserslist config.
Use browserslist key in package.json or .browserslistrc file.
Using browsers option can cause errors. Browserslist config
can be used for Babel, Autoprefixer, postcss-normalize and other tools.
If you really need to use option, rename it to overrideBrowserslist.
Learn more at:
https://github.com/browserslist/browserslist#readme
https://twitter.com/browserslist
警告意思就是说你应该将 browsers
选项写到 package.json
或 .browserlistrc
文件中。
[Android]
>= 4.0
[iOS]
>= 8
具体语法请参考这里。
(3)postcss-pxtorem 插件的配置
rootValue
:表示根元素字体大小,它会根据根元素大小进行单位转换propList
用来设定可以从 px 转为 rem 的属性
*
就是所有属性都要转换,width
就是仅转换 width
属性rootValue
应该如何设置呢?
如果你使用的是基于 lib-flexable 的 REM 适配方案,则应该设置为你的设计稿的十分之一。
例如设计稿是 750 宽,则应该设置为 75。
大多数设计稿的原型都是以 iphone6 为原型,iphone6 设备的宽是 750,我们的设计稿也是这样。
但是 Vant 建议设置为 37.5,为什么呢?
因为 Vant 是基于 375 写的,所以如果你设置为 75 的话,Vant 的样式就小了一半。
所以如果设置为 37.5
的话,Vant 的样式是没有问题的,但是我们在测量设计稿的时候都必须除2才能使用,否则就会变得很大。
这样做其实也没有问题,但是有没有更好的办法呢?我就想实现测量多少写多少(不用换算)。于是聪明的你就想,可以不可以这样来做?
rootValue
设置为 37.5 来转换rootValue
来转换通过查阅文档我们可以看到 rootValue
支持两种参数类型:
所以我们修改配置如下:
/** * PostCSS 配置文件 */ module.exports = { // 配置要使用的 PostCSS 插件 plugins: { // 配置使用 autoprefixer 插件 // 作用:生成浏览器 CSS 样式规则前缀 // VueCLI 内部已经配置了 autoprefixer 插件 // 所以又配置了一次,所以产生冲突了 // 'autoprefixer': { // autoprefixer 插件的配置 // // 配置要兼容到的环境信息 // browsers: ['Android >= 4.0', 'iOS >= 8'] // }, // 配置使用 postcss-pxtorem 插件 // 作用:把 px 转为 rem 'postcss-pxtorem': { rootValue ({ file }) { return file.indexOf('vant') !== -1 ? 37.5 : 75 }, propList: ['*'] } } }
配置完毕,把服务重启一下,最后测试,very good。
和之前项目一样,这里我们还是使用 axios 作为我们项目中的请求库,为了方便使用,我们把它封装为一个请求模块,在需要的时候直接加载即可。
1、安装 axios
npm i axios
2、创建 src/utils/request.js
/**
* 封装 axios 请求模块
*/
import axios from "axios"
const request = axios.create({
baseURL: "http://ttapi.research.itcast.cn/" // 基础路径
})
export default request
3、如何使用
Vue.prototype
原型对象中,然后在组件中通过 this.xxx
直接访问在我们的项目中建议使用方式二,更推荐(在随后的业务功能中我们就能学到)。
目标
- 能实现登录页面的布局
- 能实现基本登录功能
- 能掌握 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>
思路:
一、根据接口要求绑定获取表单数据
1、在登录页面组件的实例选项 data 中添加 user
数据字段
...
data () {
return {
user: {
mobile: '',
code: ''
}
}
}
2、在表单中使用 v-model
绑定对应数据
<!-- van-cell-group 仅仅是提供了一个上下外边框,能看到包裹的区域 --> <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("提示内容");
// loading 转圈圈提示
Toast.loading({
duration: 0, // 持续展示 toast
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, // 持续时间,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
正常网络。
参考文档:Form 表单验证
<template> <div class="login-container"> <!-- 导航栏 --> <van-nav-bar class="page-nav-bar" title="登录" /> <!-- /导航栏 --> <!-- 登录表单 --> <!-- 表单验证: 1、给 van-field 组件配置 rules 验证规则 参考文档:https://youzan.github.io/vant/#/zh-CN/form#rule-shu-ju-jie-gou 2、当表单提交的时候会自动触发表单验证 如果验证通过,会触发 submit 事件 如果验证失败,不会触发 submit --> <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 () { // 1. 获取表单数据 const user = this.user // TODO: 2. 表单验证 // 3. 提交表单请求登录 this.$toast.loading({ message: '登录中...', forbidClick: true, // 禁用背景点击 duration: 0 // 持续时间,默认 2000,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('登录失败,请稍后重试') } } // 4. 根据请求响应结果处理后续操作 } } } </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')
// 1. 校验手机号
try {
await this.$refs.loginForm.validate('mobile')
} catch (err) {
return console.log('验证失败', err)
}
// 2. 验证通过,显示倒计时
// 3. 请求发送验证码
}
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 () { // 1. 校验手机号 try { await this.$refs.loginForm.validate('mobile') } catch (err) { return console.log('验证失败', err) } // 2. 验证通过,显示倒计时 this.isCountDownShow = true // 3. 请求发送验证码 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。
所以为了能在其它模块中获取到 Token 数据,我们需要把它存储到一个公共的位置,方便随时取用。
往哪儿存?
使用容器存储 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')) // user: null }, mutations: { setUser (state, user) { state.user = user window.localStorage.setItem('TOUTIAO_USER', JSON.stringify(user)) } }, actions: { }, modules: { } })
2、登录成功以后将后端返回的 token 相关数据存储到容器中
async onLogin () { // const loginToast = this.$toast.loading({ this.$toast.loading({ duration: 0, // 持续时间,0表示持续展示不停止 forbidClick: true, // 是否禁止背景点击 message: '登录中...' // 提示消息 }) try { const res = await login(this.user) // res.data.data => { token: 'xxx', refresh_token: 'xxx' } + this.$store.commit('setUser', res.data.data) // 提示 success 或者 fail 的时候,会先把其它的 toast 先清除 this.$toast.success('登录成功') } catch (err) { console.log('登录失败', err) this.$toast.fail('登录失败,手机号或验证码错误') } // 停止 loading,它会把当前页面中所有的 toast 都给清除 // loginToast.clear() }
创建 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
:访问令牌,有效期2小时refresh_token
:刷新令牌,有效期14天,用于访问令牌过期之后重新获取新的访问令牌我们的项目接口中设定的 Token
有效期是 2 小时
,超过有效期服务端会返回 401
表示 Token 无效或过期了。
为什么过期时间这么短?
过期了怎么办?
refresh_token
解决 token
过期如何使用 refresh_token
解决 token
过期?
到课程的后面我们开发的业务功能丰富起来之后,再给大家讲解 Token 过期处理。
大家需要注意的是在学习测试的时候如果收到 401 响应码,请重新登录再测试。
概述:服务器生成token的过程中,会有两个时间,一个是token失效时间,一个是token刷新时间,刷新时间肯定比失效时间长,当用户的 token
过期时,你可以拿着过期的token去换取新的token,来保持用户的登陆状态,当然你这个过期token的过期时间必须在刷新时间之内,如果超出了刷新时间,那么返回的依旧是 401。
处理流程:
在请求的响应拦截器中统一处理 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;
通过分析页面,我们可以看到,首页、问答、视频、我的 都使用的是同一个底部标签栏,我们没必要在每个页面中都写一个,所以为了通用方便,我们可以使用 Vue Router 的嵌套路由来处理。
一、创建 tabbar 组件并配置路由
这里主要使用到的 Vant 组件:
1、创建 src/views/layout/index.vue
<template> <div class="layout-container"> <!-- 子路由出口 --> <router-view /> <!-- /子路由出口 --> <!-- 标签导航栏 --> <!-- route: 开启路由模式 --> <van-tabbar class="layout-tabbar" route> <van-tabbar-item to="/"> <i slot="icon" class="toutiao toutiao-shouye"></i> <span class="text">首页</span> </van-tabbar-item> <van-tabbar-item to="/qa"> <i slot="icon" class="toutiao toutiao-wenda"></i> <span class="text">问答</span> </van-tabbar-item> <van-tabbar-item to="/video"> <i slot="icon" class="toutiao toutiao-shipin"></i> <span class="text">视频</span> </van-tabbar-item> <van-tabbar-item to="/my"> <i slot="icon" class="toutiao toutiao-wode"></i> <span class="text">我的</span> </van-tabbar-item> </van-tabbar> <!-- /标签导航栏 --> </div> </template> <script> export default { name: 'LayoutIndex', components: {}, props: {}, data () { return { } }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped lang="less"> .layout-container { .layout-tabbar { i.toutiao { font-size: 40px; } span.text { font-size: 20px; } } } </style>
2、然后将 layout 组件配置到一级路由
{
path: '/',
component: () => import('@/views/layout')
}
访问 /
测试。
二、分别创建首页、问答、视频、我的页面组件
首页组件:
<template> <div class="home-container">首页</div> </template> <script> export default { name: 'HomePage', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped></style>
问答组件:
<template> <div class="qa-container">问答</div> </template> <script> export default { name: 'QaPage', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped></style>
视频组件:
<template> <div class="video-container">首页</div> </template> <script> export default { name: 'VideoPage', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped></style>
我的组件:
<template> <div class="my-container">首页</div> </template> <script> export default { name: 'MyPage', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped></style>
二、将四个主页面配置为 tab-bar 的子路由
{ path: '/', name: 'tab-bar', component: () => import('@/views/tab-bar'), 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') } ] }
最后测试。
<template> <div class="my-container"> <div class="header"> <img class="mobile-img" src="~@/assets/mobile.png" @click="$router.push('/login')" > </div> <div class="grid-nav"></div> <van-cell title="消息通知" is-link url="" /> <van-cell title="实名认证" is-link url="" /> <van-cell title="用户反馈" is-link url="" /> <van-cell title="小智同学" is-link url="" /> <van-cell title="系统设置" is-link url="" /> </div> </template> <script> export default { name: 'MyIndex', components: {}, props: {}, data () { return {} }, computed: {}, watch: {}, created () {}, mounted () {}, methods: {} } </script> <style scoped lang="less"> .my-container { > .header { height: 361px; background: url("~@/assets/banner.png") no-repeat; background-size: cover; display: flex; justify-content: center; align-items: center; .mobile-img { width: 132px; height: 132px; } } } </style>
<!-- 已登录:用户信息 --> <div v-if="$store.state.user" class="user-info-wrap"> ... </div> <!-- /已登录:用户信息 --> <!-- 未登录 --> <div v-else class="not-login" @click="$router.push('/login')"> ... </div> <!-- /未登录 --> <!-- 退出 --> <van-cell-group v-if="$store.state.user"> ... </van-cell-group> <!-- /退出 -->
1、给退出按钮注册点击事件
2、退出处理
onLogout () {
// 退出提示
// 在组件中需要使用 this.$dialog 来调用弹框组件
this.$dialog.confirm({
title: '确认退出吗?'
}).then(() => {
// on confirm
// 确认退出:清除登录状态(容器中的 user + 本地存储中的 user)
this.$store.commit('setUser', null)
}).catch(() => {
// on cancel
console.log('取消执行这里')
})
}
最后测试。
步骤:
1、在 api/user.js
中添加封装数据接口
/** * 获取用户自己的信息 */ export const getUserInfo = () => { return request({ method: 'GET', url: '/app/v1_0/user', // 发送请求头数据 headers: { // 注意:该接口需要授权才能访问 // token的数据格式:Bearer token数据,注意 Bearer 后面有个空格 Authorization: `Bearer ${store.state.user.token}` } }) }
2、在 views/my/index.vue
请求加载数据
+ import { getUserInfo } from '@/api/user' export default { name: 'MyPage', components: {}, props: {}, data () { return { + userInfo: {} // 用户信息 } }, computed: {}, watch: {}, +++ created () { // 初始化的时候,如果用户登录了,我才请求获取当前登录用户的信息 if (this.$store.state.user) { this.loadUser() } }, mounted () {}, methods: { +++ async loadUser () { try { const { data } = await getUserInfo() this.user = data.data } catch (err) { console.log(err) this.$toast('获取数据失败') } } } }
3、模板绑定
项目中的接口除了登录之外大多数都需要提供 token 才有访问权限。
通过接口文档可以看到,后端接口要求我们将 token 放到请求头 Header
中并以下面的格式发送。
字段名称:
Authorization
字段值:
Bearer token
,注意Bearer
和token
之间有一个空格
方式一:在每次请求的时候手动添加(麻烦)。
axios({
method: "",
url: "",
headers: {
Authorization: "Bearer token"
}
})
方式二:使用请求拦截器统一添加(推荐,更方便)。
在 src/utils/request.js
中添加拦截器统一设置 token:
/** * 请求模块 */ import axios from 'axios' import store from '@/store' const request = axios.create({ baseURL: 'http://ttapi.research.itcast.cn/' // 接口的基准路径 }) // 请求拦截器 // Add a request interceptor request.interceptors.request.use(function (config) { // Do something before request is sent // config :本次请求的配置对象 // config 里面有一个属性:headers const { user } = store.state if (user && user.token) { config.headers.Authorization = `Bearer ${user.token}` } return config }, function (error) { // Do something with request error return Promise.reject(error) }) // 响应拦截器 export default request
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。