# 综合项目实战

# 回顾

  • 脚手架用法
    • 理解前端工程化(配置webpack很繁琐)
    • Vue脚手架的好处:快速创建一个现成的项目架构
    • 熟悉脚手架项目的目录结构
      • main.js
      • App.vue
      • HelloWord.vue
    • 理解相关文件的代码
    • 掌握ES6模块化的导入导出规则
    • 掌握单文件组件的代码结构
      • template 组件的模板
      • script 组件的数据和逻辑处理
      • style 组件的样式
  • 前后端交互
    • 熟悉axios这个js的基本信息
    • axios基本用法
      • axios.get()
        • 传参:直接通过url地址传递参数(拼接字符串到url中)
        • 传参:通过params熟悉进行传参(本质上在发送请求时也会进行字符串拼接)
      • axios.post()
      • axios()
        • 支持各种请求方式:get/post/put/delete/patch
    • 案例
      • 图书列表的展示
      • 删除图书
      • 添加图书
      • 关键字搜索(函数防抖限制发送请求的频率)
      • 加载状态的控制

# axios拦截器

  • 发送请求之前,先经过请求拦截器,这里可以对请求参数做一些调整
  • 请求返回数据之前,先经过响应拦截器,这里可以对返回的数据进行处理
    // 拦截器
    // 添加一个请求拦截器
    axios.interceptors.request.use(function (config) {
        // console.log('-------------------')
        // axios({method: 'get', url: '', data: {}})
        // config表示请求相关的配置选项(参数)
        // console.log(config)
        // config.url = 'http://localhost:3000/b'
        // 请求发送之前可以做一些事情
        return config;
      }, function (error) {
        // 请求如果发生错误,该函数会触发,这里可以进行处理
        return Promise.reject(error);
      });

    // 添加一个响应拦截器
    axios.interceptors.response.use(function (response) {
        // 只要响应的结果状态码是200开头的都表示成功,该方法会触发,这里可以对返回的数据进行处理
        // console.log('===================')
        // response表示axios封装的一个对象,里面包括后台返回的原始数据
        // response.data表示后台返回的原始数据
        console.log(response)
        return response;
      }, function (error) {
        // 只要响应的结果状态码不是200开头的都表示失败,该方法会触发,这里可以提示错误信息
        return Promise.reject(error);
      });

# 项目介绍

项目整体功能:图书管理;人员管理;楼层管理

  • 图书管理
    • 图书列表
    • 添加图书
    • 删除图书
    • 修改图书

# 初始化项目

目标:能够基于VueCli创建项目

  1. 通过vue create命令创建项目
vue create mybook
  1. 进入项目跟目录
cd mybook
  1. 运行项目
npm run serve

# 页面基本布局

目标:能够基于Bootstrap实现案例的基本布局结构

image-20210224095149541

  • 分析页面的组件结构
    • Login.vue 登录组件 (Login.vue和主页面是互斥的)
    • NavBar.vue 顶部导航组件
    • Aside.vue 左侧菜单组件
    • BookList.vue 英雄列表组件
    • BookAdd.vue 添加英雄组件
    • BookEdit.vue 编辑英雄组件
    • PersonList.vue 人员列表组件
  • FloorList.vue 楼层列表组件
  • 安装bootstrap包
npm i bootstrap@3.3.7
  • 导入样式
// 引入bootstrap
import 'bootstrap/dist/css/bootstrap.min.css'

# 路由配置

目标:实现点击侧边栏的连接。需要切换右侧的内容。这个需要路由来实现。

  1. 安装vue-router
    • 脚手架环境下,一般都是安装npm包,而不是单独引入一个js文件
npm i vue-router
  1. 导入路由
    • 采用ES6模块化方式导入VueRouter
import VueRouter from 'vue-router'
  1. 注册路由(配置路由插件)
    • 插件的规则我们后续详细再讲,现在这样理解如下的代码:采用这种方式让路由在脚手架环境下生效。
Vue.use(VueRouter)
  1. 准备路由组件
  • Login.vue
  • Home.vue
  1. 配置路由规则
    • 建立组件和url地址的关联关系(访问一个连接,展示一个组件)
