# 主页模块
# 回顾
- 登录模块
- 组件基本布局
- 调用接口实现基本的登录功能
- 表单验证(基于Element-UI提供的规则进行验证)
- 防止表单重复提交
- 异步编程
- 异步的结果不可以用返回值获取
- 必须采用回调函数的方式获取
- 如果异步任务需要保证获取结果的顺序就需要进行回调的嵌套
- 如果嵌套很多层,就会出现回调地狱的问题(代码的可读性较差)
- 所以后来就诞生了一种新的技术:Promise
- 熟悉Promise的基本用法
- 基于Promise方式处理回调地狱问题
- 但是Promise依然需要回调函数,为了进一步改进就诞生了Async函数
- 熟悉Async函数的基本用法
- 基于Async函数处理回调地狱问题
- 关于axios的用法其实本身就支持Promise
# 登录代码优化
- 基于async函数重构登录的接口调用功能
handleLogin () {
this.$refs.loginForm.validate(async valid => {
if (valid) {
// 表单验证如果通过,修改登录按钮的状态
this.loading = true
try {
const ret = await axios.post('http://api-toutiao-web.itheima.net/mp/v1_0/authorizations', {
mobile: this.loginForm.mobile,
code: this.loginForm.code
})
if (ret.data.data.token) {
this.$router.push('/home')
}
} catch (e) {
console.log(e)
// 如果后端接口返回的状态码不是2XX,就会进入catch(响应失败)
// alert('用户名或者密码错误')
// 如果登录失败,让用户可以再次点击按钮
this.$message.error('用户名或者密码错误!')
this.loading = false
}
}
})
}
- try catch用法
<script type="text/javascript">
// 关于try catch语法结构
// 它主要用于解决什么问题? 捕获异常信息;保证后续代码可以继续执行
// 只要代码出错了,那么后续的代码就不再就行执行了
// 我们如何做到,前面的代码出错了,后续代码依然可以执行?try catch
function foo () {
try {
var obj = null
obj.push(123)
} catch (e) {
console.log(e)
}
console.log('-------------------')
}
foo()
</script>
# 主页整体布局
目标:基于Element-UI的相关组件实现主页布局
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-container>
<el-header>Header</el-header>
<el-main>Main</el-main>
</el-container>
</el-container>
- 布局样式
<style lang='less'>
.home {
position: absolute;
height: 100%;
width: 100%;
.el-container {
height: 100%;
.el-aside {
background-color: pink;
}
}
}
</style>
# 顶部导航布局
- 基本结构
<el-header>
<!-- 顶部导航栏布局 -->
<i class="el-icon-s-fold"></i>
<span class="cname">江苏传智播客教育科技股份有限公司</span>
<el-dropdown class='my-dropdown'>
<span class="el-dropdown-link">
<img class="user-icon" src='http://toutiao-img.itheima.net/Ftb-E6bXjx1HlnJHPhe5N6E_seaI' alt />
<span class="user-name">姓名</span>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>git地址</el-dropdown-item>
<el-dropdown-item divided>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-header>
- 布局样式
.el-header {
border-bottom: 1px solid #ddd;
.el-icon-s-fold {
font-size: 30px;
line-height: 60px;
vertical-align: middle;
}
.cname {
font-size: 18px;
margin-left: 5px;
}
.my-dropdown {
padding-top: 15px;
float: right;
.user-icon {
width: 30px;
height: 30px;
vertical-align: middle;
}
.user-name {
color: #333;
font-weight: bold;
vertical-align: middle;
padding-left: 5px;
}
}
}
# 左侧菜单布局
- 基本布局结构-导航菜单 (opens new window)
<div class="logo"></div>
<el-menu
background-color="#333B4E"
text-color="#fff"
active-text-color="#ffd04b">
<el-menu-item index="1">
<i class="el-icon-setting"></i>
<span slot="title">首页</span>
</el-menu-item>
<el-submenu index="2">
<template slot="title">
<i class="el-icon-location"></i>
<span>页面内容</span>
</template>
<el-menu-item index="2-1">发布文章</el-menu-item>
<el-menu-item index="2-2">内容列表</el-menu-item>
<el-menu-item index="2-3">评论列表</el-menu-item>
<el-menu-item index="2-4">素材管理</el-menu-item>
</el-submenu>
<el-submenu index="3">
<template slot="title">
<i class="el-icon-location"></i>
<span>粉丝管理</span>
</template>
<el-menu-item index="3-1">图文数据</el-menu-item>
<el-menu-item index="3-2">粉丝概况</el-menu-item>
<el-menu-item index="3-3">粉丝画像</el-menu-item>
<el-menu-item index="3-4">粉丝列表</el-menu-item>
</el-submenu>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<span slot="title">账户信息</span>
</el-menu-item>
</el-menu>
- 布局样式
.logo {
width: 100%;
height: 60px;
background: #002244 url(../assets/imgs/logo_admin.png) no-repeat center /
140px auto;
}
# 控制左侧菜单的切换
目标:控制左侧菜单的折叠和展开
- 基于点击状态位控制
toggleAside () {
this.isOpen = !this.isOpen
}
- aside切换
<el-aside :width="!isOpen?'200px':'64px'">
- logo切换(折叠时添加一个类名)
<div class="logo" :class="{'mini-logo': isOpen}"></div>
.min-logo {
background-position: 14px;
background-size: 150px;
}
- menu切换
:collapse="isOpen"
- 禁止菜单的切换动画(需要动态绑定)
:collapse-transition='false'
# 控制子菜单的展开
目标:控制子菜单仅仅展开一个,其他的都是折叠的
<el-menu :unique-opened='true'>
# 欢迎页面
- 二级(嵌套)路由配置
{
path: '/home',
component: Home,
// 如果访问的是/home,那么重定向到子路由welcome
redirect: '/home/welcome',
children: [
// 欢迎页面
{ path: 'welcome', component: Welcome }
]
}
- 路由填充位配置
<el-main>
<router-view></router-view>
</el-main>
- 欢迎页面组件
<template>
<div class="welcome"></div>
</template>
<style scoped lang="less">
.welcome{
width: 100%;
height: 100%;
background: url(../assets/imgs/welcome.jpg) no-repeat center
}
</style>
# 用户信息展示
目标:页面右上角展示用户信息(用户名称和头像)
- 调用接口获取用户信息(接口路径
user/profile) - 将接口返回的结果填充到模板中
async loadUserInfo () {
// 加载用户信息
try {
// 获取缓存中的token
const token = localStorage.getItem('mytoken')
const ret = await axios.get('http://api-toutiao-web.itheima.net/mp/v1_0/user/profile', {
// 请求头headers由axios规定,用于设置http协议请求头
// 后续会把header放到请求拦截器中进行统一处理
headers: {
// 请求头的名称由谁规定?后端
Authorization: 'Bearer ' + token
}
})
this.user = ret.data.data
} catch (e) {
console.log(e)
this.$message.error('获取用户信息失败!')
}
},
- 模板填充
<img class="user-icon" :src='user.photo' alt />
<span class="user-name">{{user.name}}</span>
# token登录流程分析
目标:熟悉基于token的登录流程

