# 主页模块

# 回顾

  • 登录模块
    • 组件基本布局
    • 调用接口实现基本的登录功能
    • 表单验证(基于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;
    }
  }
}

# 左侧菜单布局

<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的登录流程

image-20210228144131198

# 路由权限控制

目标:控制在没有登录的场景下,自动跳转到登录页面。

// 添加导航守卫(拦截所有的路由跳转)
// 通过路由的导航守卫拦截所有的路由跳转
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()
  }
})

image-20210228152318776

# 接口调用通用模块封装

目标:封装通用的接口调用模块 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

# 业务方法封装

目标:进一步简化代码的调用逻辑

image-20210228170337302

  • 封装专门的接口调用的业务模块-登录模块
// 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()

image-20210228161823418

# 总结

  • 主页布局
    • 主页顶部导航
    • 主页的左侧菜单
    • 右侧内容区
    • 菜单的展开和折叠
    • 控制只能打开一个子菜单
    • 配置主页的嵌套路由
    • 获取用户信息并动态展示
    • 基于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>