import Login from './components/Login.vue'
import Home from './components/Home.vue'
// 配置组件的路由映射
const routes = [
  { path: '/', redirect: '/login' },
  { path: '/login', component: Login },
  { path: '/home', component: Home }
]
  1. 实例化路由对象

    • 让上述配置的映射关系生效
const router = new VueRouter({ routes })
  1. 挂载路由对象到Vue实例上
    • 这样可以让上述配置的路由规则在Vue中生效
new Vue({
  router,
  render: h => h(App)
}).$mount('#app')
  1. 配置路由填充位

App.vue根组件

<div id='app'>
  <!-- 路由对应组件显示的位置 -->
  <router-view></router-view>
</div>

# 导航组件

实现顶部导航组件

  • 拆分组件实现布局 NavBar.vue
<template>
  <nav class="navbar navbar-inverse">
    <a class="navbar-brand" href="#">CURD</a>
  </nav>
</template>
<script>
export default {
  name: 'NavBar'
}
</script>
  • 导入组件并使用
<!-- 组件的模板HTML -->
<template>
  <div id="app" class="container">
    <!-- 顶部导航栏组件 -->
    <NavBar/>
  </div>
</template>

<!-- 组件的配置选项JS -->
<script>
// 导入另外一个组件
import NavBar from './components/NavBar.vue'

export default {
  // 组件的名称,方便调试使用
  name: 'App',
  components: {
    NavBar
  }
}
</script>

# 侧边栏组件

拆分侧边栏组件布局

  • 组件模板布局
<template>
  <div class="col-md-2">
    <div class="row">
      <div class="list-group">
        <a href="#" class="list-group-item active">图书列表</a>
        <a href="#" class="list-group-item">人员列表</a>
        <a href="#" class="list-group-item">楼层列表</a>
      </div>
    </div>
  </div>
</template>
  • 导入组件用法
<template>
  <div class="container">
    <!-- 在脚手架的环境下,使用如下的两种命名方式都可以 -->
    <!-- <NavBar/> -->
    <!-- 顶部导航栏组件 -->
    <nav-bar/>
    <!-- 左侧菜单组件:单个单词的组件名称不可以使用纯小写的方式,但是首字符大写是可以的 -->
    <aside-menu/>
  </div>
</template>

<script>
// 导入子组件
import NavBar from './components/NavBar.vue'
import AsideMenu from './components/Aside.vue'

export default {
  // 配置局部子组件
  components: {
    NavBar,
    AsideMenu
  }
}
</script>

# 控制路由跳转菜单激活

目标:实现路由跳转菜单高亮控制

激活(高亮):点击哪一个链接,哪一个链接应该添加一个类名active,没有点中的链接标签去掉类名

实现原理:基于vue-router的相关配置,自动给链接标签添加指定的类名

// linkExactActiveClass属性的作用:控制点中链接标签后添加的类名名称,默认的名称是router-link-exact-active
const router = new VueRouter({ routes , linkExactActiveClass: 'active'})

# 组件内部样式处理

  • 关于scoped

scoped 作用域 ,作用是把样式变成组件的局部样式(当前组件有效)

原理:随机给类名生成了一个唯一的属性选择器,如下图所示

image-20210224150452772

# 拆分路由模块

目标:main.js 的职责足够单一,让代码更好维护。全局资源导入,根实例初始化。

  • 封装路由模块:src/router/index.js
import Vue from 'vue'
// 导入路由构造函数
import VueRouter from 'vue-router'

// @符号指的是src目录(@是src目录的别名)
// vue文件的后缀可以省略
// 导入路由组件
import Login from '@/components/Login.vue'
import Home from '@/components/Home.vue'
import BookList from '@/components/BookList.vue'
import PersonList from '@/components/PersonList.vue'
import FloorList from '@/components/FloorList.vue'
import BookAdd from '@/components/BookAdd.vue'
import BookEdit from '@/components/BookEdit.vue'