# 路由权限控制
目标:控制在没有登录的场景下,自动跳转到登录页面。
// 添加导航守卫(拦截所有的路由跳转)
// 通过路由的导航守卫拦截所有的路由跳转
router.beforeEach((to, from, next) => {
// to 表示要跳转到哪里去
// from 表示从哪里跳转过来
// next是一个方法用于实现跳转(执行next方法才可以实现路由跳转)
// 判断用户是否已经登录,如果已经登录,就正常跳转,否则跳转到登录页面
// 获取token
const token = localStorage.getItem('mytoken')
// 什么时候跳转到登录页面?(如果直接访问/home/welcome,但是没有登录)
// 你如何判断用户没有登录?(缓存中没有token就证明没有登录),并且不能是登录页
if (to.path !== '/login' && !token) {
// 没有登录(不能是登录页面 并且 缓存中没有token)
next('/login')
} else {
// 登录了
next()
}
})

# 接口调用通用模块封装
目标:封装通用的接口调用模块
src/utils/request.js
/*
封装通用的接口调用方法
*/
import axios from 'axios'
// 设置axios的基准路径
axios.defaults.baseURL = 'http://api-toutiao-web.itheima.net/mp/v1_0/'
// 封装一个自定义方法实现请求的发送
const request = (options) => {
return axios({
// 请求方式
method: options.method,
// 请求地址
url: options.url,
// post/put请求参数
data: options.data,
// 请求头信息
headers: options.headers
})
}
// 调用方法
// request({
// method: 'get',
// url: 'user/profile'
// })
export default request
# 业务方法封装
目标:进一步简化代码的调用逻辑

- 封装专门的接口调用的业务模块-登录模块
// src/api/login.js
// 导入通用的接口调用方法
import request from '@/api/request.js'
// 封装单独的登录接口方法(专门用于登录)
export const login = (data) => {
// 这里的返回值依然是Promise实例对象
return request({
method: 'post',
url: 'authorizations',
data: data
})
}
- 基于封装好的业务方法调用接口-登录模块
// 导入接口导游的业务方法
import { login } from '@/api/login.js'
// 业务方法独立封装
const ret = await login({
mobile: this.loginForm.mobile,
code: this.loginForm.code
})
- 封装专门的接口调用的业务模块-主页模块
// src/api/home.js
// 导入通用的接口调用方法
import request from '@/api/request.js'
// 封装单独的登录接口方法(专门用于登录)
export const loadUserInfo = () => {
// 这里的返回值依然是Promise实例对象
return request({
method: 'get',
url: 'user/profile'
})
}
- 基于封装好的业务方法调用接口-主页模块
// 导入接口导游的业务方法
import { loadUserInfo } from '@/api/home.js'
// 基于封装的业务方法调用接口
const ret = await loadUserInfo()

