一、初始化项目
1.1 创建并梳理项目的结构
-
运行如下的命令,基于 vue-cli 创建 Vue2 的工程化项目:
vue create toutiao
-
重置
App.vue
根组件中的代码如下:<template> <div>App 根组件</div> </template> <script> export default { name: 'App' } </script> <style lang="less" scoped></style>
-
清空
/src/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
-
删除
components
目录下的HelloWorld.vue
组件 -
删除
views
目录下的About.vue
和Home.vue
组件 -
执行
npm run serve
命令,把项目运行起来看效果
#1.2 配置 vant 组件库
-
运行如下的命令,在项目中安装 vant 组件库:
npm i vant@2.12.12 -S
-
在
main.js
入口文件中,完整导入并注册所有的 vant 组件:import Vue from 'vue' import Vant from 'vant' import 'vant/lib/index.css' Vue.use(Vant)
-
在 App.vue 根组件中,基于
Button
按钮组件测试是否配置成功:<template> <div> <p>App 根组件</p> <van-button type="primary">主要按钮</van-button> <van-button type="info">信息按钮</van-button> <van-button type="default">默认按钮</van-button> <van-button type="warning">警告按钮</van-button> <van-button type="danger">危险按钮</van-button> </div> </template>
-
一次性、完整导入并注册所有 vant 组件的优缺点:
- 优点:所有的 vant 组件都进行了全局的注册。在每个组件中,不再需要按需引入并注册组件了。
- 缺点:项目中没有用到的组件也会被打包进来,导致打包体积过大的问题(此问题在项目发布时,可通过 CDN 加速解决)。
#1.3 安装和配置 axios
基于 Vant 的 NavBar 导航栏 组件,渲染登录组件的头部区域
2.3 覆盖 NavBar 组件的默认样式
-
安装:
npm i axios@0.21.1 -S
-
在
src
目录之下,创建utils
子目录,并在utils
目录下新建request.js
网络请求模块如下:import axios from 'axios' // 调用 axios.create() 方法,创建 axios 的实例对象 const instance = axios.create({ // 请求根路径 baseURL: 'http://www.liulongbin.top:8000' }) export default instance
二. 登录功能
2.1 使用路由渲染登录组件
-
在
/src/views
目录之下,创建Login
文件夹,并在其下新建Login.vue
登录组件,初始化组件的基本结构如下:<template> <div>登录组件</div> </template> <script> export default { // name 是当前组件的名称(建议为每个组件都指定唯一的 name 名称) name: 'Login' } </script> <style lang="less" scoped></style>
-
在
/src/router/index.js
路由模块中,导入需要通过路由渲染的login.vue
登录组件:// 导入需要的路由组件 import Login from '@/views/Login/Login.vue'
-
在路由模块的
routes
数组中,声明登录组件的路由规则如下:const routes = [ // 带有 name 名称的路由规则,叫做“命名路由” { path: '/login', component: Login, name: 'login' } ]
-
在
App.vue
根组件中声明路由占位符,当用户访问http://localhost:8080/#/login
地址的时候,会渲染出登录组件:<template> <div> <!-- 路由占位符 --> <router-view></router-view> </div> </template> <script> export default { name: 'App' } </script> <style lang="less" scoped></style>
-
渲染登录组件的 header 头部区域:
<template> <div> <!-- NavBar 组件:只需提供 title 标题 --> <van-nav-bar title="黑马头条 - 登录" /> </div> </template>
-
为
<van-nav-bar>
组件添加fixed
属性,实现顶部固定定位的效果:<van-nav-bar title="黑马头条 - 登录" fixed />
-
为
Login.vue
组件最外层的div
元素添加名为login-container
的类名,防止<van-nav-bar>
组件遮挡其它元素:<template> <div class="login-container"> <!-- NavBar 组件:只需提供 title 标题 --> <van-nav-bar title="黑马头条 - 登录" fixed /> </div> </template>
-
在
Login.vue
组件的style
节点中声明如下的类名:.login-container { padding-top: 46px; }
方案 :全局样式表 - 普通程序员的万能招式
-
在
src
目录下新建index.less
全局样式表,通过审查元素的方式找到对应的 class 类名,进行样式的覆盖:// 覆盖 NavBar 组件的默认样式 .van-nav-bar { background-color: #007bff; .van-nav-bar__title { color: white; font-size: 14px; } }
-
在
main.js
中导入全局样式表即可:// 导入 Vant 和 组件的样式表 import Vant from 'vant' import 'vant/lib/index.css' // 导入全局样式表 import './index.less' // 注册全局插件 Vue.use(Vant)
2.4 登录功能
2.4.1 渲染登录的表单
基于 Vant 的 Form 表单组件,可以快速渲染出登录表单的基本结构
-
在
Login.vue
组件的script
节点中,声明如下的data
数据:export default { name: 'Login', data() { return { // 登录表单的数据,最终要双向绑定到 form 这个数据对象上 form: { // 用户的手机号 mobile: '', // 登录的密码 code: '' } } } }
-
在
Login.vue
组件的模板结构中定义如下的 DOM 结构:<!-- 登录的表单 --> <van-form> <van-field v-model="form.mobile" type="tel" label="手机号码" placeholder="请输入手机号码" required></van-field> <van-field v-model="form.code" type="password" label="登录密码" placeholder="请输入登录密码" required></van-field> <div style="margin: 16px;"> <van-button round block type="info" native-type="submit">提交</van-button> </div> </van-form>
2.4.2 添加非空校验规则
-
在
Login.vue
组件的 data 中声明登录表单的校验规则对象,里面包含了手机号和密码的校验规则:data() { return { // 表单的校验规则对象 rules: { // 手机号的校验规则 mobile: [{ required: true, message: '请填写您的手机号', trigger: 'onBlur' }], // 密码的校验规则 code: [{ required: true, message: '请填写您的密码', trigger: 'onBlur' }] } } }
-
在
Login.vue
组件的模板结构中,为每个<van-field>
组件应用对应的校验规则:<!-- 手机号的表单项 --> <van-field type="tel" v-model="form.mobile" label="手机号码" placeholder="请输入手机号码" required :rules="rules.mobile"> </van-field> <!-- 登录密码的表单项 --> <van-field type="password" v-model="form.code" label="登录密码" placeholder="请输入登录密码" required :rules="rules.code"> </van-field>
#2.4.3 通过 pattern 进行正则校验
-
登录的手机号除了是必填项之外,还必须是以数字 1 开头的 11 位数字。此时,可以为手机号通过 pattern 进行正则校验:
// 只有同时满足以下两个验证规则,才能验证通过 mobile: [ // 必填项的校验规则 { required: true, message: '请填写您的手机号', trigger: 'onBlur' }, // 11 位手机号的校验规则 { pattern: /^1\d{10}$/, message: '请填写正确的手机号', trigger: 'onBlur' } ]
-
其中,
pattern
属性用来指定正则表达式。
#2.4.4 监听表单的提交事件
-
为
<van-form>
组件绑定submit
事件处理函数:<!-- 登录的表单 --> <van-form @submit="login"></van-form>
-
在
Login.vue
组件中的 methods 节点下声明login
事件处理函数:methods: { login() { // 只有当表单数据校验通过之后,才会调用此 login 函数 console.log('ok') // TODO:调用 API 接口,发起登录的请求 } }
#2.4.5 封装登录的 API 接口
-
在
src
目录之下,新建api
文件夹,并在其下新建userAPI.js
模块:import request from '@/utils/request.js' // 登录的 API 接口 export const loginAPI = data => { return request.post('/v1_0/authorizations', data) }
#2.4.6 调用登录的 API 接口
-
在
Login.vue
组件中,按需导入登录的 API 接口:import { loginAPI } from '@/api/userAPI'
-
在
<van-form>
组件的submit
事件处理函数中,调用loginAPI
接口:methods: { async login() { // 只有当表单数据校验通过之后,才会调用此 login 函数 const res = await loginAPI(this.form) // 当数据请求成功之后,res.data 中存储的就是服务器响应回来的数据 console.log(res) } }
#2.4.7 使用解构赋值
-
可以使用对象的解构赋值,直接从 res 中解构、得到服务器响应回来的数据:
methods: { async login() { // 只有当表单数据校验通过之后,才会调用此 login 函数 const { data: res } = await loginAPI(this.form) console.log(res) // 判断是否登录成功了 if (res.message === 'OK') { // TODO1:把登录成功的结果,存储到 vuex 中 // TODO2:登录成功后,跳转到主页 } } }
2.5 token 的存储
#2.5.1 把 token 存储到 vuex
-
在 vuex 模块中声明如下的 state 数据节点,定义专门用来存储 token 信息的
tokenInfo
对象:export default new Vuex.Store({ state: { // 用来存储 token 信息的对象,将来这个对象中会包含两个属性 { token, refresh_token } tokenInfo: {} } })
-
在 mutations 节点下,定义名为
updateTokenInfo
的 Mutation 方法,专门用来更新tokenInfo
的值:mutations: { // 更新 tokenInfo 数据的方法 updateTokenInfo(state, payload) { // 把提交过来的 payload 对象,作为 tokenInfo 的值 state.tokenInfo = payload // 测试 state 中是否有数据 console.log(state) } },
-
在
Login.vue
组件中,通过mapMutations
辅助方法,把updateTokenInfo
映射为组件的 methods 方法:// 1. 按需导入辅助方法 import { mapMutations } from 'vuex' export default { methods: { // 2. 映射 mutations 中的方法 ...mapMutations(['updateTokenInfo']), async login() { const { data: res } = await loginAPI(this.form) console.log(res) if (res.message === 'OK') { // 3. 把登录成功的结果,存储到 vuex 中 this.updateTokenInfo(res.data) // 4. 登录成功后,跳转到主页 this.$router.push('/') } } } }
#2.5.2 持久化存储 state
存储在 vuex 中的数据都是内存数据,只要浏览器一刷新,vuex 的数据就被清空了。
为了防止这个问题,我们可以把 vuex 中的数据持久化存储到浏览器的 localStorage 中。
-
在
mutations
节点下,定义名为saveStateToStorage
的 Mutation 函数,专门用来把 state 数据持久化存储到 localStorage 中:// 将 state 持久化存储到本地 saveStateToStorage(state) { localStorage.setItem('state', JSON.stringify(state)) }
-
今后,只要
tokenInfo
对象被更新了,就可以调用saveStateToStorage
方法,把最新的 state 持久化存储到本地:// 更新 tokenInfo 数据的方法 updateTokenInfo(state, payload) { state.tokenInfo = payload // 如果希望在 Mutation A 中调用 Mutation B,需要通过 this.commit() 方法来实现 // this 表示当前的 new 出来的 store 实例对象 this.commit('saveStateToStorage') },
#2.5.3 初始化 vuex 时加载本地的 state
当 new Vuex.Store() 时,需要读取 localStorage 中的数据,将读取的结果作为 state 的初始值
-
定义初始的 state 对象,命名为
initState
:// 初始的 state 对象 let initState = { // token 的信息对象 tokenInfo: {} }
-
把
initState
对象作为new Vuex.Store()
时候的 state 初始值:export default new Vuex.Store({ // 为 state 赋初值 state: initState // 省略其它代码... })
-
在
new Vuex.Store()
之前,读取 localStorage 中本地存储的 state 字符串:const stateStr = localStorage.getItem('state')
-
如果
stateStr
的值存在,则证明本地存储中有之前存储的 state 数据,需要转换后赋值给initState
:if (stateStr) { // 加载本地的数据 initState = JSON.parse(stateStr) } export default new Vuex.Store({ // 为 state 赋初值 state: initState // 省略其它代码... })
#2.6 axios 拦截器
中文官方文档地址:axios中文文档|axios中文网 | axios
#2.6.1 什么是 axios 拦截器
拦截器(英文:Interceptors)会在每次发起 ajax 请求和得到响应的时候自动被触发。
- 在组件中发起请求的时候,会触发 axios 的请求拦截器。
- 当 API 接口服务器响应回来数据以后,会触发 axios 的响应拦截器。
#2.6.2 拦截器的好处
拦截器可以在全局拦截每一次 axios 的请求和响应,并统一的进行处理。这样可以避免编写重复的代码。例如下面的需求就是拦截器的典型应用场景:
- 每次调用 API 接口之前,展示 loading 提示效果
- 每次接口调用成功之后,隐藏 loading 提示效果
#2.6.3 axios 拦截器的分类
axios 拦截器分为请求拦截器和响应拦截器。顾名思义,请求拦截器会在每次发起请求的时候被触发;响应拦截器会在每次得到响应之后被触发。
-
定义请求拦截器的固定写法:
// 添加请求拦截器 axios.interceptors.request.use( function(config) { // 在发送请求之前做些什么 return config }, function(error) { // 对请求错误做些什么 return Promise.reject(error) } )
-
定义响应拦截器的固定写法:
// 添加响应拦截器 axios.interceptors.response.use( function(response) { // 对响应数据做点什么 return response }, function(error) { // 对响应错误做点什么 return Promise.reject(error) } )
#2.6.4 基于拦截器实现 loading 效果
基于 Vant 的 Toast 轻提示 组件,可以方便的展示 loading 效果
-
在
src/utils/request.js
模块中,从vant
中按需导入Toast
组件:import { Toast } from 'vant'
-
在请求拦截器中,展示 loading 提示效果:
// 请求拦截器 // 注意:在我们的项目中,是基于 instance 实例来发起 ajax 请求的,因此一定要为 instance 实例绑定请求拦截器 instance.interceptors.request.use( config => { // 展示 loading 效果 Toast.loading({ message: '加载中...', // 文本内容 duration: 0 // 展示时长(ms),值为 0 时,toast 不会消失 }) return config }, error => { return Promise.reject(error) } )
-
在响应拦截器中,隐藏 loading 提示效果:
// 响应拦截器(注意:响应拦截器也应该绑定给 instance 实例) instance.interceptors.response.use( response => { // 隐藏 loading 效果 Toast.clear() return response }, error => { return Promise.reject(error) } )
#2.6.5 基于拦截器添加 token 认证
-
在
request.js
模块中导入 vuex 的模块:import store from '@/store/index'
-
在请求拦截器中,从
store.state
中获取到tokenInfo
对象上的token
值:instance.interceptors.request.use( config => { // 1. 获取 token 值 const tokenStr = store.state.tokenInfo.token }, function(error) { return Promise.reject(error) } )
-
如果
tokenStr
的值不为空, 则为这次请求的请求头添加Authorization
身份认证字段:instance.interceptors.request.use( config => { // 1. 获取 token 值 const tokenStr = store.state.tokenInfo.token // 2. 判断 tokenStr 的值是否为空 if (tokenStr) { // 3. 添加身份认证字段 config.headers.Authorization = `Bearer ${tokenStr}` } return config }, function(error) { return Promise.reject(error) } )