// 配置Vue的路由插件(让VueRouter发挥作用)
Vue.use(VueRouter)

// 配置组件的路由映射
const routes = [
  { path: '/', redirect: '/login' },
  { path: '/login', component: Login },
  { 
    path: '/home', 
    component: Home,
    children: [
      { path: 'books', component: BookList },
      { path: 'add', component: BookAdd },
      { path: 'edit', component: BookEdit },
      { path: 'persons', component: PersonList },
      { path: 'floors', component: FloorList }
    ] 
  }
]

// 实例化路由组件
const router = new VueRouter({
  // 该属性作用:点击路由连接后,自动添加的高亮类名
  // 点击router-link,vue-router会自动添加一个类名,默认类名router-link-exact-active
  // 但是这个默认类名可以通过如下属性进行配置
  linkExactActiveClass: 'active',
  routes
})

// 导入路由对象
export default router
  • 导入路由模块
import Vue from 'vue'
import App from './App.vue'

// 引入bootstrap
import 'bootstrap/dist/css/bootstrap.min.css'

// 导入路由实例(如果文件名称是index.js,可以省略)
import router from '@/router/index.js'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  // 挂载路由实例
  router
}).$mount('#app')

# 准备接口

目标:基于json-server模拟接口。

  • 创建db.json文件
{
  "books": [
    { "id": 1, "bname": "西游记", "author": "吴承恩", "ctime": "2020-03-21 10:38:20" },
    { "id": 2, "bname": "红楼梦", "author": "曹雪芹", "ctime": "2020-03-21 10:38:20" },
    { "id": 3, "bname": "三国演义", "author": "罗贯中", "ctime": "2020-03-21 10:38:20" }
  ]
}
  • 启动接口
json-server db.json
测试接口地址:http://localhost:3000/books
  • 安装axios包
npm i axios

# 图书列表组件

目标:实现英雄列表组件

  • 组件基本布局
<template>
  <div>
    <a href="heroes-form.html" class="btn btn-primary">添加图书</a>
    <hr>
    <table class="table table-hover">
      <thead>
      <tr>
        <th>ID</th>
        <th>图书名称</th>
        <th>出版时间</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
        <tr v-for='item in books' :key='item.id'>
          <td>{{item.id}}</td>
          <td>{{item.bookname}}</td>
          <td>{{item.createTime}}</td>
          <td>
            <button class="btn btn-success">编辑</button>
            <button class="btn btn-danger">删除</button>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script>
export default {}
</script>

<style>
</style>
  • 在组件初始化时,获取英雄列表数据
// 导入axios库
import axios from 'axios'
// 配置axios请求的基准路径
axios.defaults.baseURL = 'http://localhost:3000/'

export default {
  data () {
    return {
      // 图书列表数据
      books: []
    }
  },
  methods: {
    loadBookList () {
      // 加载图书列表数据
      axios.get('books').then(ret => {
        if (ret.status === 200) {
          this.books = ret.data
        }
      })
    }
  },
  mounted () {
    this.loadBookList()
  }
}
  • 根据list数据,进行模板渲染。
<tbody>
    <tr v-for='item in books' :key='item.id'>
      <td>{{item.id}}</td>
      <td>{{item.bookname}}</td>
      <td>{{item.createTime}}</td>
      <td>
        <button class="btn btn-success">编辑</button>
        <button class="btn btn-danger">删除</button>
      </td>
    </tr>
</tbody>

# 添加图书组件

目标:实现添加图书功能

  • 添加链接跳转
<a @click='handleAdd' href="javascript:;" class="btn btn-primary">添加图书</a>
handleAdd () {
  // 通过编程式导航跳转到添加图书的界面
  this.$router.push('/home/add')
},
  • 创建组件,基础布局 src/components/BooksAdd.vue
<template>
    <form action="" method="post" role="form">
      <legend>添加图书</legend>
      <div class="form-group">
        <label>图书名称</label>
        <input type="text" class="form-control">
      </div>
      <div class="form-group">
        <label>图书作者</label>
        <input type="text" class="form-control">
      </div>
      <button type="submit" class="btn btn-primary">提交</button>
    </form>