# 总结
- 主页布局
- 主页顶部导航
- 主页的左侧菜单
- 右侧内容区
- 菜单的展开和折叠
- 控制只能打开一个子菜单
- 配置主页的嵌套路由
- 获取用户信息并动态展示
- 基于token的登录流程
- 通过导航守卫控制组件的访问权限
- 接口模块封装
- 封装通用的接口调用模块:
request.js - 封装接口调用的业务模块:api/ 目录下的业务模块
- 重构组件的接口调用逻辑:路由组件代码
- 能够梳理清楚上述三个层面的逻辑关系
- 封装通用的接口调用模块:
# axios拦截器
目标:理解axios拦截器的作用
- 请求拦截器统一处理请求头中的token的携带
- 响应拦截器统一处理返回的数据
// 关于axios拦截器
// 添加请求拦截器(发送请求之前先经过请求拦截器)
axios.interceptors.request.use(function (config) {
// 在发送请求之前可以做一些事情
// 这里可以在接口调用之前统一添加请求头
if (config.url !== 'authorizations') {
// 登录接口不需要传递token
config.headers.authorization = 'Bearer ' + sessionStorage.getItem('mytoken')
}
return config
}, function (error) {
// 请求失败时可以做一些处理
return Promise.reject(error)
})
// 添加响应拦截器(获取数据之前先经过响应拦截器)
axios.interceptors.response.use(function (response) {
// 凡是服务器返回的http状态吗是2XX的都会触发该方法(响应成功)
// 这里可以对响应的数据做一些处理
// response是axios对接口返回的数据包装之后形成的新的数据
// response.data的属性名称data由axios规定
// 解析出原始的接口返回的数据
return response.data
}, function (error) {
// 凡是服务器返回的http状态吗是2XX之外的其他状态,都触发该方法(响应失败)
// 这里可以对象错误信息做处理
return Promise.reject(error)
})
# 导航守卫和拦截器之间的关系
目标:熟悉导航守卫和拦截器之间的关系
- 导航守卫拦截的是路由的跳转(组件的切换)
- 拦截器拦截的是接口调用的请求和响应
# 关于token失效的问题
目标:在token失效的情况下,需要跳转到登录页
- 如何判断token失效了?如果接口返回的状态位是401,证明token失效了
// 添加响应拦截器(获取数据之前先经过响应拦截器)
axios.interceptors.response.use(function (response) {
// 凡是服务器返回的http状态吗是2XX的都会触发该方法(响应成功)
// 这里可以对响应的数据做一些处理
// response是axios对接口返回的数据包装之后形成的新的数据
// response.data的属性名称data由axios规定
// 解析出原始的接口返回的数据
return response.data
}, function (error) {
// 凡是服务器返回的http状态吗是2XX之外的其他状态,都触发该方法(响应失败)
// 这里可以对象错误信息做处理
if (error.response.status === 401) {
// 证明token失效了,此时可以跳转到登录页面
// 不能用如下的方式跳转到登录页
// this.$router.push('/login')
return router.push('/login')
}
return Promise.reject(error)
})
# 退出功能
目标:点击【退出】按钮,跳转到登录页面
事件的监听(基于element-ui组件的事件处理)
确认退出确认框效果
实现退出功能
<el-dropdown @command='handleLogout' trigger="click" class='my-dropdown'>
<span class="el-dropdown-link">
<img class="user-icon " :src='avatar' alt />
<span class="user-name">{{uname}}</span>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>个人信息</el-dropdown-item>
<el-dropdown-item>git地址</el-dropdown-item>
<el-dropdown-item command='logout' divided>退出</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
handleLogout (btn) {
if (btn === 'logout') {
// 点击了退出按钮,实现退出功能
// 1、提示退出
this.$confirm('确认退出吗?')
.then(() => {
// 如果点击了确定按钮,这里会执行
// 2、清除token
sessionStorage.removeItem('mytoken')
// 3、跳转到登录页面
this.$router.push('/login')
})
}
},
- 关于Vue事件绑定的.native修饰符用法
如果希望在组件上使用原生的事件绑定,需要在事件名称后面添加
.native事件修饰符原理:添加事件修饰符之后,那么事件会自动绑定到组件的跟节点的DOM元素上
.native事件修饰符属于vue的规则
<el-dropdown-item @click.native='handleLogout' divided>退出</el-dropdown-item>