</template>

<script>
export default {}
</script>

<style>
</style>
  • 收集英雄表单数据
<form @submit.prevent='handleSubmit' action="" method="post" role="form">
  <legend>添加图书</legend>
  <div class="form-group">
    <label>图书名称</label>
    <input v-model='bookname' type="text" class="form-control">
  </div>
  <div class="form-group">
    <label>发布时间</label>
    <input v-model='createTime' type="text" class="form-control">
  </div>
  <button type="submit" class="btn btn-primary">提交</button>
</form>
  • 在处理函数中,进行添加,如果添加成功,跳转列表
handleSubmit () {
    // 调用接口添加图书
    handleSubmit () {
      // 添加图书
      this.axios.post('books', {
        bookname: this.bookname,
        createTime: this.createTime
      }).then(ret => {
        if (ret.status === 201) {
          // 添加成功,跳转列表页面
          this.$router.push('/home/books')
        }
      })
    }
}
  • 统一导入axios (main.js)
// 统一导入axios
import axios from 'axios'
// 配置axios的基准路径
axios.defaults.baseURL = 'http://localhost:3000/'
// 可以把axios添加到Vue原型对象上,这样的所有组件都可以得到axios
// 所有的组件都是Vue的实例对象(实例对象都可以访问原型上的成员)
Vue.prototype.axios = axios

# 编辑图书组件

目标:实现编辑图书功能

  • 实现动态跳转:components/BooksList.vue
<router-link :to="'/heroes/edit/'+item.id" class="btn btn-success">编辑</router-link>
  • 准备编辑组件,基础布局。 src/views/HeroesEdit.vue
<template>
    <form @submit.prevent='handleSubmit' action="" method="post" role="form">
      <legend>添加图书</legend>
      <div class="form-group">
        <label>图书名称</label>
        <input v-model='bookname' type="text" class="form-control">
      </div>
      <div class="form-group">
        <label>图书作者</label>
        <input v-model='author' type="text" class="form-control">
      </div>
      <button type="submit" class="btn btn-primary">提交</button>
    </form>
</template>

<script>
export default {}
</script>

<style>
</style>
  • 配置路由规则,通过动态路由规则,来指向编辑组件。 src/router.js
// 导入组件
import BookEdit from '../components/BookEdit.vue'
// 路由映射配置
{path: '/books/edit/:id', component: BookEdit},
  • 绑定表单数据
export default {
  data () {
    return {
      id: -1,
      bookname: '',
      author: ''
    }
  }
}
  • 组件初始化,获取当前英雄的信息,展示在表单中。
methods: {
  loadBookInfo () {
    // 获取图书的id
    const id = this.$route.params.id
    // 根据图书的id查询图书的详细信息
    axios.get('http://localhost:3000/books/' + id).then((ret) => {
      // 填充表单
      this.bookname = ret.data.bname
      this.author = ret.data.author
      this.id = ret.data.id
    })
  }
},
created () {
  // 组件显示时自动触发
  this.loadBookInfo()
}
  • 点击修改按钮,发起修改,修改成功后,列表组件。
<form @submit.prevent='handleSubmit' action="" method="post" role="form">
handleSubmit () {
    // 调用接口实现图书的编辑操作
    // 增删改查 post delete put get
    axios.put('http://localhost:3000/books/' + this.id, {
      bname: this.bookname,
      author: this.author,
      ctime: new Date()
    }).then((ret) => {
      if (ret.status === 200) {
        // 编辑成功后跳转到列表页面
        this.$router.push('/books')
      }
    }).catch(() => {
        alert('编辑图书失败')
    })
  }
}

# 删除功能

目标:实现删除图书功能

  1. 绑定删除按钮点击事件
  2. 弹出确认框
  3. 点击确认,发送删除请求
  4. 删除成功,更新当前列表
<button @click="handleDelete(item.id)" class="btn btn-danger">删除</button>
handleDelete (id) {
  if (!confirm('要删除图书吗?')) return
  // 删除图书
  this.axios.delete('/books/' + id).then(ret => {
    if (ret.status === 200) {
      // 删除成功,刷新列表
      this.loadBookList()
    }
  })
}

# 时间过滤器

目标:基于moment包实现时间格式化过滤器

  • 安装 moment
npm i moment
  • 导入moment BooksList.vue
// 导入moment
import moment from 'moment';
  • 定义过滤器
filters: {
  formatTime (time) {
    // 我们希望时间编程 XXXX年XX月XX日
    // const d = new Date(time)
    // return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' + d.getDate() + '日'
    return moment(time).format('yyyy年MM月DD日 hh:mm:ss')
  }
},
  • 使用过滤器
<td>{{item.ctime|formatTime}}</td>

# axios全局配置

目标:全局配置axios

  • 在组件中使用axios
  • 每一个组件其实也是vue实例
  • 实例都可以访问当构造函数的原型(方法|属性)
  • 所以,把axios挂载到Vue的原型上
  • 将来,任何组件都可以访问axios
// 进行axios的全局挂载
import axios from 'axios'
// 将来通过vue的实例访问$http,其实就是axios。
Vue.prototype.$http = axios
// 通过组件的实例对象this可以访问到$http
this.$http.delete('books/' + id)

# 回顾

  • axios中拦截器用法(请求拦截器和响应拦截器)
  • 熟悉组件化拆分思想
  • 配置bootstrap样式
  • 配置项目的整体路由(嵌套路由)
  • 准备组件模板
  • 准备二级路由的组件
  • 控制路由链接的高亮效果
  • 关于样式scoped属性的作用
  • 图书列表展示
  • 添加图书
  • 删除图书
  • 通过过滤器实现日期的格式化
  • 关于组件的name属性:主要用于调试
  • 编辑图书

# 编辑功能

# 编辑图书-上

  • 绑定编辑按钮的事件,传递id路由参数,跳转到编辑图书组件
toEdit (id) {
  // 跳转到编辑图书的页面
  this.$router.push('/home/edit?id=' + id)
},
  • 编辑图书组件中需要获取路由参数id,然后根据id查询图书的详细数据,回填表单
methods: {
  handleSubmit () {

  },
  loadBookInfo (id) {
    // 根据id查询图书的详细数据
    this.$http.get('books/' + id).then(ret => {
      if (ret.status === 200) {
        // 获取数据成功,更新数据(初始化表单)
        this.bookname = ret.data.bookname
        this.createTime = ret.data.createTime
      }
    })
  }
},
mounted () {
  this.loadBookInfo(this.$route.query.id)
}

# 编辑图书-下

  • 修改完成表单数据后,提交表单,跳转到列表页面
handleSubmit () {
  // 编辑图书提交表单
  this.$http.put('/books/' + this.id, {
    bookname: this.bookname,
    createTime: this.createTime
  }).then(ret => {
    if (ret.status === 200) {
      // 编辑成功,跳转到列表页面
      this.$router.push('/home/books')
    }
  })
},

# 组件的生命周期

组件的生命周期:一个事物(组件)从出现到消亡的整个过程

所有的生命周期函数都是自动触发,不要去手动调用

1、何时触发;2、有什么用

  • 创建阶段
    • beforeCreate 表示组件刚开始创建,尚未有组件的相关属性存在
    • created 表示data属性已经存在,但是data可能还没有数据
    • beforeMount 刚准备好组件的模板,但是尚未渲染到页面
    • mounted 已经把模板渲染到了页面中
    • 主要应用场景:调用接口用created;操作DOM用mounted
  • 更新阶段(数据(data、props和路由参数)的变化导致更新钩子函数触发)
    • beforeUpdate 组件数据更新前触发
    • updated 组件数据更新后触发
    • 主要应用场景:控制组件更新额状态提示
  • 销毁阶段(组件从页面中移除)
    • beforeDestroy 组件销毁前触发
    • destroyed 组件销毁后触发
    • 主要应用场景:组件销毁时,清理不再使用的相关的资源(比如定时任务)