vue3全家桶教程

Vue3全家桶教程

@description:系统学习 Vue.js 渐进式 JavaScript 框架 接触到的第一个框架

@author:liuwy

@date:2023.4.30

Ⅰ Vue基础入门

目标目录
了解Vue一、Vue简介
能够知道 Vue 的基本使用步骤二、Vue的基本使用
掌握六种指令与过滤器的使用三、Vue的指令和过滤器
案例实战——提高熟练程度四、第一个案例——品牌列表案例
对Vue基础入门的总结五、Vue基础入门总结

一、Vue简介

1.Vue数据驱动

1.1 数据驱动

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fLFmacXc-1685004795657)(./assets/Snipaste_2023-05-02_19-53-50.png)]

使用了vue的页面中,vue会监听数据的变化,从而自动重新渲染页面结构。

好处:当页面数据发生变化时,页面会自动渲染!

注意:数据驱动视图是单向的数据绑定

1.2 双向数据绑定

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYEbu4wL-1685004795658)(.\assets\03_vue基础入门_1-79_1476395055.bmp)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DIzsaOAx-1685004795659)(.\assets\03_vue基础入门_1-79_1476395056.jpg)]

在填写表单时,双向数据绑定可以辅助开发者在不操作 DOM 的前提下,自动把用户填写的内容同步到数据源中。

好处:开发者不再需要手动操作 DOM 元素,来获取表单元素最新的值!

1.3 MVVM

MVVM 是 vue 实现数据驱动视图和双向数据绑定的核心原理。它把每个 HTML 页面都拆分成了如下三个部分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xsi9yu0O-1685004795659)(.\assets\03_vue基础入门_1-79_1476395060.bmp)]

MVVM 概念中:

  • View 表示当前页面所渲染的 DOM 结构。
  • Model 表示当前页面渲染时所依赖的数据源。
  • ViewModel 表示 vue 的实例,它是 MVVM 的核心。
1.4 MVVM的工作原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jziKIvuS-1685004795660)(.\assets\03_vue基础入门_1-79_1476395056.jpg)]

ViewModel 作为 MVVM 的核心,是它把当前页面的数据源(Model)和页面的结构(View)连接在了一起。

当数据源发生变化时,会被 ViewModel 监听到,VM 会根据最新的数据源自动更新页面的结构

当表单元素的值发生变化时,也会被 VM 监听到,VM 会把变化过后最新的值自动同步到 Model 数据源中

2.Vue的版本

当前,vue 共有 3 个大版本,其中:

  • 2.x 版本的 vue 是目前企业级项目开发中的主流版本
  • 3.x 版本的 vue 于 2020-09-19 发布,生态还不完善,尚未在企业级项目开发中普及和推广
  • 1.x 版本的 vue 几乎被淘汰,不再建议学习与使用

vue3.x 和 vue2.x 版本的对比

vue2.x 中绝大多数的 API 与特性,在 vue3.x 中同样支持。同时,vue3.x 中还新增了 3.x 所特有的功能、并废弃了某些 2.x 中的旧功能:

新增的功能例如:

组合式 API、多根节点组件、更好的 TypeScript 支持等

废弃的旧功能如下:

过滤器、不再支持 o n , on, onoff 和 $once 实例方法等

二、Vue的基本使用

1.基本使用步骤

导入 vue.js 的 script 脚本文件

在页面中声明一个将要被 vue 所控制的 DOM 区域

创建 vm 实例对象(vue 实例对象)

2.基本代码与 MVVM 的对应关系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jDGmidIu-1685004795660)(.\assets\03_vue基础入门_1-79_1476395148.bmp)]

3.安装 Vue-devtools 调试工具

收藏猫chrome插件资源,下载安装:https://chrome.pictureknow.com/

扩展迷chrome插件资源,下载安装:https://www.extfans.com/

三、Vue的指令和过滤器

1.指令的概念

**指令(Directives)**是 vue 为开发者提供的模板语法,用于辅助开发者渲染页面的基本结构。

vue 中的指令按照不同的用途可以分为如下 6 大类:

① 内容渲染指令

② 属性绑定指令

③ 事件绑定指令

④ 双向绑定指令

⑤ 条件渲染指令

⑥ 列表渲染指令

注意:指令是 vue 开发中最基础、最常用、最简单的知识点。

1.1 内容渲染指令

内容渲染指令用来辅助开发者渲染 DOM 元素的文本内容。常用的内容渲染指令有如下 3 个:

  • v-text
  • {{ }}
  • v-html
v-text
<div id="app">
    <p v-text="username"></p>
    <p v-text="gender">性别</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            username: 'zs',
            gender: '男'
        }
    })
</script>

注意:v-text 指令会覆盖元素内默认的值。

{{}}

vue 提供的 {{ }} 语法,专门用来解决 v-text 会覆盖默认文本内容的问题。这种 {{ }} 语法的专业名称是插值表达式(英文名为:Mustache)。

<div id="app">
    <p>姓名:{{username}}</p>
    <p>性别:{{gender}}</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            username: 'zs',
            gender: '男'
        }
    })
</script>

注意:相对于 v-text 指令来说,插值表达式在开发中更常用一些!因为它不会覆盖元素中默认的文本内容

v-html

v-text 指令和插值表达式只能渲染纯文本内容。如果要把包含 HTML 标签的字符串渲染为页面的 HTML 元素,则需要用到 v-html 这个指令:

<div id="app">
    <p v-text="desc"></p>
    <p>{{desc}}</p>
    <p v-html="desc"></p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            username: 'zs',
            gender: '男',
            desc: '<i>abc</i>'
        }
    })
</script>
1.2 属性绑定指令
v-bind

如果需要为元素的属性动态绑定属性值,则需要用到 v-bind 属性绑定指令。用法示例如下:

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <input type="text" v-bind:placeholder="inputValue">
    <hr>
    <img v-bind:src="imgSrc" alt="">
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 文本框的占位符内容
            inputValue: '请输入内容',
            // 图片的 src 地址
            imgSrc: './images/logo.png',
        },
    })
</script>
属性绑定指令的简写形式

由于 v-bind 指令在开发中使用频率非常高,vue 官方为其提供了简写形式(简写为英文的 : )。

使用 Javascript 表达式

在 vue 提供的模板渲染语法中,除了支持绑定简单的数据值之外,还支持 Javascript 表达式的运算,例如:

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <p>{{number + 1}}</p>
    <p>{{ok ? 'True' : 'False'}}</p>
    <p>{{message.split('').reverse().join('')}}</p>
    <p :id="'list-' + id">xxx</p>
    <p>{{user.name}}</p>
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 数值
            number: 9,
            // 布尔值
            ok: false,
            // 字符串
            message: 'ABC',
            // id 值
            id: 3,
            // 用户的信息对象
            user: {
                name: 'zs',
            },
        },
    })
</script>
1.3 事件绑定指令
v-on

vue 提供了 v-on 事件绑定指令,用来辅助程序员为 DOM 元素绑定事件监听。通过 v-on 绑定的事件处理函数,需要在 methods 节点中进行声明,语法格式如下:

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <h3>count 的值为:{{count}}</h3>
    <!-- TODO:点击按钮,让 data 中的 count 值自增 +1 -->
    <button v-on:click="addCount">+1</button>
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 计数器的值
            count: 0,
        },
        methods: {
            // 点击按钮,让 count 自增 +1
            addCount() {
                // this 访问当前的实例对象vm
                this.count += 1
            },
        },
    })
</script>

注意:原生 DOM 对象有 onclick、oninput、onkeyup 等原生事件,替换为 vue 的事件绑定形式后,分别为:v-on:click、v-on:input、v-on:keyup

事件绑定的简写形式

由于 v-on 指令在开发中使用频率非常高,vue 官方为其提供了简写形式(简写为英文的 @ )。

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <h3>count 的值为:{{count}}</h3>
    <!-- TODO:点击按钮,让 data 中的 count 值自增 +1 -->
    <!-- 简写到行内的事件处理 -->
    <button @click="count+=1">+1</button>
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 计数器的值
            count: 0,
        },
        methods: {
            // 点击按钮,让 count 自增 +1
            // 如果事件处理函数中的代码足够简单,只有一行代码,则可以简写到行内
            // addCount() {
            //   this.count += 1
            // },
        },
    })
</script>
事件对象 event

在原生的 DOM 事件绑定中,可以在事件处理函数的形参处,接收事件对象 event。同理,在 v-on 指令(简写为 @ )所绑定的事件处理函数中,同样可以接收到事件对象 event,示例代码如下:

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <h3>count 的值为:{{count}}</h3>
    <button v-on:click="addCount">+1</button>
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 计数器的值
            count: 0,
        },
        methods: {
            // 点击按钮,让 count 自增 +1
            addCount(e) {
                const nowBgColor = e.target.style.backgroundColor
                e.target.style.backgroundColor = nowBgColor === 'red' ? '' : 'red'
                this.count += 1
            },
        },
    })
</script>
绑定事件并传参

在使用 v-on 指令绑定事件时,可以使用 ( ) 进行传参,示例代码如下:

<!-- vue 实例要控制的 DOM 区域 -->
<div id="app">
    <h3>count 的值为:{{count}}</h3>
    <button @click="addCount(2, $event)">+2</button>
</div>

<!-- 导入 vue 脚本文件 -->
<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 创建 VM 实例对象
    const vm = new Vue({
        // 指定当前 VM 要控制的区域
        el: '#app',
        // 数据源
        data: {
            // 计数器的值
            count: 0,
        },
        methods: {
            addCount(step, e) {
                const bgColor = e.target.style.backgroundColor
                e.target.style.backgroundColor = bgColor === 'red' ? '' : 'red'
                this.count += step
            },
        },
    })
</script>
事件修饰符

在事件处理函数中调用 preventDefault() 或 stopPropagation() 是非常常见的需求。因此,vue 提供了事件修饰符的概念,来辅助程序员更方便的对事件的触发进行控制。常用的 5 个事件修饰符如下:

事件修饰符说明
.prevent阻止默认行为(例如:阻止 a 连接的跳转、阻止表单的提交等)
.stop阻止事件冒泡
.capture以捕获模式触发当前的事件处理函数
.once绑定的事件只触发1次
.self只有在 event.target 是当前元素自身时触发事件处理函数
<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">
    <h4>① .prevent 事件修饰符的应用场景</h4>
    <a href="https://www.baidu.com" @click.prevent="onLinkClick">百度首页</a>

    <hr />

    <h4>② .stop 事件修饰符的应用场景</h4>
    <div class="outer" @click="onOuterClick">
        外层的 div
        <div class="inner" @click.stop="onInnerClick">内部的 div</div>
    </div>

    <hr />

    <h4>③ .capture 事件修饰符的应用场景</h4>
    <div class="outer" @click.capture="onOuterClick">
        外层的 div
        <div class="inner" @click="onInnerClick">内部的 div</div>
    </div>

    <hr />

    <h4>④ .once 事件修饰符的应用场景</h4>
    <div class="inner" @click.once="onInnerClick">内部的 div</div>

    <hr />

    <h4>⑤ .self 事件修饰符的应用场景</h4>
    <div class="box" @click="onBoxClick">
        最外层的 box
        <div class="outer" @click.self="onOuterClick">
            中间的 div
            <div class="inner" @click="onInnerClick">内部的 div</div>
        </div>
    </div>

    <hr />
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        // 声明处理函数的节点
        methods: {
            // 超链接的点击事件处理函数
            onLinkClick() {
                alert('ok')
            },
            // 点击了外层的 div
            onOuterClick() {
                console.log('触发了 outer 的 click 事件处理函数')
            },
            // 点击了内部的 div
            onInnerClick() {
                console.log('触发了 inner 的 click 事件处理函数')
            },
            onBoxClick() {
                console.log('触发了 box 的 click 事件处理函数')
            }
        },
    })
</script>
按键修饰符

在监听键盘事件时,我们经常需要判断详细的按键。此时,可以为键盘相关的事件添加按键修饰符,例如:

<div id="app">
    <input type="text" @keyup.enter="submit" @keyup.esc="clearInput" />
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {},
        methods: {
            // 获取文本框最新的值
            submit(e) {
                console.log('摁下了 enter 键,最新的值是:' + e.target.value)
            },
            // 清空文本框的值
            clearInput(e) {
                e.target.value = ''
            },
        },
    })
</script>
1.4 双向绑定指令
v-model

vue 提供了 v-model 双向数据绑定指令,用来辅助开发者在不操作 DOM 的前提下,快速获取表单的数据。

<div id="app">
    <p>用户名是:{{username}}</p>
    <input type="text" v-model="username" />

    <hr />

    <p>选中的省份是:{{province}}</p>
    <select v-model="province">
        <option value="">请选择</option>
        <option value="1">北京</option>
        <option value="2">河北</option>
        <option value="3">黑龙江</option>
    </select>
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // 姓名
            username: 'zs',
            // 省份
            province: '1',
        },
    })
</script>

注意:v-model 指令只能配合表单元素一起使用!

v-model 指令的修饰符

为了方便对用户输入的内容进行处理,vue 为 v-model 指令提供了 3 个修饰符,分别是:

修饰符作用示例
.number自动将用户的输入值转为数值类型
<div id="app">
    姓名:<input type="text" v-model.trim="username" />

    <hr />

    年龄:<input type="text" v-model.number="age" />

    <hr />

    地址:<input type="text" v-model.lazy="address" />
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // 姓名
            username: 'zs',
            // 年龄
            age: 1,
            // 地址
            address: '北京市',
        },
    })
</script>
1.5 条件渲染指令

条件渲染指令用来辅助开发者按需控制 DOM 的显示与隐藏。条件渲染指令有如下两个,分别是:

  • v-if
  • v-show
<div id="app">
    <button @click="flag = !flag">Toggle Flag</button>

    <p v-if="flag">请求成功 --- 被 v-if 控制</p>
    <p v-show="flag">请求成功 --- 被 v-show 控制</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // flag 用来控制元素的显示与隐藏
            // 值为 true 时显示元素
            // 值为 false 时隐藏元素
            flag: false,
        },
    })
</script>
v-if 和 v-show 的区别

实现原理不同:

  • v-if 指令会动态地创建或移除 DOM 元素,从而控制元素在页面上的显示与隐藏;
  • v-show 指令会动态为元素添加或移除 style=“display: none;” 样式,从而控制元素的显示与隐藏;

性能消耗不同:

v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。

  • 如果需要非常频繁地切换,则使用 v-show 较好
  • 如果在运行时条件很少改变,则使用 v-if 较好
v-else 和 v-else-if

v-if 可以单独使用,或配合 v-else 指令一起使用

v-else-if 指令,顾名思义,充当 v-if 的“else-if 块”,可以连续使用

<div id="app">
    <p v-if="num > 0.5">随机数 > 0.5</p>
    <p v-else>随机数 ≤ 0.5</p>

    <hr />

    <p v-if="type === 'A'">优秀</p>
    <p v-else-if="type === 'B'">良好</p>
    <p v-else-if="type === 'C'">一般</p>
    <p v-else></p>
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // 生成 1 以内的随机数
            num: Math.random(),
            // 类型
            type: 'A'
        },
    })
</script>
1.6 列表渲染指令
v-for

vue 提供了 v-for 指令,用来辅助开发者基于一个数组来循环渲染相似的 UI 结构。

v-for 指令需要使用 item in items 的特殊语法,其中:

  • items 是待循环的数组
  • item 是当前的循环项
v-for 中的索引

v-for 指令还支持一个可选的第二个参数,即当前项的索引。语法格式为 (item, index) in items,示例代码如下:

<div id="app">
    <ul>
        <li v-for="(user, i) in list">索引是:{{i}},姓名是:{{user.name}}</li>
    </ul>
</div>

<script src="./lib/vue-2.6.12.js"></script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // 用户列表的数据
            list: [
                { id: 1, name: 'zs' },
                { id: 2, name: 'ls' },
            ],
        },
    })
</script>

注意:v-for 指令中的 item 项和 index 索引都是形参,可以根据需要进行重命名。例如 (user, i) in userlist

使用 key 维护列表的状态

列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新

为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每项提供一个唯一的 key 属性

<!-- 在页面中声明一个将要被 vue 所控制的 DOM 区域 -->
<div id="app">

    <!-- 添加用户的区域 -->
    <div>
        <input type="text" v-model="name">
        <button @click="addNewUser">添加</button>
    </div>

    <!-- 用户列表区域 -->
    <ul>
        <li v-for="(user, index) in userlist" :key="user.id">
            <input type="checkbox" />
            姓名:{{user.name}}
        </li>
    </ul>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            // 用户列表
            userlist: [
                { id: 1, name: 'zs' },
                { id: 2, name: 'ls' }
            ],
            // 输入的用户名
            name: '',
            // 下一个可用的 id 值
            nextId: 3
        },
        methods: {
            // 点击了添加按钮
            addNewUser() {
                this.userlist.unshift({ id: this.nextId, name: this.name })
                this.name = ''
                this.nextId++
            }
        },
    })
</script>
key 的注意事项
  1. key 的值只能是字符串数字类型
  2. key 的值必须具有唯一性(即:key 的值不能重复)
  3. 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
  4. 使用 index 的值当作 key 的值没有任何意义(因为 index 的值不具有唯一性)
  5. 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)

2.过滤器

**过滤器(Filters)**常用于文本的格式化。例如:

hello -> Hello

过滤器从本质上可以理解为一个函数 :即“管道符”前待处理的参数作为过滤器函数的参数,返回值为处理后的值。

过滤器应该被添加在 JavaScript 表达式的尾部,由“管道符”进行调用,示例代码如下:

2.1 过滤器的简单使用
<div id="app"> <!-- 通过 过滤器 将 title 和 message 转换为 "首字符大写的形式" --> 
    <p :title="info | capitalize">{{message | capitalize}}</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            message: 'hello vue.js',
            info: 'title info',
        },
        filters: {
            capitalize(str) {
                return str.charAt(0).toUpperCase() + str.slice(1)
            }
        }
    })
</script>
2.2 私有过滤器和全局过滤器

在 filters 节点下定义的过滤器,称为“私有过滤器”,因为它只能在当前 vm 实例所控制的 el 区域内使用。如果希望在多个 vue 实例之间共享过滤器,则可以按照如下的格式定义全局过滤器

<div id="app">
    <p :title="info | capitalize">{{message | capitalize}}</p>
</div>

<div id="app2">
    <p>{{abc | capitalize}}</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 全局过滤器
    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase() + str.slice(1) + '~~~'
    })
</script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            message: 'hello vue.js',
            info: 'title info',
        },
        // 私有过滤器,只能被当前 vm 所控制的区域所使用
        filters: {
            capitalize(str) {
                return str.charAt(0).toUpperCase() + str.slice(1)
            },
        },
    })
</script>

<script>
    const vm2 = new Vue({
        el: '#app2',
        data: {
            abc: 'abc'
        }
    })
</script>

注意:如果全局过滤器与私有过滤器函数名冲突,则以私有过滤器为准——(就近原则)

2.3连续调用多个过滤器

过滤器可以串联地进行调用,例如:

<div id="app">
    <p :title="info | capitalize">{{message | capitalize | maxLength}}</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 全局过滤器
    // 首字母转大写的过滤器
    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase() + str.slice(1)
    })

    // 定义控制文本长度的过滤器
    Vue.filter('maxLength', (str) => {
        if(str.length <= 10) return str
        return str.slice(0, 10) + '...'
    })
</script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            message: 'hello vue.js',
            info: 'title info',
        },
    })
</script>
2.4 过滤器传参

过滤器的本质JavaScript 函数,因此可以接收参数,格式如下:

<div id="app">
    <p :title="info | capitalize">{{message | capitalize | maxLength(3)}}</p>
</div>

<script src="./lib/vue-2.6.12.js"></script>
<script>
    // 全局过滤器
    // 首字母转大写的过滤器
    Vue.filter('capitalize', (str) => {
        return str.charAt(0).toUpperCase() + str.slice(1)
    })

    // 定义控制文本长度的过滤器
    // 这个位置的 len = 10 是在没有传递第二个参数时,给 len 一个默认值为 10 。
    Vue.filter('maxLength', (str, len = 10) => {
        if(str.length <= len) return str
        return str.slice(0, len) + '...'
    })
</script>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            message: 'hello vue.js',
            info: 'title info',
        },
    })
</script>
2.5过滤器的兼容性

过滤器仅在 vue 2.x 和 1.x 中受支持在 vue 3.x 的版本中剔除了过滤器相关的功能。

在企业级项目开发中:

如果使用的是 2.x 版本的 vue,则依然可以使用过滤器相关的功能

如果项目已经升级到了 3.x 版本的 vue,官方建议使用计算属性或方法代替被剔除的过滤器功能

具体的迁移指南,请参考 vue 3.x 的官方文档给出的说明:

https://v3.vuejs.org/guide/migration/filters.html#migration-strategy

四、第一个案例——品牌列表案例

1.案例描述

1.1 案例效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PBXmoDag-1685004795661)(D:.\assets\品牌列表案例——案例效果.png)]

2.2 用到的知识点
bootstrap 4.x 相关的知识点vue 指令与过滤器 相关的知识点
卡片(Card)、表单相关(Forms)、按钮(Buttons)、表格(Tables)插值表达式、属性绑定、事件绑定、双向数据绑定、修饰符、条件渲染、列表渲染、全局过滤器
2.3 整体实现步骤
  1. 创建基本的 vue 实例
  2. 基于 vue 渲染表格数据
  3. 实现添加品牌的功能
  4. 实现删除品牌的功能
  5. 实现修改品牌状态的功能

2.案例实现

2.1 创建基本的 Vue 实例

步骤1:导入 vue 的 js 文件

<script src="./lib/vue-2.6.12.js"></script>

步骤2:在 标签中声明 el 区域

<div id="app">

步骤3:创建 vue 实例对象

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            brandlist: [
                { id: 1, name: '宝马', state: true, addtime: new Date() },
                { id: 2, name: '奥迪', state: true, addtime: new Date() },
                { id: 3, name: '奔驰', state: true, addtime: new Date() },
            ],
        },
    })
</script>
2.2 基于 Vue 渲染表格数据

步骤1:使用 v-for 指令循环渲染表格的数据:

<!-- TODO:循环渲染表格的每一行数据 -->
<tr v-for="(item, index) in brandlist" :key="item.id">
    <td>{{index + 1}}</td>
    <td>{{item.brandname}}</td>
    <td>{{item.state}}</td>
    <td>{{item.addtime}}</td>
    <td>
        <a href="#">删除</a>
    </td>
</tr>

步骤2:将品牌的状态渲染为 Switch 开关效果:

<td>
    <div class="custom-control custom-switch">
        <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.state">
        <label class="custom-control-label" :for="item.id" v-if="item.state">已启用</label>
        <label class="custom-control-label" :for="item.id" v-else>已禁用</label>
    </div>
</td>

**Switch 开关效果的官方文档地址:**https://v4.bootcss.com/docs/components/forms/#switches

步骤3:使用全局过滤器对时间进行格式化:

<!-- 对 创建时间 这一项调用 dateFormat 过滤器 -->
<td>{{item.addtime | dateFormat}}</td>

<script>
    // 创建全局的过滤器 dateFormat
    Vue.filter('dateFormat', (dateStr) => {
    const dt = new Date(dateStr)

    const y = dt.getFullYear()
    const m = padZero(dt.getMonth() + 1)
    const d = padZero(dt.getDate())

    const hh = padZero(dt.getHours())
    const mm = padZero(dt.getMinutes())
    const ss = padZero(dt.getSeconds())

    // 模板字符串进行时间的格式拼接
    return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
    })

    // 补零函数
    padZero = (n) => {
    	return n > 9 ? n : '0' + n
    }
    
</script>
2.3 添加品牌

步骤1:阻止表单的默认提交行为:

<form class="form-inline" @submit.prevent>

步骤2:为 input 输入框进行 v-model 双向数据绑定:

<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname" />

注意:需要在 data 数据中声明 brandname 属性字段。

data: {
	brandname: '',
},

步骤3:为“添加品牌”的 button 按钮绑定 click 事件处理函数:

<button type="submit" class="btn btn-primary mb-2" @click="addNewbrand">添加品牌</button>

步骤4:在 data 中声明 nextId 属性(用来记录下一个可用的 id 值),并在 methods 中声明

addNewBrand 事件处理函数:

data: {
	nextId: 4,
},
methods: {
	// 添加新的品牌数据
	addNewbrand() {
		if (!this.brandname) return alert('品牌名称不能为空!')
		this.brandlist.push({
			id: this.nextId,
			brandname: this.brandname,
			state: true,
			addtime: new Date()
		})
		this.brandname = ''
		this.nextId++
	},
},

步骤5:监听 input 输入框的 keyup 事件,通过 .esc 按键修饰符快速清空文本框中的内容:

<input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname" @keyup.esc="brandname = ''" />
2.4 删除品牌

步骤1:为删除的 a 链接绑定 click 点击事件处理函数,并阻止其默认行为:

<a href="#" @click.prevent="removeBrand(item.id)">删除</a>

步骤2:在 methods 节点中声明 removeBrand 事件处理函数如下:

// 删除品牌 根据id删除对应的数据
removeBrand() {
    this.brandlist = this.brandlist.filter(x => x.id !== id)
},

3.案例最终代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 导入 bootstrap 的样式表 -->
    <!-- https://v4.bootcss.com/docs/components/forms/#switches -->
    <link rel="stylesheet" href="./lib/bootstrap.css" />
    <style>
        :root {
            font-size: 13px;
        }

        body {
            padding: 8px;
        }
    </style>
</head>

<body>
    <div id="app">
        <!-- 卡片区域 -->
        <div class="card">
            <h5 class="card-header">添加品牌</h5>
            <div class="card-body">
                <!-- 添加品牌的表单 -->
                <form class="form-inline" @submit.prevent>
                    <div class="input-group mb-2 mr-sm-2">
                        <div class="input-group-prepend">
                            <div class="input-group-text">品牌名称</div>
                        </div>
                        <!-- 文本输入框 -->
                        <input type="text" class="form-control" placeholder="请输入品牌名称" v-model.trim="brandname"
                            @keyup.esc="brandname = ''" />
                    </div>

                    <!-- 提交表单的按钮 -->
                    <button type="submit" class="btn btn-primary mb-2" @click="addNewbrand">添加品牌</button>
                </form>
            </div>
        </div>
        <!-- 品牌列表 -->
        <table class="table table-bordered table-striped mt-2">
            <thead>
                <tr>
                    <th>#</th>
                    <th>品牌名称</th>
                    <th>状态</th>
                    <th>创建时间</th>
                    <th>操作</th>
                </tr>
            </thead>
            <!-- 表格的主体区域 -->
            <tbody>
                <!-- TODO:循环渲染表格的每一行数据 -->
                <tr v-for="(item, index) in brandlist" :key="item.id">
                    <td>{{index + 1}}</td>
                    <td>{{item.brandname}}</td>
                    <td>
                        <div class="custom-control custom-switch">
                            <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.state">
                            <label class="custom-control-label" :for="item.id" v-if="item.state">已启用</label>
                            <label class="custom-control-label" :for="item.id" v-else>已禁用</label>
                        </div>
                    </td>
                    <td>{{item.addtime | dateFormat}}</td>
                    <td>
                        <a href="#" @click.prevent="removeBrand(item.id)">删除</a>
                    </td>
                </tr>
            </tbody>
        </table>
    </div>
    <script src="./lib/vue-2.6.12.js"></script>
    <script>

        // 创建全局的过滤器 dateFormat
        Vue.filter('dateFormat', (dateStr) => {
            const dt = new Date(dateStr)

            const y = dt.getFullYear()
            const m = padZero(dt.getMonth() + 1)
            const d = padZero(dt.getDate())

            const hh = padZero(dt.getHours())
            const mm = padZero(dt.getMinutes())
            const ss = padZero(dt.getSeconds())

            return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
        })

        // 补零函数
        padZero = (n) => {
            return n > 9 ? n : '0' + n
        }

        const vm = new Vue({
            el: '#app',
            data: {
                brandname: '',
                nextId: 4,
                brandlist: [
                    { id: 1, brandname: '宝马', state: true, addtime: new Date() },
                    { id: 2, brandname: '奥迪', state: false, addtime: new Date() },
                    { id: 3, brandname: '奔驰', state: true, addtime: new Date() },

                ],
            },
            methods: {
                // 添加新的品牌数据
                addNewbrand() {
                    if (!this.brandname) return alert('品牌名称不能为空!')
                    this.brandlist.push({
                        id: this.nextId,
                        brandname: this.brandname,
                        state: true,
                        addtime: new Date()
                    })
                    this.brandname = ''
                    this.nextId++
                },
                // 删除品牌
                removeBrand(id) {
                    this.brandlist = this.brandlist.filter(x => x.id !== id)
                },
            },
        })
    </script>
</body>

</html>

五、Vue基础入门总结

一、能够知道 vue 的基本使用步骤

  • 导入 vue.js 文件
  • new Vue() 构造函数,得到 vm 实例对象
  • 声明 el 和 data 数据节点
  • MVVM 的对应关系

二、掌握 vue 中常见指令的基本用法

  • 插值表达式、v-bind、v-on、v-if 和 v-else
  • v-for 和 :key、v-model

三、掌握 vue 中过滤器的基本用法

  • 全局过滤器 Vue.filter(‘过滤器名称’, function)
  • 私有过滤器 filters 节点

Ⅱ vue组件基础(上)

目标目录
了解什么是单页面应用程序六、单页面应用程序
了解如何用 vite 创建项目七、vite 的基本使用
组件化开发的优点与好处八、组件化开发思想
template、script、style三个节点九、vue 组件的构成
组件的注册、样式冲突、props、动态绑定样式十、组件的基本使用
实现一个简单组件的封装十一、第二个案例——封装组件案例
总结与概括十二、vue 组件基础(上)总结

六、单页面应用程序

1.什么是单页面应用程序

单页面应用程序(英文名:Single Page Application)简称 SPA,顾名思义,指的是一个 Web 网站中只有唯一的一个 HTML 页面,所有的功能与交互都在这唯一的一个页面内完成

2.单页面应用程序的特点

单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源( HTML、JavaScript 和 CSS)。

一旦页面加载完成了,SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换HTML 的内容,从而实现页面与用户的交互。

3.单页面应用程序的优缺点

SPA 单页面应用程序最显著的 3 个优点如下:

① 良好的交互体验

​ 🤞单页应用的内容的改变不需要重新加载整个页面

​ 🤞获取数据也是通过 Ajax 异步获取

​ 🤞没有页面之间的跳转,不会出现“白屏现象”

② 良好的前后端工作分离模式

​ 🤞后端专注于提供 API 接口,更易实现 API 接口的复用

​ 🤞前端专注于页面的渲染,更利于前端工程化的发展

③ 减轻服务器的压力

​ 🤞服务器只提供数据,不负责页面的合成与逻辑的处理,吞吐能力会提高几倍

任何一种技术都有自己的局限性,对于 SPA 单页面应用程序来说,主要的缺点有如下两个:

① 首屏加载慢

解决方式:

​ 🤞路由懒加载

​ 🤞代码压缩

​ 🤞CDN 加速

​ 🤞网络传输压缩

② 不利于 SEO

解决方式:

​ 🤞SSR 服务器端渲染

4.如何快速创建Vue的SPA项目

vue 官方提供了两种快速创建工程化的 SPA 项目的方式:

基于 vite 创建 SPA 项目

基于 vue-cli 创建 SPA 项目

vitevue-cli
支持的 vue 版本仅支持 vue3.x支持 3.x 和 2.x
是否基于 webpack
运行速度较慢
功能完整度小而巧(逐渐完善)大而全
是否建议在企业级开发中使用目前不建议建议在企业级开发中使用

七、vite的基本使用

1.创建 vite 项目

基于 vite 创建 vue 3.x 的工程化项目

在你想创建项目的位置打开 PowerShell 窗口,输入:

npm init vite-app 项目名称

然后 cd 到项目目录中:

cd 项目名称

安装 npm 依赖包:

npm i

运行 dev 启动项目:

npm run dev

2.梳理项目的结构

其中:

🤞node_modules 目录用来存放第三方依赖包

🤞public 是公共的静态资源目录

🤞src 是项目的源代码目录(程序员写的所有代码都要放在此目录下)

🤞assets 目录用来存放项目中所有的静态资源文件(css、fonts等)

🤞components 目录用来存放项目中所有的自定义组件

🤞App.vue 是项目的根组件

🤞index.css 是项目的全局样式表文件

🤞main.js 是整个项目的打包入口文件

🤞.gitignore 是 Git 的忽略文件

🤞index.html 是 SPA 单页面应用程序中唯一的 HTML 页面

🤞package.json 是项目的包管理配置文件

3. vite 项目的运行流程

在工程化的项目中,vue 要做的事情很单纯:通过 main.jsApp.vue 渲染到 index.html 的指定区域中。

其中:

App.vue 用来编写待渲染的模板结构

index.html 中需要预留一个 el 区域

main.js 把 App.vue 渲染到了 index.html 所预留的区域中

3.1 在 App.vue 中编写模板结构
<template>
  <h1>这是app.vue根组件</h1> 
  <h3>avc</h3>
</template>
3.2 在 index.html 中预留 el 区域
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body
3.3 在 main.js 中把 App.vue 渲染到 index.html 所预留的区域中
// 1.按需导入 createApp 函数
import { createApp } from 'vue'
// 2.导入待渲染的 App.vue 组件
import App from './App.vue'

// 3.调用 createApp 函数,创建 SPA 应用的实例
const app = createApp(App)

// 4.调用 mount() 方法把 App 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')

八、组件化开发思想

1.什么是组件化开发思想

组件化开发指的是:根据封装的思想,把页面上可重用的部分封装为组件,从而方便项目的开发和维护。

例如:http://www.ibootstrap.cn/ 所展示的效果,就契合了组件化开发的思想。用户可以通过拖拽组件的方式,快速生成一个页面的布局结构。

2.前端组件化开发的好处

前端组件化开发的好处主要体现在以下两方面:

  • 提高了前端代码的复用性和灵活性
  • 提升了开发效率和后期的可维护性

3.vue中的组件化开发

vue 是一个完全支持组件化开发的框架vue 中规定组件的后缀名.vue。之前接触到的 App.vue 文件本质上就是一个 vue 的组件

九、vue 组件的构成

1.vue 组件组成结构

每个 .vue 组件都由 3 部分构成,分别是:

  • template -> 组件的模板结构
  • script -> 组件的 JavaScript 行为
  • style -> 组件的样式

其中,每个组件中必须包含 template 模板结构,而 script 行为和 style 样式是可选的组成部分。

2.组件的 template 节点

vue 规定:每个组件对应的模板结构,需要定义到 节点中。

<template>
<!-- 当前组件的 DOM 结构,需要定义到 template 标签的内部 -->
</template>

注意: 是 vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素。

2.1 在 template 中使用指令

在组件的 节点中,支持使用前面所学的指令语法,来辅助开发者渲染当前组件的 DOM 结构。

2.2 在 template 中定义根节点

在 vue 2.x 的版本中, 节点内的 DOM 结构仅支持单个根节点

但是,在 vue 3.x 的版本中, 中支持定义多个根节点

3.组件的 script 节点

vue 规定:组件内的

script 节点的基本结构如下:

<script>
    // 今后,组件相关的 data 数据, methods 方法等
    // 都需要的定义到 export default 所导出的对象中。
    export default {}
</script>
3.1 script 中的 name 节点

可以通过 name 节点为当前组件定义一个名称:每个首字母大写

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XLeyXqTG-1685004795662)(.\assets\Snipaste_2023-05-02_16-25-54.png)]

在使用 vue-devtools 进行项目调试的时候,自定义的组件名称可以清晰的区分每个组件

3.2 script 中的 data 节点

vue 组件渲染期间需要用到的数据,可以定义在 data 节点中:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5VcU35f1-1685004795662)(.\assets\Snipaste_2023-05-02_16-27-13.png)]

vue 规定:组件中的 data 必须是一个函数,不能直接指向一个数据对象。

3.3 script 中的 methods 节点

组件中的事件处理函数,必须定义到 methods 节点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mGM1CQFC-1685004795662)(.\assets\Snipaste_2023-05-02_17-14-01.png)]

4.组件的 style 节点

vue 规定:组件内的

<style lang="css">
    h1{
        font-weight: normal;
    }
</style>

其中

4.1 让 style 中支持 less 语法

如果希望使用 less 语法编写组件的 style 样式,可以按照如下两个步骤进行配置:

① 运行 npm install less -D 命令安装依赖包,从而提供 less 语法的编译支持

② 在

<style lang="less">
    h1{
        font-weight: normal;
        i{
            color: red;
            font-style: normal;
        }
    }
</style>

十、组件的基本使用

1.组件的注册

组件之间可以进行相互的引用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yn2STzcv-1685004795663)(./assets/Snipaste_2023-05-02_19-34-03.png)]

vue 中组件的引用原则:先注册后使用

1.1 注册组件的两种方式

vue 中注册组件的方式分为“全局注册”和“局部注册”两种,其中:

  • 全局注册的组件,可以在全局任何一个组件内使用
  • 局部注册的组件,只能在当前注册的范围内使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nwCmvBt1-1685004795663)(./assets/Snipaste_2023-05-02_19-34-16.png)]

1.2 全局注册组件
// 1.按需导入 createApp 函数
import { createApp } from 'vue'
// 2.导入待渲染的 App.vue 组件
import App from './App.vue'

// ① 导入需要被全局注册的组件
import Swiper from './components/01.globalReg/Swiper.vue'
import Test from './components/01.globalReg/Test.vue'

// 3.调用 createApp 函数,创建 SPA 应用的实例
const app = createApp(App)

// ② 全局注册组件
app.component('my-swiper', Swiper)
app.component('my-test', Test)
// 4.调用 mount() 方法把 App 组件的模板结构,渲染到指定的 el 区域中
app.mount('#app')
1.3 使用全局注册组件

使用app.component() 方法注册的全局组件,直接以标签的形式进行使用即可

<my-swiper></my-swiper>
<my-test></my-test>
1.4 局部注册组件
<template>
<my-search></my-search>
</template>
export default {
	components: {
    	"my-search": Search,
	},
}
1.5 全局注册和局部注册的区别
  • 全局注册的组件,可以在全局任何一个组件内使用
  • 局部注册的组件,只能在当前注册的范围内使用

应用场景:

如果某些组件在开发期间的使用频率很高,推荐进行全局注册;

如果某些组件只在特定的情况下会被用到,推荐进行局部注册。

1.6 组件注册时名称的大小写

在进行组件的注册时,定义组件注册名称的方式有两种:

①使用kebab-case命名法(俗称短横线命名法,例如my-swiper 和my-search)

②使用PascalCase命名法(俗称帕斯卡命名法或大驼峰命名法,例如MySwiper和MySearch)

短横线命名法的特点:

  • 必须严格按照短横线名称进行使用

帕斯卡命名法的特点:

  • 既可以严格按照帕斯卡名称进行使用,又可以转化为短横线名称进行使用

注意:在实际开发中,推荐使用帕斯卡命名法为组件注册名称,因为它的适用性更强。

1.7 通过 name 属性注册组件

在注册组件期间,除了可以直接提供组件的注册名称之外,还可以把组件的name 属性作为注册后组件的名称,示例代码如下:

app.component(Test.name, Test)

2.组件之间的样式冲突问题

默认情况下,写在.vue 组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。导致组件

之间样式冲突的根本原因是:

①单页面应用程序中,所有组件的DOM 结构,都是基于唯一的index.html 页面进行呈现的

②每个组件中的样式,都会影响整个index.html 页面中的DOM 元素

2.1 解决样式冲突的问题

为每个组件分配唯一的自定义属性,在编写组件样式时,通过属性选择器来控制样式的作用域,示例代码如下:

<template>
	<div class="container" data-v-001>
		<h3 data-v-001>轮播图组件</h3>
	</div>
</template>

<style>
    /*  通过中括号“属性选择器”,来防止组件之间的样式冲突问题,
    	因为每个组件分配的自定义属性是“唯一”的  */
    .container[data-v-001]{
        border: 1px solid red;
    }
</style>
2.2 style 节点的 scoped 属性

为了提高开发效率和开发体验,vue 为style 节点提供了scoped属性,从而防止组件之间的样式冲突问题:

<style lang="less" scoped>
p {
  color: red;
}
</style>
2.3 /deep/ 样式穿透

如果给当前组件的style 节点添加了scoped 属性,则当前组件的样式对其子组件是不生效的。如果想让某些样式对子组件生效,可以使用**/deep/ 深度选择器**。

注意:/deep/是vue2.x 中实现样式穿透的方案。在vue3.x 中推荐使用:**deep()**替代/deep/。

<style lang="less" scoped>
p {
  color: red;
}

/* /deep/ .title {
   color: blue;
 }*/

:deep(.title) {
  color: blue;
}
</style>

3.组件的 props

为了提高组件的复用性,在封装vue 组件时需要遵守如下的原则:

  • 组件的DOM 结构Style 样式要尽量复用
  • 组件中要展示的数据,尽量由组件的使用者提供

为了方便使用者为组件提供要展示的数据,vue 组件提供了props 的概念。

3.1 什么是组件的 props

props 是组件的自定义属性,组件的使用者可以通过props 把数据传递到子组件内部,供子组件内部进行使用。

props 的作用:父组件通过props 向子组件传递要展示的数据。

props 的好处:提高了组件的复用性。

代码示例如下:

<!-- 通过自定义 props,把文章的标题和作者,传递到 my-article -->
<my-article title="面朝大海,春暖花开" author="海子"></my-article>
3.2 在组件中声明 props

在封装vue 组件时,可以把动态的数据项声明为props自定义属性。自定义属性可以在当前组件的模板结构中被直接使用。

<!-- my-article 组件的定义如下: -->
<template>
	<h3>标题:{{title}}</h3>
    <h3>作者:{{author}}</h3>
</template>

<script>
export default {
    props: ['title','author'], //父组件传递 my-article 组件的数据,必须在 props 节点中声明
}
</script>
3.3 无法使用未声明的 props

如果父组件给子组件传递了未声明的props 属性,则这些属性会被忽略,无法被子组件使用

<!-- my-article 组件的定义如下: -->
<template>
	<h3>标题:{{title}}</h3>
    <h3>作者:{{author}}</h3>
</template>

<script>
export default {
    name: 'MyArticle',
    // 外界可以传递指定的数据,到当前的组件中
    props: ['title'], // author 属性没有声明,因此子组件中无法访问到 author 的值
}
</script>
3.4 动态绑定 props 的值

可以使用v-bind 属性绑定的形式,为组件动态绑定props 的值

<my-article :title="info.title" :author="'post by ' + info.author" pub-time="1989"></my-article>
3.5 props 的大小写命名

组件中如果使用“camelCase(驼峰命名法)”声明了props 属性的名称,则有两种方式为其绑定属性的值

<template>
    <!-- 两种都可以 -->
	<!-- 短横线分割命名 -->
    <my-article :title="info.title" :author="'post by ' + info.author" pub-time="1989"></my-article>
	<!-- 驼峰命名 -->
    <my-article :title="info.title" :author="'post by ' + info.author" pubTime="1989"></my-article>
</template>

<script>
export default {
  name: 'MyArticle',
  // 外界可以传递指定的数据,到当前的组件中
  props: ['title', 'author', 'pubTime']
}
</script>

4. Class 和 Style 绑定

在实际开发中经常会遇到动态操作元素样式的需求。因此,vue 允许开发者通过 v-bind 属性绑定指令,为元素动态绑定 class 属性的值和行内的 style 样式。

4.1 动态绑定 HTML 的 class

可以通过三元表达式,动态的为元素绑定 class 的类名。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yJe7Lyqk-1685004795663)(./assets/Snipaste_2023-05-06_20-02-35.png)]

4.2 以数组语法绑定 HTML 的 class

如果元素需要动态绑定多个 class 的类名,此时可以使用数组的语法格式:

<h3 class="thin" :class="[isItalic ? 'italic' : '', isDelete ? 'delete' : '']">MyStyle 组件</h3>

<script>
export default {
  name: 'MyStyle',
  data() {
    return {
      // 字体是否倾斜
      isItalic: false,
      // 是否应用删除效果
      isDelete: false,
    }
  },
}
</script>

<style lang="less">
// 字体变细
.thin {
  font-weight: 200;
}

// 倾斜字体
.italic {
  font-style: italic;
}

.delete {
  text-decoration: line-through;
}
</style>
4.3 以对象语法绑定 HTML 的 class
<template>
<h3 class="thin" :class="classObj">MyStyle 组件</h3>
<button @click="classObj.italic = !classObj.italic">Toggle Italic</button>
<button @click="classObj.delete = !classObj.delete">Toggle Delete</button>
</template>

<script>
export default {
  name: 'MyStyle',
  data() {
    return {
      // 字体是否倾斜
      isItalic: false,
      // 是否应用删除效果
      isDelete: false,]
      classObj: {
        italic: false,
        delete: false,
      },
    }
  },
}
</script>

<style lang="less">
// 字体变细
.thin {
  font-weight: 200;
}

// 倾斜字体
.italic {
  font-style: italic;
}

.delete {
  text-decoration: line-through;
}
</style>
4.4 以对象语法绑定内联的 style
<template>
<div :style="{ color: active, fontSize: fsize + 'px', 'background-color': bgcolor }">黑马程序员</div>
	<button @click="fsize+=1">字号 +1</button>
	<button @click="fsize-=1">字号 -1</button>
</template>

<script>
export default {
  name: 'MyStyle',
  data() {
    return {
      // 高亮时的文本颜色
      active: 'red',
      // 文字的大小
      fsize: 30,
      // 背景颜色
      bgcolor: 'pink',
      },
    }
  },
}
</script>

十一、第二个案例——封装组件案例

1.案例描述

1.1 案例效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z6gMlngQ-1685004795664)(./assets/Snipaste_2023-05-07_11-15-55.png)]

封装要求:

  1. 允许用户自定义 title 标题
  2. 允许用户自定义 bgcolor 背景色
  3. 允许用户自定义 color 文本颜色
  4. MyHeader 组件需要在页面顶部进行 fixed 固定定位,且 z-index 等于 999

使用示例如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4hvWeDkp-1685004795664)(./assets/Snipaste_2023-05-07_11-16-16.png)]

1.2 用到的知识点
  • 组件的封装与注册
  • props
  • 样式绑定
1.3 整体实现步骤
  • 创建 MyHeader 组件
  • 渲染 MyHeader 组件的基本结构
  • 在 App 组件中注册并使用 MyHeader 组件
  • 通过 props 为组件传递数据

2.案例实现代码

<!-- MyHeader.vue组件部分 -->
<template>
  <div
    class="header-container"
    :style="{ backgroundColor: bgcolor, color: color }"
  >
    xxx
  </div>
  {{ title || "Header 组件" }}
</template>

<script>
export default {
  name: "MyHeader",
  props: ["title", "bgcolor", "color"],
};
</script>

<style lang="less" scoped>
.header-container {
  height: 45px;
  background-color: pink;
  text-align: center;
  line-height: 45px;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 999;
}
</style>
<!-- app.vue部分 -->
<template>
  <div class="app-container">
    <h1>App根组件</h1>
  </div>
  <hr />
  <MyHeader title="黑马程序员" bgcolor="blue" color="white"></MyHeader>
</template>

<script>
import MyHeader from "./06.MyHeader/MyHeader.vue";
export default {
  name: "MyApp",
  components: {
    MyHeader,
  },
};
</script>

<style lang="less" scoped>
.app-container {
  margin-top: 45px;
}
</style>

十二、vue 组件基础(上)总结

① 能够说出什么是单页面应用程序及组件化开发

🤞SPA、只有一个页面、组件是对 UI 结构的复用

② 能够说出 .vue 单文件组件的组成部分

🤞template、script、style(scoped、lang)

③ 能够知道如何注册 vue 的组件

🤞全局注册(app.component)、局部注册(components)

④ 能够知道如何声明组件的 props 属性

🤞props 数组

④ 能够知道如何在组件中进行样式绑定

🤞动态绑定 class、动态绑定 style

Ⅲ vue组件基础(下)

目标目录
能够知道如何对 props 进行验证十三、props 验证
能够知道如何使用计算属性十四、计算属性
能够知道如何为组件自定义事件十五、自定义事件
能够知道如何在组件上使用 v-model十六、组件上的 v-model
实现任务列表案例十七、任务列表案例
总结和概括十八、vue 组件基础(下)总结

十三、props验证

1.什么是 props 验证

props 验证指的是:在封装组件时对外界传递过来的 props 数据进行合法性的校验,从而防止数据不合法的问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rAIVbEBd-1685004795665)(./assets/Snipaste_2023-05-07_13-45-04.png)]

使用数组类型的 props 节点的缺点:无法为每个 prop 指定具体的数据类型

2.对象类型的 props 节点

使用对象类型的 props 节点,可以对每个 prop 进行数据类型的校验,示意图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X5IgXS1i-1685004795665)(./assets/Snipaste_2023-05-07_13-48-11.png)]

3.props 验证

对象类型的 props 节点提供了多种数据验证方案,例如:

基础的类型检查

多个可能的类型

必填项校验

属性默认值

自定义验证函数

3.1 基础的类型检查

可以直接为组件的 prop 属性指定基础的校验类型,从而防止组件的使用者为其绑定错误类型的数据:

<script>
export default{
    props:{
        propA: String,  // 字符串类型
        propB: Number,  // 数字类型
        propC: Boolean,	// 布尔值类型
        propD: Array,	// 数组类型
        propE: Object,	// 对象类型
        propF: Date,	// 日期类型
        propG: Function,// 函数类型
        propH: Symbol,	// 符号类型
    }
}
</script>
3.2 多个可能的类型

如果某个 prop 属性值的类型不唯一,此时可以通过数组的形式,为其指定多个可能的类型:

<script>
export default{
    props:{
        // propA 属性的值可以是“字符串”或“数字”
        propA: [String,Number]  
    }
}
</script>
3.3 必填项校验

如果组件的某个 prop 属性是必填项,必须让组件的使用者为其传递属性的值。此时,可以通过如下的方式将其设置为必填项:

<script>
export default{
  	props: {
        // 通过“配置对象”的形式,来定义 propB 属性的“验证规则”
    	propB: {
      		type: Number, // 当前属性的值必须是 String 字符串类型
      		required: true, // 当前属性的值是必填项,如果使用者没指定 propB 属性的值,则在终端进行警告提示
    	},
  	}  
}
</script>
3.4 属性默认值

在封装组件时,可以为某个 prop 属性指定默认值:

<script>
export default{
  	props: {
        // 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
    	propC: {
      		type: Number,
      		default: 100, // 如果使用者没有指定 propC 的值,则 propC 属性的默认值为 100
    	},
  	}  
}
</script>
3.5 自定义验证函数

在封装组件时,可以为 prop 属性指定自定义的验证函数,从而对 prop 属性的值进行更加精确的控制:

<script>
export default{
    props: {
        // 通过“配置对象”的形式,来定义 propC 属性的“验证规则”
        propD: {
            // 通过 calidator 函数,对 propD 属性的值进行校验,“属性的值”可以通过形参 value 进行接收
            validator(value) {
                // propD 属性的值,必须匹配下列字符串中的一个
                // validator 函数的返回值为 true 表示验证通过,false 表示验证失败
                return ['success', 'warning', 'danger'].indexOf(value) !== -1
            }
        }
    }
}
</script>

十四、计算属性

1.什么是计算属性

计算属性本质上就是一个 function 函数,它可以实时监听 data 中数据的变化,并 return 一个计算后的新值,供组件渲染 DOM 时使用。

2.如何声明计算属性

计算属性需要以 function 函数的形式声明到组件的 computed 选项中:

<template>
  <div>
    <input type="text" v-model.number="count" />
    <p>{{ count }} 乘以 2 的值为:{{ plus }}</p>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  data() {
    return {
      count: 1,
    }
  },
  computed: {
    plus() {
      console.log('计算属性被执行了')
      return this.count * 2
    },
  },
}
</script>

注意:计算属性侧重于得到一个计算的结果,因此计算属性中必须有 return 返回值

3.计算属性的使用注意点

① 计算属性必须定义在 computed 节点中

② 计算属性必须是一个 function 函数

③ 计算属性必须有返回值

④ 计算属性必须当做普通属性使用

4.计算属性和方法的区别

相对于方法来说,计算属性会缓存计算的结果,只有计算属性的依赖项发生变化时,才会重新进行运算。因此计算属性的性能更好:

<script>
export default{  
computed: {
	plus() {
		console.log('计算属性被执行了')
		return this.count * 2
	},
},
methods: {
	plus() {
		console.log('方法被执行了')
		return this.count * 2
	}
}
}  
</script>

5.计算属性案例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmWjnVuN-1685004795665)(./assets/Snipaste_2023-05-07_15-58-25.png)]

5.1 动态计算已勾选商品的总数量
<template>
    <!-- TODO: 1. 动态计算已勾选的商品的总数量 -->
    <span>总数量:{{ total }}</span>
</template>

<script>
export default {  
  computed: {
    // 动态计算出勾选水果的总数量
    total() {
      let t = 0;
      this.fruitlist.forEach((x) => {
        if (x.state) {
          t += x.count;
        }
      })
      return t;
    },
  },
}
</script>
5.2 动态计算已勾选的商品的总价
<template>
      <!-- TODO: 2. 动态计算已勾选的商品的总价 -->
      <span>总价:{{ amount }}</span>
</template>

<script>
export default {  
  computed: {
    // 动态计算已勾选的商品的总价
    amount(){
		let a = 0;
        this.fruitlist
        	.filter((x) => x.state)
        	.forEach((x) => {
            	a += x.price * x.count;
        	});
        return a;
    },
  },
}
</script>
5.3 控制按钮的禁用状态
<template>
      <!-- TODO: 3. 动态计算按钮的禁用状态 -->
      <button type="button" class="btn btn-primary" :disabled="isdisabled">
</template>

<script>
export default {  
  computed: {
	// 控制按钮的禁用状态
    isdisabled() {
      return this.total === 0;
    },
  },
};
</script>

十五、自定义事件

1.什么是自定义事件

在封装组件时,为了让组件的使用者可以监听到组件内状态的变化,此时需要用到组件的自定义事件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K4L8Ciia-1685004795665)(./assets/Snipaste_2023-05-08_14-19-26.png)]

2.自定义事件的3个使用步骤

封装组件时:

声明自定义事件

触发自定义事件

使用组件时:

监听自定义事件

2.1 声明自定义事件

开发者为自定义组件封装的自定义事件,必须事先在 emits 节点中声明:

<template>
  <div>
    <p>count 的值是:{{ count }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  // 1. 声明自定义事件
  emits: ['countChange'],
  data() {
    return {
      count: 0,
    }
  },
}
</script>
2.2 触发自定义事件

在 emits 节点下声明的自定义事件,可以通过 this.$emit(‘自定义事件的名称’) 方法进行触发:

<template>
  <div>
    <p>count 的值是:{{ count }}</p>
    <button @click="add">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  methods: {
    add() {
      this.count++
      // 2. this.$emit() 触发自定义事件
      this.$emit('countChange')
    },
  },
}
</script>
2.3 监听自定义事件

在使用自定义的组件时,可以通过 v-on 的形式监听自定义事件:

<template>
  <div>
    <h1>app 根组件</h1>
    <hr />
    <my-counter @countChange="getCount"></my-counter>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  methods: {
    getCount() {
      console.log('触发了 countChange 自定义事件')
    },
  },
  components: {
    MyCounter,
  },
}
</script>

3.自定义事件传参

在调用 this.$emit() 方法触发自定义事件时,可以通过第 2 个参数为自定义事件传参,示例代码如下:

<script>      
this.$emit('countChange', this.count) // 触发自定义事件时,通过第二个参数传参
</script>

<script>
export default {
  name: 'MyApp',
  methods: {
    getCount(val) {
      console.log('触发了 countChange 自定义事件', val)
    },
  },
  components: {
    MyCounter,
  },
}
</script>

十六、组件上的 v-model

1. 为什么需要在组件上使用 v-model

v-model 是双向数据绑定指令,当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。示意图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lldOBzlP-1685004795666)(./assets/Snipaste_2023-05-08_15-16-00.png)]

  • 外界数据的变化自动同步到 counter 组件中
  • counter 组件中数据的变化,也会自动同步到外界

2. 在组件上使用 v-model 的步骤

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4RTfAPQC-1685004795666)(./assets/Snipaste_2023-05-08_15-20-25.png)]

父 -》子 同步数据

父组件通过 v-bind: 属性绑定的形式,把数据传递给子组件

子组件中,通过 props 接收父组件传递过来的数据

子 -》父 同步数据

在 v-bind: 指令之前添加 v-model 指令

在子组件中声明 emits 自定义事件,格式为 update:xxx

调用 $emit() 触发自定义事件,更新父组件中的数据

十七、第三个案例——任务列表案例

1.案例效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IHqRMJfO-1685004795666)(./assets/Snipaste_2023-05-08_15-41-36.png)]

2.用到的知识点

① vite 创建项目

② 组件的封装与注册

③ props

④ 样式绑定

⑤ 计算属性

⑥ 自定义事件

⑦ 组件上的 v-model

3.整体实现步骤

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j2vAdedr-1685004795666)(./assets/Snipaste_2023-05-08_15-48-46.png)]

① 使用 vite 初始化项目

② 梳理项目结构

③ 封装 todo-list 组件

④ 封装 todo-input 组件

⑤ 封装 todo-button 组件

4.具体实现

4.1 初始化项目

1.在终端运行以上的命令,初始化 vite 项目:

npm init vite-app todo

2.使用 vscode 打开项目,并安装依赖项:

npm install

3.安装 less 语法相关的依赖项:

npm i less -D
4.2 梳理项目结构

1.重置 index.css 中的全局样式如下:

:root {
  font-size: 12px;
}

body {
  padding: 8px;
}

2.重置 App.vue 组件的代码结构如下:

<template>
  <div>
    <h1>App 根组件</h1>
  </div>
</template>

<script>
export default {
  name: "MyApp",
  data() {
    return {
      // 任务列表数据
      todolist: [
        { id: 1, task: "周一早晨9点开会", done: false },
        { id: 2, task: "周一晚上8点聚餐", done: false },
        { id: 3, task: "准备周三上午的演讲稿", done: true },
      ],
    };
  },
};
</script>

<style lang="less" scoped></style>

3.删除 components 目录下的 HelloWorld.vue 组件。

4.在终端运行以下的命令,把项目运行起来:

npm run dev
4.3 封装 todo-list 组件

4.3.1 创建并注册 TodoList 组件

1.在 src/components/todo-list/ 目录下新建 TodoList.vue 组件:

<template>
  <div>TodoList 组件</div>
</template>

<script>
export default {
  name: "TodoListVue",
};
</script>

<style lang="less" scoped>
</style>

2.在 App.vue 组件中导入并注册 TodoList.vue 组件:

<template>
	<!-- 使用 TodoList 组件 -->
    <todo-list-vue></todo-list-vue>
</template>

<script>
// 导入 TodoList 组件
import TodoListVue from "./components/TodoList.vue";

export default {
  name: "MyApp",
  // 注册 TodoList 组件   
  components: {
    TodoListVue,
  },
};
</script>

4.3.2 基于 bootstrap 渲染列表组件

1.在 main.js 入口文件中,导入 src/assets/css/bootstrap.css 样式表:

import { createApp } from 'vue'
import App from './App.vue'

// 导入 bootstrap.css 样式表
import './assets/css/bootstrap.css'
import './index.css'

createApp(App).mount('#app')

2.根据 bootstrap 提供的列表组(https://v4.bootcss.com/docs/components/list-group/#with-badges)和复选框(https://v4.bootcss.com/docs/components/forms/#checkboxes-and-radios-1)渲染列表组件和基本效果:

<template>
  <!-- 列表组 -->
  <ul class="list-group">
    <li class="list-group-item d-flex justify-content-between align-items-center">
      <!-- 复选框 -->
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" id="customCheck1">
        <label class="custom-control-label" for="customCheck1">Check this custom checkbox</label>
      </div>
      <!-- 徽标 badge 效果 -->
      <span class="badge badge-success badge-pill">完成</span>
      <span class="badge badge-warning badge-pill">未完成</span>
    </li>    
  </ul>
</template>

4.3.3 为 TodoList 声明 props 属性

1.为了接受外界传递过来的列表数据,需要在 TodoList 组件中声明如下的 props 属性:

<script>
export default {
  name: "TodoListVue",
  props: {
    // 列表数据
    list: {
      type: Array,
      required: true,
      default: [],
    }
  }
};
</script>

2.在 App 组件中通过 list 属性,将数据传递到 TodoList 组件之中:

<template>
  <div>
    <h1>App 根组件</h1>

    <todo-list-vue :list="todolist"></todo-list-vue>
  </div>
</template>

4.3.4 渲染列表的 DOM 结构

1.通过 v-for 指令,循环渲染列表的 DOM 结构:

<template>
  <!-- 列表组 -->
  <ul class="list-group">
    <li class="list-group-item d-flex justify-content-between align-items-center" v-for="item in list" :key="item.id">
      <!-- 复选框 -->
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" :id="item.id" >
        <label class="custom-control-label" for="customCheck1">{{item.task}}</label>
      </div>
      <!-- 徽标 -->
      <span class="badge badge-success badge-pill">完成</span>
      <span class="badge badge-warning badge-pill">未完成</span>
    </li>
  </ul>
</template>

2.通过 v-if 和 v-else 指令,按需渲染 badge 效果:

      <!-- 徽标 -->
      <span class="badge badge-success badge-pill" v-if="item.done">完成</span>
      <span class="badge badge-warning badge-pill" v-else>未完成</span>

3.通过 v-model 指令,双向绑定任务的完成状态:

      <!-- 复选框 -->
      <div class="custom-control custom-checkbox">
        <input type="checkbox" class="custom-control-input" :id="item.id" v-model="item.done">
        <label class="custom-control-label" :for="item.id">{{item.task}}</label>
      </div>

4.通过 v-bind 属性绑定,动态切换元素的 class 类名:

<label class="custom-control-label" :class="item.done ? 'delete' : ''" :for="item.id">{{item.task}}</label>

在 TodoList 组件中声明如下的样式,美化当前组件的 UI 结构:

<style lang="less" scoped>
.list-group{
  width: 400px;
}

.delete{
  text-decoration: line-through;
  color: gray;
  font-style: italic;
}
</style>
4.4 封装 todo-input 组件

4.4.1 创建并注册 TodoInput 组件

1.在 src /components/todo-input/ 目录下新建 TodoInput 组件:

<template>
 <div>TodoInputVue 组件</div>
</template>

<script>
export default {
 name: 'TodoInputVue',
}
</script>

<style lang="less" scoped>
</style>

2.在 App.vue 组件中导入并注册 TodoInput.vue 组件:

<script>
// 导入 TodoList 组件
import TodoList from "./components/todo-list/TodoList.vue";
// 导入 TodoInput 组件
import TodoInput from "./components/todo-input/TodoInput.vue";
    
export default {
	name: "MyApp",
	// 注册私有组件
	components: {
 		TodoList,
 		TodoInput,
	},
};
</script>

3.在 App.vue 的 template 模板结构中使用注册的 TodoInput 组件:

<template>
<div>
 <h1>App 根组件</h1>

 <hr>


 <!-- 使用 TodoInput 组件 -->
 <todo-input></todo-input>
 <!-- 使用 TodoList 组件 -->
 <todo-list :list="todolist" class="mt-2"></todo-list>

</div>
</template>

4.4.2 基于bootstrap 渲染组件结构

1.根据 bootstrap 提供的 inline-forms(https://v4.bootcss.com/docs/components/forms/#inline-forms)渲染 TodoInput 组件的基本结构。

2.在 TodoInput 组件中渲染如下的 DOM 结构:

<template>
    <!-- form 表单 -->
    <form class="form-inline">
        <div class="input-group mb-2 mr-sm-2">
            <!-- 输入框前缀 -->
            <div class="input-group-prepend">
                <div class="input-group-text">任务</div>
            </div>
            <!-- 文本输入框 -->
            <input type="text" class="form-control" placeholder="请输入任务信息" style="width: 356px;">
        </div>
        <!-- 添加按钮 -->
        <button type="submit" class="btn btn-primary mb-2">添加新任务</button>‘
    </form>
</template>

4.4.3 通过自定义事件向外传递数据

需求描述:

在 App 组件中,监听 TodoInput 组件的自定义事件,获取到要添加的任务名称。示例代码如下:

<todo-input @add="onAddNewTask"></todo-input>

1.在 TodoInput 组件的 data 中声明如下的数据:

<script>
export default {
    name: 'TodoInput',
    data() {
        return {
            // 新任务的名称
            taskname: '',
        }
    },
}
</script>

2.为 input 输入框进行 v-model 的双向数据绑定

<input type="text" class="form-control" placeholder="请输入任务信息" style="width: 356px;" v-model.trim="taskname" />

3.监听 form 表单的 submit 事件,阻止默认提交行为并指定事件处理函数:

    <form class="form-inline" @submit.prevent="onFormSubmit">
	</form>

4.在 methods 中声明 onFormSubmit 事件处理函数:

    methods: {
        // 表单提交的事件处理函数
        onFormSubmit() {
            // 1.判断任务名称是否为空
            if (!this.taskname) return alert('任务名称不能为空');
            
            // 2.触发自定义的 add 事件,并向外界传递数据
            // 3.清空文本框
        },
    },

5.声明自定义事件如下:

export default {
    name: 'TodoInput',
    emits: ['add'],
}

6.进一步完善 onFormSubmit 事件处理函数:

    methods: {
        // 表单提交的事件处理函数
        onFormSubmit() {
            // 1.判断任务名称是否为空
            if (!this.taskname) return alert('任务名称不能为空');
            
            // 2.触发自定义的 add 事件,并向外界传递数据
			this.$emit('add', this.taskname)
            
            // 3.清空文本框
            this.taskname = ''
        },
    },

4.4.4 实现添加任务的功能

1.在 App.vue 组件中监听 TodoInput 组件自定义的 add 事件:

    <!-- 使用 TodoInput 组件 -->
	<!-- 监听 TodoInput 的 add 自定义事件 -->
    <todo-input @add="onAddNewTask"></todo-input>

2.在 App.vue 组件的 data 中声明 nextId 来模拟 id 自增 +1 的操作:

  data() {
    return {
      // 任务列表数据
      todolist: [
        { id: 1, task: "周一早晨9点开会", done: false },
        { id: 2, task: "周一晚上8点聚餐", done: false },
        { id: 3, task: "准备周三上午的演讲稿", done: true },
      ],
      // 下一个可用 Id 值
      nextId: 4,
    };
  },

3.在 App.vue 组件的 methods 中声明 onAddNewTask 事件处理函数:

  methods: {
    // TodoInput 组件 add 事件的处理函数
    onAddNewTask(taskname) {
      // 1.向任务列表中新增任务信息
      this.todolist.push({
        id: this.nextId,
        task: taskname,
        done: false, // 完成状态默认为 false
      })

      //2.让 nextId 自增+1
      this.nextId++
    }
  },
4.5 封装 todo-button 组件

4.5.1 创建并注册 TodoButton 组件

1.在 src/components/todo-button/ 目录下新建 TodoButton.vue 组件:

<template>
    <div>TodoButton 组件</div>
</template>


<script>
export default {
    name: 'TodoButton',
}
</script>

<style lang="less" scoped></style>

2.在 App.vue 组件中导入并注册 TodoButton.vue 组件:

<script>
// 导入 TodoList 组件
import TodoList from "./components/todo-list/TodoList.vue";
// 导入 TodoInput 组件
import TodoInput from "./components/todo-input/TodoInput.vue";
// 导入 TodoButton 组件
import TodoButton from "./components/todo-button/TodoButton.vue";
    
export default {
	name: "MyApp",
	// 注册私有组件
	components: {
 		TodoList,
 		TodoInput,
		TodoButton,
	},
};
</script>

3.在 App.vue 的 template 模板结构中使用注册的 TodoButton 组件:

<template>
<div>
 <h1>App 根组件</h1>

 <hr>


 <!-- 使用 TodoInput 组件 -->
 <todo-input></todo-input>
 <!-- 使用 TodoList 组件 -->
 <todo-list :list="todolist" class="mt-2"></todo-list>
 <!-- 使用 TodoButton 组件 -->
 <todo-button></todo-button>
    
</div>
</template>

4.5.2 基于 bootstrap 渲染组件结构

1.根据 bootstrap 提供的 Button group(https://v4.bootcss.com/docs/components/forms/button-group)渲染 TodoButton 组件的基本结构。

2.在 TodoButton 组件中渲染如下的 DOM 结构:

<template>
 <div class="button-container mt-3">
     <div class="btn-group">
         <button type="button" class="btn btn-primary">全部</button>
         <button type="button" class="btn btn-secondary">已完成</button>
         <button type="button" class="btn btn-secondary">未完成</button>
     </div>
 </div>
</template>

3.通过 button-container 类名美化组件的样式:

<style lang="less" scoped>
.button-container {
 // 添加固定宽度
 width: 400px;
 // 文本居中效果
 text-align: center;
}
</style>

4.5.3 通过 props 指定默认激活的按钮

1.在 TodoButton 组件中声明如下的 props ,用来指定默认激活的按钮的索引:

<script>
export default {
    name: 'TodoButton',
    props: {
        // 激活项的索引值
        active: {
            type: Number,
            required: true,
            // 默认激活索引值为 0 的按钮(全部:0,已完成:1,未完成:2)
            default: 0,
        },
    },
}
</script>

2.通过 动态绑定 class 类名 的方式控制按钮的激活状态:

<template>
    <div class="button-container mt-3">
        <div class="btn-group">
            <button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'">全部</button>
            <button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'">已完成</button>
            <button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'">未完成</button>
        </div>
    </div>
</template>

3.在 App 组件中声明默认激活项的索引,并通过属性绑定的方式传递给 TodoButton 组件:

    <!-- 使用 TodoButton 组件 -->
    <todo-button :active="activeBtnIndex"></todo-button>
<script>
export default {
  name: "MyApp",
  data() {
    return {
      // 任务列表数据
      todolist: [
        { id: 1, task: "周一早晨9点开会", done: false },
        { id: 2, task: "周一晚上8点聚餐", done: false },
        { id: 3, task: "准备周三上午的演讲稿", done: true },
      ],
      // 下一个可用 Id 值
      nextId: 4,
      // 激活的按钮的索引
      activeBtnIndex: 0,
    };
  },
}
</script>

4.5.4 通过 v-model 更新激活项的索引

需求分析:

父 -》 子 通过 props 传递了激活项的索引(active)

子 -》 父 需要更新父组件中激活项的索引

这种场景下适合在组件上使用 v-model 指令,维护组件内外数据的同步。

1.为 TodoButton 组件中的三个按钮分别绑定 click 事件处理函数:

<template>
    <div class="button-container mt-3">
        <div class="btn-group">
            <button type="button" class="btn" :class="active === 0 ? 'btn-primary' : 'btn-secondary'"
                @click="onBtnClick(0)">全部</button>
            <button type="button" class="btn" :class="active === 1 ? 'btn-primary' : 'btn-secondary'"
                @click="onBtnClick(1)">已完成</button>
            <button type="button" class="btn" :class="active === 2 ? 'btn-primary' : 'btn-secondary'"
                @click="onBtnClick(2)">未完成</button>
        </div>
    </div>
</template>

2.在 TodoButton 组件中声明如下的自定义事件,用来更新父组件通过 v-model 指令传递过来的 props 数据:

export default {
    name: 'TodoButton',
    // 声明和 v-model 相关的自定义事件
    emits: ['update:active'],
    props: {
        // 激活项的索引值
        active: {
            type: Number,
            required: true,
            // 默认激活索引值为 0 的按钮(全部:0,已完成:1,未完成:2)
            default: 0,
        },
    },
}

3.在 TodoButton 组件的 methods 节点中声明 onBtnClick 事件处理函数:

<script>
methods: {
    // 按钮的点击事件处理函数
    onBtnClick(index) {
        // 如果当前点击的按钮的索引值,等于 props 传递过来的索引值,则没必要触发 update:active 自定义事件
        if (index === this.active) return
        // 通过 this.$emit() 方法触发自定义事件
        this.$emit('update:active', index)
    },
},
</script>

<!-- 使用 TodoButton 组件,使用 v-model 指令双向绑定 -->
<todo-button v-model:active="activeBtnIndex"></todo-button>

4.5.5 通过计算属性动态切换列表的数据

需求分析:

点击不同的按钮,切换显示不同的列表数据。此时可以根据当前激活按钮得到索引,动态计算出要显示的列表数据并返回即可!

1.在 App 根组件中声明如下的计算属性:

computed: {
	// 根据激活按钮的索引值,动态计算要展示的列表数据
	tasklist() {
		// 对“源数据”进行 switch...case 的匹配,并返回“计算之后的结果”
		switch (this.activeBtnIndex) {
			case 0: // 全部
				return this.todolist
			case 1: // 已完成
				return this.todolist.filter(x => x.done)
			case 2: // 未完成
				return this.todolist.filter(x => !x.done)
		}
	},
},

2.在 App 根组件的 DOM 结构中,将 TodoList 组件的 :list=“todolist” 修改为:

    <!-- 使用 TodoList 组件 -->
    <todo-list :list="tasklist" class="mt-2"></todo-list>

十八、vue组件基础(下)总结

① 能够知道如何对 props 进行验证

  • 数组格式、对象格式
  • type、default、required、validator

② 能够知道如何使用计算属性

  • computed 节点、必须 return 一个结果、缓存计算结果

③ 能够知道如何为组件绑定自定义事件

  • v-on 绑定自定义事件、emits、$emit()

④ 能够知道如何在组件上使用 v-model

  • 应用场景:实现组件内外的数据同步
  • v-model:props名称、emits、$emit(‘update:props名称’)

Ⅳ vue组件高级(上)

目标目录
监视数据的变化,从而做出对应的操作十九、watch 侦听器
在最合适的周期,做最对的事二十、组件的生命周期
组件之间如何进行数据共享,没有详细的 vuex二十一、组件之间的数据共享
vue 3.x 中全局配置 axios二十二、vue 3.x 中全局配置 axios
对以上内容的复习巩固二十三、第四个案例——购物车案例
总结与概括二十四、vue组件高级(上)总结

十九、watch 侦听器

1.什么是 watch 侦听器

watch 侦听器允许开发者监视数据的变化,从而针对数据的变化做特定的操作。例如,监视用户名的变化并发起请求,判断用户名是否可用。

2.watch 侦听器的基本语法

开发者需要在 watch 节点下,定义自己的侦听器:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tCAIwBWM-1685004795667)(./assets/Snipaste_2023-05-09_15-26-59.png)]

3.使用 watch 检测用户名是否可用

监听 username 值的变化,并使用 axios 发起 Ajax 请求,检测当前输入的用户名是否可用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TGXudntd-1685004795667)(./assets/Snipaste_2023-05-09_15-27-09.png)]

4.immediate 选项

默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,则需要使用 immediate 选项:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YiiZ50SB-1685004795668)(./assets/Snipaste_2023-05-09_15-27-37.png)]

5.deep 选项

watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zkSVlm9q-1685004795668)(./assets/Snipaste_2023-05-09_15-27-47.png)]

6.监听对象单个属性的变化

如果只想监听对象中单个属性的变化,则可以按照如下的方式定义 watch 侦听器:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I4Fv37ar-1685004795668)(./assets/Snipaste_2023-05-09_15-27-56.png)]

7.计算属性 vs 侦听器

计算属性和侦听器侧重的应用场景不同

计算属性侧重于监听多个值的变化,最终计算并返回一个新值

侦听器侧重于监听单个数据的变化,最终执行特定的业务处理,不需要有任何返回值

二十、组件的生命周期

1.组件的运行过程

组件的生命周期指的是:组件从创建 -> 运行(渲染) -> 销毁的整个过程,强调的是一个时间段

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QdLuviry-1685004795669)(./assets/Snipaste_2023-05-09_21-30-12.png)]

2.如何监听组件的不同时刻

vue 框架为组件内置了不同时刻的生命周期函数,生命周期函数会伴随着组件的运行而自动调用。例如:

① 当组件在内存中被创建完毕之后,会自动调用 created 函数

② 当组件被成功的渲染到页面上之后,会自动调用 mounted 函数

③ 当组件被销毁完毕之后,会自动调用 unmounted 函数

3.如何监听组件的更新

当组件的 data 数据更新之后,vue 会自动重新渲染组件的 DOM 结构,从而保证 View 视图展示的数据和 Model 数据源 保持一致。

当组件被重新渲染完毕之后,会自动调用 updated 生命周期函数。

4.组件中主要的生命周期函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HTv6jbPt-1685004795669)(./assets/Snipaste_2023-05-09_21-30-32.png)]

注意:在实际开发中,created最常用的生命周期函数!

5.组件中全部的生命周期函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mUKSw271-1685004795669)(./assets/Snipaste_2023-05-09_21-30-44.png)]

6.完整的生命周期图示

可以参考 vue 官方文档给出的“生命周期图示”,进一步理解组件生命周期执行的过程:

https://cn.vuejs.org/guide/essentials/lifecycle.html#lifecycle-diagram

二十一、组件之间的数据共享

1.组件之间的关系

在项目开发中,组件之间的关系分为如下 3 种:

① 父子关系

② 兄弟关系

③ 后代关系

2.父子组件之间的数据共享

父子组件之间的数据共享又分为:

① 父 -> 子共享数据

② 子 -> 父共享数据

③ 父 <-> 子双向数据同步

2.1 父组件向子组件共享数据

父组件通过 v-bind 属性绑定向子组件共享数据。同时,子组件需要使用 props 接收数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Amd0khqu-1685004795669)(./assets/Snipaste_2023-05-10_16-17-29.png)]

2.2 子组件向父组件共享数据

子组件通过自定义事件的方式向父组件共享数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eUKE0LpY-1685004795669)(./assets/Snipaste_2023-05-10_16-17-40.png)]

2.3 父子组件之间数据的双向同步

父组件在使用子组件期间,可以使用 v-model 指令维护组件内外数据的双向同步:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PxlQndOW-1685004795670)(./assets/Snipaste_2023-05-10_16-29-44.png)]

3.兄弟组件之间的数据共享

兄弟组件之间实现数据共享的方案是 EventBus。可以借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kUiFkjPh-1685004795670)(./assets/Snipaste_2023-05-10_16-38-43.png)]

3.1 安装 mitt 依赖包
npm i mitt
3.2 创建公共的 EventBus 模块

在项目中创建公共的 eventBus 模块:

// 创建 eventBus.js 文件

// 导入 mitt 包
import mitt from 'mitt'

// 创建 EventBus 的实例对象
const bus = mitt()

// 将 EventBus 的实例对象共享出去
export default bus
3.3 在数据接收方自定义事件

在数据接收方,调用 bus.on(‘事件名称’, 事件处理函数) 方法注册一个自定义事件

<script>
// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'

export default {
  name: 'MyRight',
  data() {
    return {
      num: 0,
    }
  },
  created() {
    // 调用 bus.on() 方法注册一个自定义事件,通过事件处理函数的形参接收数据
    bus.on('countChange', count => {
      this.num = count
    })
  },
}
</script>
3.4 在数据接发送方触发事件

在数据发送方,调用 bus.emit(‘事件名称’, 要发送的数据) 方法触发自定义事件

<script>
// 导入 eventBus.js 模块,得到共享的 bus 对象
import bus from './eventBus.js'

export default {
  name: 'MyLeft',
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    add() {
      this.count++
      // 调用 bus.emit() 方法触发自定义事件,并发送数据
      bus.emit('countChange', this.count)
    },
  },
}
</script>

4.后代关系组件之间的数据共享

后代关系组件之间共享数据,指的是父节点的组件向其子孙组件共享数据。此时组件之间的嵌套关系比较复杂,可以使用 provideinject 实现后代关系组件之间的数据共享。

4.1 父节点通过 provide 共享数据

父节点的组件可以通过 provide 方法,对其子孙组件共享数据:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gd4gVaKR-1685004795670)(./assets/Snipaste_2023-05-10_17-45-00.png)]

4.2 子孙节点通过 inject 接收数据

子孙节点可以使用 inject 数组,接收父级节点向下共享的数据。示例代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PhycTIWx-1685004795670)(./assets/Snipaste_2023-05-10_17-45-08.png)]

4.3 父节点对外共享响应式数据

父节点使用 provide 向下共享数据时,可以结合 computed 函数向下共享响应式的数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dYErWZHa-1685004795671)(./assets/Snipaste_2023-05-10_17-45-16.png)]

4.4 子孙节点使用响应式数据

如果父级节点共享的是响应式的数据,则子孙节点必须以 .value 的形式进行使用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E3qlE2Rv-1685004795671)(./assets/Snipaste_2023-05-10_17-45-24.png)]

5.vuex

vuex 是终极的组件之间的数据共享方案。在企业级的 vue 项目开发中,vuex 可以让组件之间的数据共享变得高效清晰、且易于维护

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sV50wh7B-1685004795671)(./assets/Snipaste_2023-05-10_17-45-33.png)]

6.总结

父子关系

① 父 -> 子 属性绑定

② 子 -> 父 事件绑定

③ 父 <-> 子 组件上的 v-model

兄弟关系

④ EventBus

后代关系

⑤ provide & inject

全局数据共享

⑥ vuex

二十二、vue 3.x 中全局配置 axios

1.为什么要全局配置 axios

在实际项目开发中,几乎每个组件中都会用到 axios 发起数据请求。此时会遇到如下两个问题:

① 每个组件中都需要导入 axios(代码臃肿)

② 每次发请求都需要填写完整的请求路径(不利于后期的维护)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9w0Suzrg-1685004795671)(./assets/Snipaste_2023-05-10_19-02-45.png)]

2.如何全局配置 axios

在 main.js 入口文件中,通过 app.config.globalProperties 全局挂载 axios:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eaq7eMT7-1685004795671)(./assets/Snipaste_2023-05-10_19-02-59.png)]

3.使用 axios

// npm 安装 axios 包
npm i axios -S
// -S 表示项目运行期间仍需使用的包
// -D 表示项目开发期间需要使用的包


// main.js 配置文件中
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'

// 导入 axios
import axios from 'axios'

const app = createApp(App)

// 配置请求的根路径
axios.defaults.baseURL = 'http://www.esbook.cn'
// 将 axios 挂载为全局的 $http 自定义属性
app.config.globalProperties.$http = axios

app.mount('#app')


// 使用 axios 发起 Ajax 数据请求
methods: {
 // 请求商品列表的数据
 async getGoodsList() {
     // 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
     const { data: res } = await this.$http.get('/api/cart')
     // 2.判断请求是否成功
     if (res.status !== 200) return alert('请求商品列表数据失败!')
     // 3.将请求到的数据存储到 data 中,供页面渲染期间使用
     this.goodslist = res.list
 }
},
    
// 使用 axios 发起 get 请求传参
    async getBrandList() {
        // 在 vue 的 选项式api 中,通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
        // 在 vue 的 组合式api 中,vue3.0中是没有this的。使用getCurrentInstance来获取上下文
        // const { proxy } = getCurrentInstance() 这里的proxy相当于this
        // 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
     	const { data: res } = await this.$http.get('/selectById',{
            params:{
                id: 4,
            }
        })
     	// 2.判断请求是否成功
     	if (res.status !== 200) return alert('请求商品列表数据失败!')
    	 // 3.将请求到的数据存储到 data 中,供页面渲染期间使用
     	this.goodslist = res.list
    }

二十三、第四个案例——购物车案例

1.案例效果

2.实现步骤

① 初始化项目基本结构

② 封装 EsHeader 组件

③ 基于 axios 请求商品列表数据( GET 请求,地址为 https://www.escook.cn/api/cart )

④ 封装 EsFooter 组件

⑤ 封装 EsGoods 组件

⑥ 封装 EsCounter 组件

3.具体实现

3.1 初始化项目结构

1.初始化 vite 项目:

npm init vite-app code-cart
cd code-cart
npm i

2.清理项目结构:

  • 把 bootstrap 相关文件放入 src/assets 目录下
  • 在 main.js 中导入 bootstrap.css
  • 清空 App.vue 组件
  • 删除 components 目录下的 HelloWorld.vue 组件

3.为组件的样式启用 less 语法

npm i less -D

4.初始化 index.css 全局样式如下:

:root{
	font-size: 12px;
}
3.2 封装 es-header 组件
3.2.1 创建并注册 EsHeader 组件

1.在 src/components/es-header/ 目录下新建 EsHeader.vue 组件:

<template>
    <div>EsHeader 组件</div>
</template>

<script>
export default {
    name: 'EsHeader',
}
</script>

<style lang="less" scoped></style>

2.在 App.vue 组件中导入、注册并在模板结构中使用 EsHeader.vue 组件:

<template>
    <h1>App 根组件</h1>
    <hr>
    <!-- 使用 EsHeader 组件 -->
    <es-header></es-header>
</template>

<script>
import EsHeader from './components/es-header/EsHeader.vue';

export default {
    name: 'MyApp',
    components: {
        // 注册 header 组件
        EsHeader,
    }
}
</script>
3.2.2 封装 es-header 组件

封装需求

  • 允许用户自定义 title 标题内容
  • 允许用户自定义 color 文字颜色
  • 允许用户自定义 bgcolor 背景颜色
  • 允许用户自定义 fsize 字体大小
  • es-header 组件必须固定定位到页面的顶部位置,高度为 45px,文本居中,z-index 为 999

1.在 es-header 组件中封装以下的 props 属性:

<script>
export default {
    name: 'EsHeader',
    props: {
        title: {
            // 标题内容
            type: String,
            default: 'es-header',
        },
        bgcolor: {
            // 背景颜色
            type: String,
            default: '#007bff',
        },
        color: {
            // 文字颜色
            type: String,
            default: '#ffffff',
        },
        fsize: {
            // 文字大小
            type: Number,
            default: 12,
        },
    },
}
</script>

2.渲染标题,并动态为 DOM 元素绑定行内的 style 样式对象:

<template>
    <div class="header-container" :style="{ backgroundColor: bgcolor, color: color, fontSize: fsize + 'px' }">
        {{ title }}
    </div>
</template>

<style lang="less" scoped>
.header-container {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 45px;
    text-align: center;
    line-height: 45px;
    z-index: 999;
}
</style>

3.调整 App.vue 根组件的样式,并传入 title 属性:

<template>
    <div class="app-container">
        <h1>App 根组件</h1>
        <hr>
        <!-- 使用 EsHeader 组件 -->
        <es-header title="购物车案例"></es-header>
    </div>
</template>

<style lang="less">
.app-container {
    padding-top: 45px;
}
</style>
3.3 基于 axios 请求商品列表数据
3.3.1 全局配置 axios

1.运行如下的命令安装 axios:

npm i axios -S

2.在 main.js 入口文件中导入并全局配置 axios:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'

// 导入 axios
import axios from 'axios'

const app = createApp(App)

// 配置请求的根路径
axios.defaults.baseURL = 'https://applet-base-api-t.itheima.net'
// 将 axios 挂载为全局的 $http 自定义属性
app.config.globalProperties.$http = axios

app.mount('#app')
3.3.2 请求商品列表数据

1.在 App.vue 根组件中声明如下的 data 数据:

data() {
    return {
        // 商品列表的数据
        goodslist: [],
    }
},

2.在 App.vue 根组件的 created 生命周期函数中,预调用获取商品列表数据的 methods 方法:

// 组件实例创建完毕之后的生命周期函数
created() {
	// 调用 methods 中的 getGoodsList 方法,请求商品列表的数据
	this.getGoodsList()
},

3.在 App.vue 根组件的 methods 节点中,声明刚才预调用的 getGoodsList 方法:

methods: {
    // 请求商品列表的数据
    async getGoodsList() {
        // 1.通过组件实例 this 访问到全局挂载的 $http 属性,并发起 Ajax 数据请求
        const { data: res } = await this.$http.get('/api/cart')
        // 2.判断请求是否成功
        if (res.status !== 200) return alert('请求商品列表数据失败!')
        // 3.将请求到的数据存储到 data 中,供页面渲染期间使用
        this.goodslist = res.list
    }
},
3.4 封装 es-footer 组件
3.4.1 创建并注册 EsFooter 组件

1.在 src/component/es-footer/ 目录下新建 EsFooter.vue 组件:

<template>
    <div>
        EsFooter 组件
    </div>
</template>
 
<script>
export default {
    name: 'EsFooter',
}
</script>

<style lang="less" scoped></style>

2.在 App.vue 组件中导入并注册 EsFooter.vue 组件:

<template>
 <h1>App 根组件</h1>
 <hr>
 <!-- 使用 EsHeader 组件 -->
 <es-header></es-header>
 <!-- 使用 EsFooter 组件 -->
 <es-footer></es-footer>
</template>

<script>
import EsHeader from './components/es-header/EsHeader.vue';
import EsFooter from './components/es-footer/EsFooter.vue';
    
export default {
 name: 'MyApp',
 components: {
     // 注册 header 组件
     EsHeader,
     // 注册 footer 组件
     EsFooter,
 }
}
</script>
3.4.2 封装 es-footer 组件

封装需求:

1.es-footer 组件必须固定定位到页面底部的位置,高度为 50px,内容两端贴边对齐,z-index 为 999

2.允许用户自定义 amount 总价格(单位是元),并在渲染时保留两位小数

3.允许用户自定义 total 总数量,并渲染到结算按钮中;如果要结算的商品数量为0,则禁用结算按钮

4.允许用户自定义 isfull 全选按钮的选中状态

5.允许用户通过自定义事件的形式,监听全选按钮选中状态的变化,并获取到最新的选中状态


1.将 EsFooter.vue 组件在页面底部进行固定定位,基于 bootstrap 渲染左侧全选按钮:

<template>
    <div class="footer-container">
        <div class="custom-control custom-checkbox">
            <input type="checkbox" class="custom-control-input" id="fullCheck">
            <label class="custom-control-label" for="fullCheck">全选</label>
        </div>
    </div>
</template>
 
<script>
export default {
    name: 'EsFooter',
}
</script>

<style lang="less" scoped>
.footer-container {
    height: 50px;
    width: 100%;
    background-color: white;
    border-top: 1px solid #efefef;
    position: fixed;
    bottom: 0;
    left: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 10px;
}

:root {
    font-size: 12px;
}
</style>

2.全局样式表更改属性:

.custom-control .custom-control-label::before {
    border-radius: 1.25em;
}

3.渲染合计区域和结算按钮:

<template>
    <div class="footer-container">
        <!-- 全选框 -->
        <div class="custom-control custom-checkbox">
            <input type="checkbox" class="custom-control-input" id="fullCheck">
            <label class="custom-control-label" for="fullCheck">全选</label>
        </div>

        <!-- 合计部分 -->
        <div>
            <span>合计:</span>
            <span class="amount">¥0.00</span>
        </div>

        <!-- 结算按钮 -->
        <button type="button" class="btn btn-primary btn-settle">结算(0)</button>
    </div>
</template>
 
<script>
export default {
    name: 'EsFooter',
}
</script>

<style lang="less" scoped>
.footer-container {
    height: 50px;
    width: 100%;
    background-color: white;
    border-top: 1px solid #efefef;
    position: fixed;
    bottom: 0;
    left: 0;
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 10px;
}

:root {
    font-size: 12px;
}

.custom-control .custom-control-label::before {
    border-radius: 1.25em;
}

.amount {
    font-weight: bold;
    color: red;
}

.btn-settle {
    min-width: 90px;
    height: 38px;
    border-radius: 19px;
}
</style>
3.4.3 封装自定义属性 amount

amount 是已勾选商品的总价格

1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:

<script>
export default {
    name: 'EsFooter',
    props: {
        // 已勾选商品的总价格
        amount: {
            type: Number,
            default: 0,
        },
    },
}
</script>

2.在 EsFooter.vue 组件的 DOM 结构中渲染 amount 的值:

<!-- 合计部分 -->
<div>
    <span>合计:</span>
    <!-- 将 amount 的值保留为两位小数 -->
    <span class="amount">¥{{ amount.toFixed(2) }}</span>
</div>
3.4.4 封装自定义属性 total

total是已勾选商品的总数量

1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:

<script>
export default {
    name: 'EsFooter',
    props: {
        // 已勾选商品的总价格
        amount: {
            type: Number,
            default: 0,
        },
        // 已勾选商品的总数量
        total: {
            type: Number,
            default: 0,
        }
    },
}
</script>

2.结算按钮动态控制

<!-- disabled 的值为 true,禁用按钮 -->
<button type="button" class="btn btn-primary btn-settle" :disabled="total === 0">结算({{ total }})</button>
3.4.5 封装自定义属性 isfull

isfull 是全选按钮的选中状态,true 表示选中,false 表示未选中

1.在 EsFooter.vue 组件的 props 节点中,声明如下自定义属性:

<script>
export default {
    name: 'EsFooter',
    props: {
        // 已勾选商品的总价格
        amount: {
            type: Number,
            default: 0,
        },
        // 已勾选商品的总数量
        total: {
            type: Number,
            default: 0,
        },
        // 全选按钮的选中状态
        isFull: {
            type: Boolean,
            default: false,
        }
    },
}
</script>

2.动态绑定选中状态

<!-- 全选框 -->
<div class="custom-control custom-checkbox">
	<input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull">
	<label class="custom-control-label" for="fullCheck">全选</label>
</div>
3.4.6 封装自定义事件 fullChange

通过自定义事件 fullChange,把最新的选中状态传递给组件的使用者

1.监听复选框选中状态变化的 change 事件:

<!-- 全选框 -->
<div class="custom-control custom-checkbox">
	<input type="checkbox" class="custom-control-input" id="fullCheck" :checked="isfull" @change="onCheckBoxChange">
	<label class="custom-control-label" for="fullCheck">全选</label>
</div>

2.在 methods 中声明 onCheckBoxChange,并通过事件对象 e 获取到最新的选中状态:

    methods: {
        // 监听复选框选中状态的变化
        onCheckBoxChange(e) {
            // e.target.checked 是复选框最新的选中状态
            this.$emit('fullChange', e.target.checked)
        }
    },

3.在 emits 中声明自定义事件:

    // 声明自定义事件
    emits: ['fullChange'],

4.在 App.vue 根组件中测试 EsFooter.vue 组件:

       <!-- 使用 EsFooter 组件 -->
        <es-footer :total="0" :amount="0" @fullChange="onFullStateChange"></es-footer>

5.在methods 中声明 onFullStateChange 处理函数,通过形参获取到全选按钮最新的选中状态:

    methods: {
        // 监听选中状态变化的事件
        onFullStateChange(isfull) {
            console.log(isfull)
        }
    },
3.5 封装 es-goods 组件
3.5.1 创建并注册 EsGoods 组件
<template>
    <div>
        EsGoods 组件
    </div>
</template>

<script>
export default {
    name: 'EsGoods',
}   
</script>

<style lang="less" scoped></style>
3.5.2 封装 es-goods 组件

封装需求

1.实现 EsGoods 组件的基础布局

2.封装组件的 6 个自定义属性 (id, thumb, title, price, count, checked)

3.封装组件的自定义事件 stateChange,允许外界监听组件选中状态的变化

<!-- 使用 goods 组件 -->
<es-goods
 v-for="item in goodslist"
 :key="item.id"
 :id="item.id"
 :thumb="item.goods_img"
 :title="item.goods_name"
 :price="item.goods_price"
 :count="item.goods_count"
 :checked="item.goods_state"
 @stateChange="onGoodsStateChange"
></es-goods>

1.渲染组件的基础布局

1.1 渲染 EsGoods 组件的基础 DOM 结构:

<template>
    <div class="goods-container">
        <!-- 左侧图片区域 -->
        <div class="left">
            <!-- 商品的缩略图 -->
            <img src="" alt="商品图片" class="thumb" />
        </div>
        <!-- 右侧信息区域 -->
        <div class="right">
            <!-- 商品名称 -->
            <div class="top">xxxx</div>
            <div class="bottom">
                <!-- 商品价格 -->
                <div class="price">¥0.00</div>
                <!-- 商品数量 -->
                <div class="count">数量</div>
            </div>
        </div>
    </div>
</template>

1.2 美化布局样式:

.goods-container {
    display: flex;
    padding: 10px;

    // 左侧图片的样式
    .left {
        margin-right: 10px;

        // 商品图片
        .thumb {
            display: block;
            width: 100px;
            height: 100px;
            background-color: #efefef;
        }
    }

    // 右侧商品名称、单价、数量的样式
    .right {
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        flex: 1;

        .top {
            font-weight: bold;
        }

        .bottom {
            display: flex;
            justify-content: space-between;
            align-items: center;

            .price {
                color: red;
                font-weight: bold;
            }
        }
    }
}

1.3 在商品缩略图之外包裹复选框( https://v4.bootcss.com/docs/components/forms/#checkboxes )效果:

<!-- 左侧图片和复选框区域 -->
<div class="left">
	<!-- 复选框 -->
	<div class="custom-control custom-checkbox">
		<input type="checkbox" class="custom-control-input" id="customCheck1" />
		<!-- 将商品图片包裹于 label 之中,点击图片可以切换“复选框”的选中状态 -->
		<label class="custom-control-label" for="customCheck1">
			<img src="" alt="商品图片" class="thumb" />
		</label>
	</div>
</div>

1.4 覆盖复选框的默认样式:

.custom-control-label::before,
.custom-control-label::after {
	top: 3.4rem;
}

.custom-control .custom-control-label::before {
    border-radius: 1.25em;
}

1.5 在 App.vue 组件中循环渲染 EsGoods.vue 组件:

<!-- 使用 EsGoods 组件 -->
<es-goods v-for="item in goodslist"></es-goods>

1.6 为 EsGoods.vue 添加顶边框:

.goods-container {
	display: flex;
	padding: 10px;
	// 最终生成的选择器为 .goods-container + .goods-container
	// 在 css 中,(+)是“相邻兄弟选择器”,表示:选择紧连着另一元素后的元素,二者具有相同的父元素。
	+ .goods-container {
		border-top: 1px solid #efefef;
	}
	// ...省略其他样式
}

2.封装自定义属性 id

2.1 在 EsGoods.vue 组件的 props 节点中,声明如下的自定义属性:

export default {
    name: 'EsGoods',
    props: {
        // 唯一的 key 值
        id: {
            // id 的值可以是“字符串”也可以是“数值”
            type: [String, Number],
            required: true,
        },
    },
}  

2.2 在渲染复选框时动态绑定 input 的 id 属性和 label 的 for 属性值:

<!-- 复选框 -->
<div class="custom-control custom-checkbox">
	<input type="checkbox" class="custom-control-input" :id="id" />
	<label class="custom-control-label" :for="id">
		<img src="" alt="商品图片" class="thumb" />
	</label>
</div>

2.3 在 App.vue 中使用 EsGoods.vue 组件时,动态绑定 id 属性的值:

<!-- 使用 goods 组件 -->
<es-goods v-for="item in goodslist" :id="item.goods_id"></es-goods>

3.封装其他属性

除了 id 属性之外,EsGoods 组件还需要封装:缩略图(thumb)、商品名称(title)、单价(price)、数量(count)、勾选状态(checked)这 5 个属性

3.1 在 EsGoods.vue 组件的 props 节点中,声明如下的自定义属性:

export default {
    name: 'EsGoods',
    props: {
        // 唯一的 key 值
        id: {
            type: [String, Number],
            required: true,
        },
        // 1. 商品的缩略图
        thumb: {
            type: String,
            required: true,
        },
        // 2. 商品的名称
        title: {
            type: String,
            required: true,
        },
        // 3. 单价
        price: {
            type: Number,
            required: true,
        },
        // 4. 数量
        count: {
            type: Number,
            required: true,
        },
        // 5. 商品的勾选状态
        checked: {
            type: Boolean,
            required: true,
        },
    },
}

3.2 在 EsGoods.vue 组件的 DOM 结构中渲染商品的信息数据:

<template>
    <div class="goods-container">
        <!-- 左侧图片和复选框区域 -->
        <div class="left">
            <!-- 复选框 -->
            <div class="custom-control custom-checkbox">
                <input type="checkbox" class="custom-control-input" id="id" :checked="checked" />
                <!-- 将商品图片包裹于 label 之中,点击图片可以切换“复选框”的选中状态 -->
                <label class="custom-control-label" for="id">
                    <img :src="thumb" alt="商品图片" class="thumb" />
                </label>
            </div>
        </div>
        <!-- 右侧信息区域 -->
        <div class="right">
            <!-- 商品名称 -->
            <div class="top">{{ title }}</div>
            <div class="bottom">
                <!-- 商品价格 -->
                <div class="price">¥{{ price.toFixed(2) }}</div>
                <!-- 商品数量 -->
                <div class="count">{{ count }}</div>
            </div>
        </div>
    </div>
</template>

3.3 在 App.vue 组件中使用 EsGoods.vue 组件时,动态绑定对应属性的值:

<!-- 使用 EsGoods 组件 -->
<es-goods v-for="item in goodslist" :key="item.goods_id" :id="item.goods_id" :thumb="item.goods_img" :title="item.goods_name" :price="item.goods_price" :count="item.goods_count" :checked="item.goods_state"></es-goods>

4.封装自定义事件 stateChange

点击复选框时,可以把最新的勾选状态,通过自定义事件的方式传递给组件的使用者。

4.1 在 EsGoods.vue 组件中,监听 checkbox 选中状态变化的事件:

<!-- 监听复选框的 change 事件 -->
<input type="checkbox" class="custom-control-input" :id="id" :checked="checked" @change="onCheckBoxChange" />

4.2 在 EsGoods.vue 组件的 methods 中声明对应的事件处理函数:

methods: {
	// 监听复选框选中状态变化的事件
	onCheckBoxChange(e) {
		// e.target.checked 是最新的勾选状态
		console.log(e.target.checked)
	},
},

4.3 在 EsGoods.vue 组件中声明自定义事件:

emits: ['stateChange'],

4.4 完善 onCheckBoxChange 函数的处理逻辑,调用 $emit() 函数触发自定义事件:

methods: {
	// 监听复选框选中状态变化的事件
	onCheckBoxChange(e) {
		// 向外发送的数据是一个对象,包含了 { id, value } 两个属性
		this.$emit('stateChange', {
			id: this.id,
			value: e.target.checked,
		})
	},
},

4.5 在 App.vue 根组件中使用 EsGoods.vue 组件时,监听它的 stateChange 事件:

<!-- 使用 goods 组件 -->
<es-goods
    v-for="item in goodslist"
    :key="item.id"
    :id="item.id"
    :thumb="item.goods_img"
    :title="item.goods_name"
    :price="item.goods_price"
    :count="item.goods_count"
    :checked="item.goods_state"
    @stateChange="onGoodsStateChange"
></es-goods>

4.6 在 App.vue 的 methods 中声明如下的事件处理函数:

methods: {
    // 监听商品选中状态变化的事件
    onGoodsStateChange(e) {
        // 1. 根据 id 进行查找(注意:e 是一个对象,包含了 id 和 value 两个属性)
        const findResult = this.goodslist.find(x => x.id === e.id)
        // 2. 找到了对应的商品,则更新其选中状态
        if (findResult) {
        	findResult.goods_state = e.value
        }
    },
}
3.6 实现合计、结算数量、全选功能
3.6.1 动态统计已勾选商品的总价格

需求分析

合计的商品总价格,依赖于 goodslist 数组中每一件商品信息的变化,此场景下适合使用计算属性

1.在 App.vue 中声明如下的计算属性:

computed: {
    // 已勾选商品的总价
        amount() {
        // 1. 定义商品总价格
        let a = 0
        // 2. 循环累加商品总价格
        this.goodslist
            .filter(x => x.goods_state)
            .forEach(x => a += x.goods_price * x.goods_count)
        // 3. 返回累加的结果
        return a
    },
},

2.在 App.vue 中使用 EsFooter.vue 组件时,动态绑定已勾选商品的总价格

<!-- 使用 footer 组件 -->
<es-footer :total="0" :amount="amount" @fullChange="onFullStateChange"></es-footer>
3.6.2 动态统计已勾选商品的总数量

1.在 App.vue 中声明如下的计算属性:

computed: {
    // 已勾选商品的总数量
    total() {
        // 1. 定义已勾选的商品总数量
        let t = 0
        // 2. 循环累加
        this.goodslist
            .filter(x => x.goods_state)
            .forEach(x => (t += x.goods_count))
        // 3. 返回计算的结果
        return t
    },
},

2.在 App.vue 中使用 EsFooter.vue 组件时,动态绑定已勾选商品的总数量

<!-- 使用 footer 组件 -->
<es-footer :total="total" :amount="amount"
@fullChange="onFullStateChange"></es-footer>
3.6.3 实现全选功能

1.在 App.vue 组件中监听到 EsFooter.vue 组件的选中状态发生变化时,立即更新 goodslist 中每件商品的选中状态即可:

<!-- 使用 footer 组件 -->
<es-footer :total="total" :amount="amount"
@fullChange="onFullStateChange"></es-footer>

2.在 onFullStateChange 的事件处理函数中修改每件商品的选中状态:

methods: {
    // 监听全选按钮状态的变化
    onFullStateChange(isFull) {
    	this.goodslist.forEach(x => x.goods_state = isFull)
    },
}
3.7 封装 es-counter 组件
3.7.1 创建并注册 EsCounter 组件

1.在 src/components/es-counter/ 目录下新建 EsCounter.vue 组件:

<template>
	<div>EsCounter 组件</div>
</template>

<script>
export default {
	name: 'EsCounter',
}
</script>

<style lang="less" scoped>
</style>

2.在 EsGoods.vue 组件中导入并注册 EsCounter.vue 组件:

// 导入 counter 组件
import EsCounter from '../es-counter/EsCounter.vue'

export default {
    name: 'EsGoods',
    components: {
    	// 注册 counter 组件
    	EsCounter,
    }
}

3.在 EsGoods.vue 的 template 模板结构中使用 EsCounter.vue 组件:

<div class="bottom">
    <!-- 商品价格 -->
    <div class="price">¥{{ price.toFixed(2) }}</div>
    <!-- 商品数量 -->
    <div class="count">
        <!-- 使用 es-counter 组件 -->
        <es-counter></es-counter>
    </div>
</div>
3.7.2 封装 EsCounter 组件

封装要求

  1. 渲染组件的 基础布局

  2. 实现数量值的 加减操作

  3. 处理 min 最小值

  4. 使用 watch 侦听器处理文本框输入的结果

  5. 封装 numChange 自定义事件

<es-counter :num="count" :min="1" @numChange="getNumber"></es-counter>

2.1 渲染组件基础布局

1.基于 bootstrap 提供的 Buttons https://v4.bootcss.com/docs/components/buttons/#examples 和 form-control 渲染组件的基础布局:

<template>
    <div class="counter-container">
        <!-- 数量 -1 按钮 -->
        <button type="button" class="btn btn-light btn-sm">-</button>
        <!-- 输入框 -->
        <input type="number" class="form-control form-control-sm ipt-num" />
        <!-- 数量 +1 按钮 -->
        <button type="button" class="btn btn-light btn-sm">+</button>
    </div>
</template>

2.美化当前组件的样式:

.counter-container {
    display: flex;

    // 按钮的样式
    .btn {
        width: 25px;
    }

    // 输入框的样式
    .ipt-num {
        width: 34px;
        text-align: center;
        margin: 0 4px;
    }
}

2.2 实现数值的渲染及加减操作

思路分析:

  1. 加减操作需要依赖于 EsCounter 组件的 data 数据

  2. 初始数据依赖于父组件通过 props 传递进来

将父组件传递进来的 props 初始值转存到 data 中,形成 EsCounter 组件的内部状态!

1.在 EsCounter.vue 组件中声明如下的 props:

props: {
    // 数量值
    num: {
        type: Number,
        default: 0,
    },
},

2.在 EsGoods.vue 组件中通过属性绑定的形式,将数据传递到 EsCounter.vue 组件中:

<!-- 商品数量 -->
<div class="count">
     <es-counter :num="count"></es-counter>
</div>

注意:不要直接把 num 通过 v-model 指令双向绑定到 input 输入框,因为 vue 规定:props **的值只读的!**例如下面的做法是错误的:

<!-- Warning 警告:不要模仿下面的操作 -->
<input type="number" class="form-control form-control-sm ipt-num" v-model.number="num" />

3.正确的做法:将 props 的初始值转存到 data 中,因为 data **中的数据是可读可写的!**示例代码如下:

export default {
    name: 'EsCounter',
    props: {
        // 初始数量值【只读数据】
        num: {
            type: Number,
            default: 0,
        },
    },
    data() {
        return {
            // 内部状态值【可读可写的数据】
            // 通过 this 可以访问到 props 中的初始值
            number: this.num,
        }
    },
}

并且把 data 中的 number 双向绑定到 input 输入框:

<input type="number" class="form-control form-control-sm ipt-num" v-model.number="number" />

4.为 -1 和 +1 按钮绑定响应的点击事件处理函数:

<button type="button" class="btn btn-light btn-sm" @click="onSubClick">-</button>
<input type="number" class="form-control form-control-sm ipt-num" v-model.number="number" />
<button type="button" class="btn btn-light btn-sm" @click="onAddClick">+</button>

并在 methods 中声明对应的事件处理函数如下:

methods: {
    // -1 按钮的事件处理函数
    onSubClick() {
    	this.number -= 1
    },
    // +1 按钮的事件处理函数
    onAddClick() {
    	this.number += 1
    },
},

2.3 实现 min 最小值的处理

需求分析:

购买商品时,购买的数量最小值为 1

1.在 EsCounter.vue 组件中封装如下的 props:

export default {
    name: 'EsCounter',
    props: {
        // 数量值
        num: {
            type: Number,
            default: 0,
        },
        // 最小值
        min: {
            type: Number,
            // min 属性的值默认为 NaN,表示不限制最小值
            default: NaN,
        },
    },
}

2.在 -1 按钮的事件处理函数中,对 min 的值进行判断和处理:

methods: {
    // -1 按钮的事件处理函数
    onSubClick() {
        // 判断条件:min 的值存在,且 number - 1 之后小于 min
        if (!isNaN(this.min) && this.number - 1 < this.min) return
        this.number -= 1
    },
}

3.在 EsGoods.vue 组件中使用 EsCounter.vue 组件时指定 min 最小值:

<!-- 商品数量 -->
<div class="count">
    <!-- 指定数量的最小值为 1 -->
    <es-counter :num="count" :min="1"></es-counter>
</div>

2.4 处理输入框的输入结果

思路分析:

  1. 将输入的新值转化为整数

  2. 如果转换的结果不是数字,或小于1,则强制 number 的值等于1

  3. 如果新值为小数,则把转换的结果赋值给 number

1.为输入框的 v-model 指令添加 .lazy 修饰符(当输入框触发 change 事件时更新 v-model 所绑定到的数据源):

<input type="number" class="form-control form-control-sm ipt-num" v-model.number.lazy="number" />

2.通过 watch 侦听器监听 number 数值的变化,并按照分析的步骤实现代码:

export default {
    name: 'EsCounter',
    watch: {
        // 监听 number 数值的变化
        number(newVal) {
            // 1. 将输入的新值转化为整数
            const parseResult = parseInt(newVal)
            // 2. 如果转换的结果不是数字,或小于1,则强制 number 的值等于 1
            if (isNaN(parseResult) || parseResult < 1) {
                this.number = 1
                return
            }
            // 3. 如果新值为小数,则把转换的结果赋值给 number
            if (String(newVal).indexOf('.') !== -1) {
                this.number = parseResult
                return
            }
            console.log(this.number)
    	},
    },
}

2.5 把最新的数据传递给使用者

需求分析:

当 EsGoods 组件使用 EsCounter 组件时,期望能够监听到商品数量的变化,此时需要使用自定

义事件的方式,把最新的数据传递给组件的使用者

1.在 EsCounter.vue 组件中声明自定义事件如下:

emits: ['numChange'],

2.在 EsCounter.vue 组件的 watch 侦听器中触发自定义事件:

    watch: {
        // 监听 number 数值的变化
        number(newVal) {
            // 1.将输入的新值转换为整数
            const parseResult = parseInt(newVal)
            // 2.如果转换的结果不是数字,或小于1,则强制 number 的值等于 1 
            if (isNaN(parseResult) || parseResult < 1) {
                this.number = 1
                return
            }
            // 3.如果新值为小数,则把转换的结果赋值给 number
            if (String(newVal).indexOf('.') !== -1) {
                this.number = parseResult
                return
            }
            // 触发自定义事件,把最新的 number 数值传递给组件的使用者
            this.$emit('numChange', this.number)
        }
    },

3.在 EsGoods.vue 组件中监听 EsCounter.vue 组件的自定义事件:

<!-- 商品数量 -->
<div class="count">
	<es-counter :num="count" :min="1" @numChange="getNumber"></escounter>
</div>

并声明对应的事件处理函数如下:

methods: {
    // 监听数量变化的事件
    getNumber(num) {
    	console.log(num)
    },
}

2.6 更新购物车中商品的数量

思路分析:

  1. 在 EsGoods 组件中获取到最新的商品数量

  2. 在 EsGoods 组件中声明自定义事件

  3. 在 EsGoods 组件中触发自定义事件,向外传递数据对象 { id, value }

  4. 在 App 根组件中监听 EsGoods 组件的自定义事件,并根据 id 更新对应商品的数量

1.在 EsGoods.vue 组件中声明自定义事件 countChange :

emits: ['numChange', 'countChange'],

2.在 EsCounter.vue 组件的 numChange 事件处理函数中,触发步骤1声明的自定义事件:

<es-counter :num="count" :min="1" @numChange="getNumber"></es-counter>

<script>
methods: {
    // 监听数量变化的事件
    getNumber(num) {
        // 触发自定义事件,向外传递数据对象 { id, value }
        this.$emit('countChange', {
            // 商品的 id
            id: this.id,
            // 最新的数量
            value: num,
        })
    },
}
</script>

3.在 App.vue 根组件中使用 EsGoods.vue 组件时,监听它的自定义事件 countChange :

<!-- 使用 goods 组件 -->
<es-goods
 v-for="item in goodslist"
 :key="item.id"
 :id="item.id"
 :thumb="item.goods_img"
 :title="item.goods_name"
 :price="item.goods_price"
 :count="item.goods_count"
 :checked="item.goods_state"
 @stateChange="onGoodsStateChange"
 @countChange="onGoodsCountChange"
></es-goods>

并在 methods 中声明对应的事件处理函数:

methods: {
    // 监听商品数量变化的事件
    onGoodsCountChange(e) {
        // 根据 id 进行查找
        const findResult = this.goodslist.find(x => x.id === e.id)
        // 找到了对应的商品,则更新其数量
        if (findResult) {
            findResult.goods_count = e.value
        }
    }
}

二十四、vue组件高级(上)总结

① 能够掌握 watch 侦听器的基本使用

  • 定义最基本的 watch 侦听器
  • immediate、 deep、监听对象中单个属性的变化

② 能够知道 vue 中常用的生命周期函数

  • 创建阶段、运行阶段、销毁阶段
  • created、mounted

③ 能够知道如何实现组件之间的数据共享

  • 父子组件、兄弟组件、后代组件

④ 能够知道如何在 vue3 的项目中全局配置 axios

  • main.js 入口文件中进行配置
  • app.config.globalProperties.$http = axios

Ⅴ vue组件高级(下)

目标目录
如何使用 ref 引用 DOM 和组件实例、$nextTick 的调用时机二十五、ref 引用
keep-alive 元素的作用二十六、动态组件
插槽的基本用法二十七、插槽
如何自定义指令二十八、自定义指令
以上内容的复习巩固二十九、第五个案例——Table 案例
总结与概括三十、vue组件高级(下)总结

二十五、ref 引用

1.什么是 ref 引用

ref 用来辅助开发者在不依赖于 jQuery 的情况下,获取 DOM 元素或组件的引用。

每个 vue 的组件实例上,都包含一个 $refs 对象,里面存储着对应的 DOM 元素或组件的引用。默认情况下,组件的 $refs 指向一个空对象

<template>
  <div>
    <h1>App 根组件</h1>
    <hr>

    <button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  methods: {
    getRefs() {
      // this 代表当前组件的实例对象,this.$refs 默认指向空对象 
      console.log(this)
    }
  },
}
</script>

<style lang="less" scoped></style>

2.使用 ref 引用 DOM 元素

如果想要使用 ref 引用页面上的 DOM 元素,则可以按照如下的方式进行操作:

<template>
  <div>
    <h1 ref="myh1">App 根组件</h1>
    <hr>

    <button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  methods: {
    getRefs() {
      this.$refs.myh1.style.color = 'red'
    }
  },
}
</script>

<style lang="less" scoped></style>

3.使用 ref 引用组件实例

如果想要使用 ref 引用页面上的组件实例,则可以按照如下的方式进行操作:

// 根组件
<template>
  <div>
    <h1 ref="myh1">App 根组件</h1>
    <hr>

    <button type="button" class="btn btn-primary" @click="getRefs">获取 $refs 引用</button>

    <my-counter ref="counterRef"></my-counter>
  </div>
</template>

<script>
import MyCounter from './Counter.vue'

export default {
  name: 'MyApp',
  methods: {
    getRefs() {
      this.$refs.counterRef.reset()
    }
  },
  components: {
    MyCounter,
  }
}
</script>

<style lang="less" scoped></style>

// 子组件
<template>
  <div class="counter-container">
    <h3>Counter 组件 --- {{ count }}</h3>
    <hr />
    <button type="button" class="btn btn-info" @click="count += 1">+1</button>
  </div>
</template>

<script>
export default {
  name: 'MyCounter',
  data() {
    return {
      count: 0,
    }
  },
  methods: {
    reset() {
      this.count = 0
    }
  },
}
</script>

<style lang="less" scoped>
.counter-container {
  margin: 20px;
  padding: 20px;
  border: 1px solid #efefef;
  border-radius: 4px;
  box-shadow: 0px 1px 10px #efefef;
}
</style>

4.控制文本框和按钮的按需切换

通过布尔值 inputVisible 来控制组件中的文本框与按钮的按需切换:

<template>
  <div>
    <h1>App 根组件</h1>
    <hr />

    <input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
    <button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  data() {
    return {
      inputVisible: false,
    }
  },
  methods: {
    showInput() {
      this.inputVisible = true
    }
  }
}
</script>

<style lang="less" scoped>
input.form-control {
  width: 280px;
  display: inline;
  vertical-align: bottom;
}
</style>

5.让文本框自动获得焦点

错误写法:当文本框展示出来之后,希望它立即获得焦点,为其添加 ref 引用,并调用原生 DOM 对象的.focus() 方法:

<template>
  <div>
    <h1>App 根组件</h1>
    <hr />

    <input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
    <button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  data() {
    return {
      inputVisible: false,
    }
  },
  methods: {
    showInput() {
      // 展示文本框
      this.inputVisible = true
      // 获取到文本框的引用对象,然后调用 focus() 方法
      this.$refs.ipt.focus()
    }
  }
}
</script>

<style lang="less" scoped>
input.form-control {
  width: 280px;
  display: inline;
  vertical-align: bottom;
}
</style>

正确写法:使用组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行。通俗的理解是:等组件的DOM 异步地重新渲染完成后,再执行 cb 回调函数。从而能保证 cb 回调函数可以操作到最新的 DOM 元素。

<template>
  <div>
    <h1>App 根组件</h1>
    <hr />

    <input type="text" class="form-control" v-if="inputVisible" ref="ipt" />
    <button type="button" class="btn btn-primary" v-else @click="showInput">展示 input 输入框</button>
  </div>
</template>

<script>
export default {
  name: 'MyApp',
  data() {
    return {
      inputVisible: false,
    }
  },
  methods: {
    showInput() {
      // 展示文本框
      this.inputVisible = true
      // 把对 input 文本框的操作,推迟到下次 DOM 更新之后,否则页面上根本不存在文本框元素
      this.$nextTick(() => {
        // 获取到文本框的引用对象,然后调用 focus() 方法
        this.$refs.ipt.focus()
      })
    },
  },
}
</script>

<style lang="less" scoped>
input.form-control {
  width: 280px;
  display: inline;
  vertical-align: bottom;
}
</style>

二十六、动态组件

1.什么是动态组件

动态组件指的是动态切换组件的显示与隐藏。vue 提供了一个内置的 组件,专门用来实现组件的动态渲染。

① 是组件的占位符

② 通过 is 属性动态指定要渲染的组件名称

2.如何实现动态组件渲染

<template>
  <div>
    <h1 class="mb-4">App 根组件</h1>
    <button type="button" class="btn btn-primary" @click="comName = 'MyHome'">首页</button>
    <button type="button" class="btn btn-info ml-2" @click="comName = 'MyMovie'">电影</button>
    <hr />
    <component :is="comName"></component>
  </div>
</template>

<script>
// 导入组件
import MyHome from './Home.vue'
import MyMovie from './Movie.vue'

export default {
  name: 'MyApp',
  data() {
    return {
      comName: 'MyHome'
    }
  },
  // 注册组件
  components: {
    MyHome,
    MyMovie,
  },
}
</script>

<style lang="less" scoped></style>

3.使用 keep-alive 保持状态

默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 组件保持动态组件的状态

<template>
  <div>
    <h1 class="mb-4">App 根组件</h1>
    <button type="button" class="btn btn-primary" @click="comName = 'MyHome'">首页</button>
    <button type="button" class="btn btn-info ml-2" @click="comName = 'MyMovie'">电影</button>
    <hr />
    <!-- 使用 keep-alive 组件 -->
    <keep-alive>
    	<component :is="comName"></component>
    </keep-alive>
  </div>
</template>

二十七、插槽

1.什么是插槽

插槽Slot)是 vue 为组件的封装者提供的能力。允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jHLcqvTe-1685004795672)(./assets/Snipaste_2023-05-14_16-42-54.png)]

可以把插槽认为是组件封装期间,为用户预留的内容的占位符。

2.插槽的基础用法

在封装组件时,可以通过 元素定义插槽,从而为用户预留内容占位符

2.1 没有预留插槽的内容会被丢弃

如果在封装组件时没有预留任何 插槽,则用户提供的任何自定义内容都会被丢弃。

2.2 后备内容

封装组件时,可以为预留的 插槽提供后备内容(默认内容)。如果组件的使用者没有为插槽提供任何内容,则后备内容会生效。

3.具名插槽

如果在封装组件时需要预留多个插槽节点,则需要为每个 插槽指定具体的 name 名称。这种带有具体名称的插槽叫做“具名插槽”:

<template>
  <div>
    <!-- 我们希望把页头放到这里 -->
    <header>
      <slot name="header"></slot>
    </header>
    <!-- 我们希望把主要内容放到这里,没有指定 name 名称的插槽,会有隐含的名称叫做 "default" -->
    <main>
      <slot></slot>
    </main>
    <!-- 我们希望把页脚放到这里 -->
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
3.1 为具名插槽提供内容

在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<template>
  <div>
    <h1>App 根组件</h1>
    <hr />

    <!-- 使用组件 -->
    <my-article>
      <template #header>
        <h1>滕王阁序</h1>
      </template>
      <template #default>
        <p>豫章故郡,洪都新府。</p>
        <p>星分翼轸,地接衡庐</p>
        <p>襟三江而带五湖,控蛮荆而引瓯越。</p>
      </template>
      <template #footer>
        <p>落款:王勃</p>
      </template>
    </my-article>
  </div>
</template>
3.2 具名插槽的简写形式

v-onv-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header可以被重写为 #header

4.作用域插槽

在封装组件的过程中,可以为预留的 插槽绑定 props 数据,这种带有 props 数据的 叫做“作用域插槽”:

<!-- 子组件 -->
<div>
    <h3>这是 TEST 组件</h3>
    <slot :info="infomation"></slot>
</div>

<!-- 使用组件 -->
<my-test>
	<template v-slot:default="scope">
    	{{ scope }}
    </template>
</my-test>
4.1 解构作用域插槽的 Prop

作用域插槽对外提供的数据对象,可以使用解构赋值简化数据的接收过程:

<!-- 使用自定义组件 -->
<my-test>
    <template #default="{ msg, info }">
<p>{{ msg }}</p>
<p>{{ info.address }}</p>
    </template>
</my-test>
4.2 声明作用域插槽

在封装 MyTable 组件的过程中,可以通过作用域插槽把表格每一行的数据传递给组件的使用者:

<!-- 表格主体区域 -->
<tbody>
	<!-- 循环渲染表格数据 -->
	<tr v-for="item in list" :key="item.id">
		<slot :user="item"></slot>
	</tr>
</tbody>
4.3 使用作用域插槽

在使用 MyTable 组件时,自定义单元格的渲染方式,并接收作用域插槽对外提供的数据:

<!-- 表格主体区域 -->
<tbody>
	<!-- 循环渲染表格数据 -->
	<tr v-for="item in list" :key="item.id">
		<slot :user="item"></slot>
	</tr>
</tbody>
<!-- 使用组件,填充插槽 -->
<my-table>
	<template #default="{ user }">
		<td>{{ user.id }}</td>
		<td>{{ user.name }}</td>
		<td>
			<input type="checkbox" :checked="user.state" />
		</td>
	</template>
</my-table>

二十八、自定义指令

1.什么是自定义指令

vue 官方提供了 v-for、v-model、v-if 等常用的内置指令。除此之外 vue 还允许开发者自定义指令。

vue 中的自定义指令分为两类,分别是:

  • 私有自定义指令
  • 全局自定义指令

2.声明私有自定义指令的语法

在每个 vue 组件中,可以在 directives 节点下声明私有自定义指令:

directives: {
	// 自定义私有指令
	focus: {
        // 当被绑定的元素插入到 DOM 中时,自动触发 mounted 函数
		mounted(el) {
            // 被绑定的元素自动获取焦点
			el.focus()
		},
	},
},

3.使用自定义指令

在使用自定义指令时,需要加上v-前缀:

<input v-focus />

4.声明全局自定义指令

全局共享的自定义指令需要通过“单页面应用程序的实例对象”进行声明:

app.directive('focus', {
  mounted(el) {
    console.log('mounted')
    el.focus()
  },
})

5.updeated 函数

mounted 函数只在元素第一次插入 DOM 时被调用,当 DOM 更新时 mounted 函数不会被触发。 updated 函数会在每次 DOM 更新完成后被调用。示例代码如下:

app.directive('focus', {
  // 第一次插入 DOM 时触发这个函数
  mounted(el) {
    console.log('mounted')
    el.focus()
  },
  // 每次 DOM 更新时都会触发 updated 函数
  updated(el) {
    console.log('updated')
    el.focus()
  },
})

注意:在 vue2 的项目中使用自定义指令时,【 mounted -> bind 】【 updated -> update

6.函数简写

如果 mounted 和updated 函数中的逻辑完全相同,则可以简写成如下格式:

app.directive('focus', (el) => {
  // 在 mounted 和 updated 时都会触发相同的业务处理
  el.focus()
})

7.指令的参数值

在绑定指令时,可以通过“等号”的形式为指令绑定具体的参数值

<!-- 在使用 v-color 指令时,可以通过 “等号” 绑定指令的值 -->
<h3 v-color="'red'">MyHome 组件 --- {{ count }}</h3>

<input type="text" class="form-control" v-focus v-color="'cyan'" />

<!-- 自定义 v-color 指令 -->
app.directive('color', (el, binding) => {
  <!-- binding.value 就是通过 “等号” 为指令绑定的值 -->
  el.style.color = binding.value
})

二十九、第五个案例——Table 案例

1.案例描述

1.1 案例效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fWPqoSKM-1685004795673)(./assets/Snipaste_2023-05-17_17-29-53.png)]

1.2 用到的知识点
  • 组件封装
  • 具名插槽
  • 作用域插槽
  • 自定义指令
1.3 实现步骤
  1. 搭建项目的基本结构
  2. 请求商品列表的数据
  3. 封装 MyTable 组件
  4. 实现删除功能
  5. 实现添加标签的功能

2.具体实现

2.1 搭建项目基本结构

1.初始化项目

1 初始化项目
npm init vite-app case-table
2 cd 到项目根目录
cd case-table
3 安装项目依赖项
npm install
4 安装 less 依赖包
npm i less -D
5 项目运行
npm run dev

2.梳理项目结构

  • 重置 App.vue 根组件的代码结构
  • 删除 components 目录下 HelloWorld.vue 组件
  • 重置 index.css 中的样式
  • 在 main.js 入口文件中导入 bootstrap 样式文件
2.2 请求商品列表数据

1.运行如下命令,安装 Ajax 的请求库:

npm i axios -S

2.在 main.js 入口模块中导入并全局配置axios:

// 1.导入 axios
import axios from 'axios'

const app = createApp(App)

// 2.将 axios 挂载到全局,今后,每个组件中,都可以直接通过this.$http 代替 axios 发起 Ajax 请求
app.config.globalProperties.$http = axios

// 3. 配置请求的根路径
axios.defaults.baseURL = 'https://www.escook.cn'
// 此网站接口已废弃

app.mount('#app')

3.在 App.vue 组件的 data 中声明 goodslist 商品列表数据:

data() {
    return {
       // 商品列表数据
       goodslist:[]
    }
}

4.在 App.vue 组件的 methods 中声明 getGoodsList 方法,用来从服务器请求商品列表的数据:

methods: {
	// 初始化商品列表数据
    async getGoodsList() {
 		// 发起 Ajax 请求
 		const { data: res } = await this.$http.get('/api/goods')
 		// 请求失败
 		if (res.status !== 0) return console.log('获取商品列表失败!')
 		// 请求成功
 		this.goodslist = res.data
 	}
}

5.在 App.vue 组件中,声明 created 生命周期函数,并调用 getGoodsList 方法:

created() {
	this.getGoodsList()
}
2.3 封装 MyTable 组件

封装要求

1.用户通过名为 data 的 prop 属性,为 MyTable.vue 组件指定数据源

2.在 MyTable.vue 组件中,预留名称为 header 的具名插槽

3.在 MyTable.vue 组件中,预留名称为 body 的作用域插槽

1.创建并使用 MyTable 组件

  1. 在 components/my-table 目录下新建 MyTable.vue 组件
  2. 在 App.vue 组件中导入并注册 MyTable.vue 组件
  3. 在 App.vue 组件的 DOM 结构中使用 MyTable.vue 组件

2.为表格声明 data 数据源

2.1 在 MyTable.vue 组件的 props 节点中声明表格的 data 数据源

<script>
export default {
    name: 'MyTable',
    props: {
        // 表格的数据源
        data: {
            type: Array,
            required: true,
            default: [],
        }
    },
}
</script>

2.2 在 App.vue 组件中使用 MyTable.vue 组件时,通过属性绑定的形式为表格指定 data 数据源

<!-- 使用表格组件 -->
<my-table :data="goodslist"></my-table>

3.封装 MyTable 组件的模板结构

3.1 基于 bootstrap 提供的 Tables( https://v4.bootcss.com/docs/content/tables/ ),在 MyTable.vue 组件中渲染最基本的模板结构:

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <th>#</th>
                <th>商品名称</th>
                <th>价格</th>
                <th>标签</th>
                <th>操作</th>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody></tbody>
    </table>
</template>

3.2 为了提高组件的复用性,最好把表格的 标题区域 预留为 具名插槽,方便使用者自定义表格的标题:

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <!-- 命名插槽 -->
                <slot name="header"></slot>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody></tbody>
    </table>
</template>

3.3 在 App.vue 组件中,通过具名插槽的形式,为 MyTable.vue 组件指定标题名称:

<!-- 使用表格组件 -->
<my-table :data="goodslist">
    <!-- 表格的标题 -->
    <template #header>
		<th>#</th>
		<th>商品名称</th>
		<th>价格</th>
		<th>标签</th>
		<th>操作</th>
    </template>
</my-table>

4.预留名称为 body 的作用域插槽

4.1 在 MyTable.vue 组件中,通过 v-for 指令循环渲染表格的数据行:

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <!-- 命名插槽 -->
                <slot name="header"></slot>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody>
            <!-- 使用 v-for 指令,循环渲染表格的数据行 -->
            <tr v-for="(item, i) in data" :key="item.id"></tr>
        </tbody>
    </table>
</template>

4.2 为了提高 MyTable.vue 组件的复用性,最好把表格数据行里面的 td 单元格预留为 具名插槽:

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <!-- 命名插槽 -->
                <slot name="header"></slot>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody>
            <!-- 使用 v-for 指令,循环渲染表格的数据行 -->
            <tr v-for="(item, i) in data" :key="item.id">
                <!-- 为数据行的 td 预留的插槽 -->
                <slot name="body"></slot>
            </tr>
        </tbody>
    </table>
</template>

4.3 为了让组件的使用者在提供 body 插槽的内容时,能够自定义内容的渲染方式,需要把 body 具名插槽升级为 作用域插槽 :

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <!-- 命名插槽 -->
                <slot name="header"></slot>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody>
            <!-- 使用 v-for 指令,循环渲染表格的数据行 -->
            <tr v-for="(item, i) in data" :key="item.id">
                <!-- 为数据行的 td 预留的 "作用域插槽" -->
                <slot name="body" :row="item" :index="index"></slot>
            </tr>
        </tbody>
    </table>
</template>

4.4 在 App.vue 组件中,基于作用域插槽的方式渲染表格的数据:

	<!-- 使用表格组件 -->
	<my-table :data="goodslist">
		<!-- 表格的标题 -->
		<template #header>
			<th>#</th>
			<th>商品名称</th>
			<th>价格</th>
			<th>标签</th>
			<th>操作</th>
		</template>

		<template #body="{ row, index }">
			<td>{{ index + 1 }}</td>
			<td>{{ row.goods_name }}</td>
			<td>¥{{ row.goods_price }}</td>
			<td>{{ row.tags }}</td>
			<td>
				<button type="button" class="btn btn-danger btn-sm">删除</button>
			</td>
		</template>
	</my-table>
2.4 实现删除功能

1.为删除按钮绑定 click 事件处理函数:

<button type="button" class="btn btn-danger btn-sm" @click="onRemove(row.id)">删除</button>

2.在 App.vue 组件的 methods 中声明事件处理函数如下:

methods: {
	// 根据 id 删除商品信息
	onRemove(id) {
		this.goodslist = this.goodslist.filter(x => x.id !== id)
	},
},
2.5 实现添加标签功能

1.自定义渲染标签列

根据 bootstrap 提供的 Badge ( https://v4.bootcss.com/docs/components/badge/#contextual-variations )效果,循环渲染商品的标签信息如下:

<td>
	<span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span></td>

2.实现 input 和 button 的按需展示

2.1 使用 v-if 结合 v-else 指令,控制 input 和 button 的按需展示:

<td>
	<!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示-->
	<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible">
	<button type="button" class="btn btn-primary btn-sm" v-else>+Tag</button>
	<span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span>
</td>

2.2 点击按钮,控制 input 和 button 的切换:

<td>
	<!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示-->
	<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible">
	<button type="button" class="btn btn-primary btn-sm" @click="row.inputVisible = true" v-else>+Tag</button>
	<span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span>
</td>

3.让 input 自动获取焦点

3.1 在 App.vue 组件中,通过 directives 节点自定义 v-focus 指令如下:

directives: {
	// 封装自动获得焦点的指令
	focus(el) {
		el.focus()
	},
},

3.2 为 input 输入框应用 v-focus 指令:

<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus>

4.文本框失去焦点自动隐藏

4.1 使用 v-model 指令把 input 输入框的值双向绑定到 row.inputValue 中:

<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus
v-model.trim="row.inputValue">

4.2 监听文本框的 blur 事件,在触发其事件处理函数时,把 当前行的数据 传递进去:

<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)">

4.3 在 App.vue 组件的 methods 节点下声明 onInputConfirm 事件处理函数:

onInputConfirm(row) {
	// 1. 把用户在文本框中输入的值,预先转存到常量 val 中
	const val = row.inputValue
	// 2. 清空文本框的值
	row.inputValue = ''
	// 3. 隐藏文本框
	row.inputVisible = false
}

5.为商品添加新的 tag 标签

进一步修改 onInputConfirm 事件处理函数如下:

onInputConfirm(row) {
	// 1. 把用户在文本框中输入的值,预先转存到常量 val 中
	const val = row.inputValue
	// 2. 清空文本框的值
	row.inputValue = ''
	// 3. 隐藏文本框
	row.inputVisible = false

	// 1.1 判断 val 的值是否为空,如果为空,则不进行添加
	// 1.2 判断 val 的值是否已存在于 tags 数组中,防止重复添加
	if (!val || row.tags.indexOf(val) !== -1) return
	// 2. 将用户输入的内容,作为新标签 push 到当前行的 tags 数组中
	row.tags.push(val)
}

6.响应文本框的回车按键

当用户在文本框中敲击了 回车键 的时候,也希望能够把当前输入的内容添加为 tag 标签。此时,可以为文本框绑定 keyup 事件如下:

<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onIputConfirm(row)" >

7.响应文本框的 esc 按键

当用户在文本框中敲击了 esc 按键的时候,希望能够快速清空文本框的内容。此时,可以为文本框绑定 keyup 事件如下:

<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onIputConfirm(row)" @keyup.esc="row.inputValue = ''">

3.完整代码

App 根组件

<template>
	<div>
		<h1>App 根组件</h1>
	</div>
	<hr>
	<!-- 使用表格组件 -->
	<my-table :data="goodslist">
		<!-- 表格的标题 -->
		<template #header>
			<th>#</th>
			<th>商品名称</th>
			<th>价格</th>
			<th>标签</th>
			<th>操作</th>
		</template>

		<template #body="{ row, index }">
			<td>{{ index + 1 }}</td>
			<td>{{ row.goods_name }}</td>
			<td>¥{{ row.goods_price }}</td>
			<td>
				<!-- 基于当前行的 inputVisible,来控制 input 和 button 的按需展示-->
				<input type="text" class="form-control form-control-sm ipt-tag" v-if="row.inputVisible" v-focus
					v-model.trim="row.inputValue" @blur="onInputConfirm(row)" @keyup.enter="onInputConfirm(row)"
					@keyup.esc="row.inputValue = ''">
				<button type="button" class="btn btn-primary btn-sm" @click="row.inputVisible = true" v-else>+Tag</button>
				<span class="badge badge-warning ml-2" v-for="item in row.tags" :key="item">{{ item }}</span>
			</td>
			<td>
				<button type="button" class="btn btn-danger btn-sm" @click="onRemove(row.id)">删除</button>
			</td>
		</template>
	</my-table>
</template>

<script>
import MyTable from './components/my-table/MyTable.vue'


export default {
	name: 'MyApp',
	data() {
		return {
			goodslist: []
		}
	},
	created() {
		this.getGoodsList()
	},
	methods: {
		async getGoodsList() {
			const { data: res } = await this.$http.get('/api/goods')
			if (res.status !== 0) return console.log('获取商品列表失败!')
			this.goodslist = res.data
		},
		// 根据 id 删除商品信息
		onRemove(id) {
			this.goodslist = this.goodslist.filter(x => x.id !== id)
		},
		onInputConfirm(row) {
			// 1. 把用户在文本框中输入的值,预先转存到常量 val 中
			const val = row.inputValue
			// 2. 清空文本框的值
			row.inputValue = ''
			// 3. 隐藏文本框
			row.inputVisible = false

			// 1.1 判断 val 的值是否为空,如果为空,则不进行添加
			// 1.2 判断 val 的值是否已存在于 tags 数组中,防止重复添加
			if (!val || row.tags.indexOf(val) !== -1) return
			// 2. 将用户输入的内容,作为新标签 push 到当前行的 tags 数组中
			row.tags.push(val)
		}
	},
	directives: {
		// 封装自动获得焦点的指令
		focus(el) {
			el.focus()
		},
	},
	components: {
		MyTable,
	}
}
</script>

<style lang="less" scoped></style>

MyTable 子组件

<template>
    <table class="table table-bordered table-striped">
        <!-- 表格的标题区域 -->
        <thead>
            <tr>
                <!-- 命名插槽 -->
                <slot name="header"></slot>
            </tr>
        </thead>
        <!-- 表格的主体区域 -->
        <tbody>
            <!-- 使用 v-for 指令,循环渲染表格的数据行 -->
            <tr v-for="(item, index) in data" :key="item.id">
                <!-- 为数据行的 td 预留的 "作用域插槽" -->
                <slot name="body" :row="item" :index="index"></slot>
            </tr>
        </tbody>
    </table>
</template>

<script>
export default {
    name: 'MyTable',
    props: {
        // 表格的数据源
        data: {
            type: Array,
            required: true,
            default: [],
        }
    },
}
</script>

<style lang="less" scoped></style>

main.js 入口文件

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import './assets/css/bootstrap.css'
import axios from 'axios'

const app = createApp(App)

app.config.globalProperties.$http = axios

axios.defaults.baseURL = 'https://applet-base-api-t.itheima.net'

app.mount('#app')

三十、vue 组件高级(下)总结

① 能够知道如何使用 ref 引用 DOM 和组件实例

  • 通过 ref 属性指定引用的名称、使用 this.$refs 访问引用实例

② 能够知道 $nextTick 的调用时机

  • 组件的 DOM 更新之后,才执行 $nextTick 中的回调

③ 能够说出 keep-alive 元素的作用

  • 保持动态组件的状态

④ 能够掌握插槽的基本用法

  • 标签、具名插槽、作用域插槽、v-slot: 简写为 #

⑤ 能够知道如何自定义指令

  • 私有自定义指令、全局自定义指令

Ⅵ 路由

目标目录
前端路由的工作方式以及原理三十一、前端路由的概念与原理
vue-router 4.x 的基本使用三十二、vue-router 的基本用法
重定向、高亮、嵌套、动态、编程式导航、命名、守卫三十三、vue-router 的高级用法
巩固 vue-router 的使用三十四、第六个案例——后台管理案例
概括与总结三十五、路由总结

三十一、前端路由的概念与原理

1.什么是路由

路由(英文:router)就是对应关系。路由分为两大类:

  • 后端路由
  • 前端路由

2.回顾:后端路由

后端路由指的是:请求方式、请求地址与 function 处理函数之间的对应关系。在 node.js 课程中,express 路由的基本用法如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aTI392F3-1685004795673)(./assets/Snipaste_2023-05-18_21-22-28.png)]

3.SPA 与前端路由

SPA 指的是一个 web 网站只有唯一的一个 HTML 页面,所有组件的展示与切换都在这唯一的一个页面内完成。此时,不同组件之间的切换需要通过前端路由来实现。

结论:在 SPA 项目中,不同功能之间的切换,要依赖于前端路由来完成!

4.什么是前端路由

通俗易懂的概念:Hash 地址组件之间的对应关系

5.前端路由的工作方式

① 用户点击了页面上的路由链接

② 导致了 URL 地址栏中的 Hash 值发生了变化

前端路由监听了到 Hash 地址的变化

④ 前端路由把当前 Hash 地址对应的组件渲染都浏览器中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BjuiS0HL-1685004795674)(./assets/Snipaste_2023-05-18_21-31-12.png)]

结论:前端路由,指的是 Hash 地址组件之间的对应关系

6.实现简易的前端路由

6.1 导入并注册 MyHome、MyMovie、MyAbout 三个组件:

<script>
import MyHome from './MyHome.vue'
import MyMovie from './MyMovie.vue'
import MyAbout from './MyAbout.vue'

export default {
  name: 'MyApp',
  components: {
    MyHome,
    MyMovie,
    MyAbout,
  },
}
</script>

6.2 通过 标签的 is 属性,动态切换要显示的组件:

<template>
  <div>
    <h1>App 根组件</h1>
    <component :is="comName"></component>
  </div>
</template>

<script>
import MyHome from './MyHome.vue'
import MyMovie from './MyMovie.vue'
import MyAbout from './MyAbout.vue'

export default {
  name: 'MyApp',
  data() {
    return {
      comName: 'MyHome',
    }
  },
}
</script>

6.3 在组件的结构中声明如下 3 个 链接,通过点击不同的 链接,切换浏览器地址栏中的 Hash 值:

<a href="#/home">Home</a>&nbsp; 
<a href="#/movie">Movie</a>&nbsp; 
<a href="#/about">About</a>&nbsp;

6.4 在 created 生命周期函数中监听浏览器地址栏中 Hash 地址的变化,动态切换要展示的组件的名称:

created() {
    // 监听 hash 值变化的事件
    window.onhashchange = () => {
        // 通过 location.hash 获取到最新的 hash 值,并进行匹配
        switch (location.hash) {
            case '#/home':
            this.comName = 'MyHome'
            break
            case '#/movie':
            this.comName = 'MyMovie'
            break
            case '#/about':
            this.comName = 'MyAbout'
            break
        }
    }
},

三十二、vue-router 的基本用法

1.什么是 vue-router

vue-router 是 vue.js 官方给出的路由解决方案。它只能结合 vue 项目进行使用,能够轻松的管理 SPA 项目中组件的切换。

2.vue-router 的版本

vue-router 目前有 3.x 的版本和 4.x 的版本。其中:

  • vue-router 3.x 只能结合 vue2 进行使用
  • vue-router 4.x 只能结合 vue3 进行使用

vue-router 3.x 的官方文档地址:https://router.vuejs.org/zh/

vue-router 4.x 的官方文档地址:https://next.router.vuejs.org/

3.vue-router 4.x 的基本使用步骤

① 在项目中安装 vue-router

② 定义路由组件

③ 声明路由链接和占位符

④ 创建路由模块

⑤ 导入并挂载路由模块

3.1 在项目中安装 vue-router

在 vue3 的项目中,只能安装并使用 vue-router 4.x

npm install vue-router@next -S
3.2 定义路由组件

在项目中定义 MyHome.vue、MyMovie.vue、MyAbout.vue 三个组件,将来要使用 vue-router 来控制它们的展示与切换。

3.3 声明路由链接和占位符

可以使用 标签来声明路由链接,并使用 标签来声明路由占位符:

<template>
  <div>
    <h1>vue-router 的基本使用</h1>
    <!-- 声明路由链接 -->
    <router-link to="/home">首页</router-link>&nbsp;
    <router-link to="/movie">电影</router-link>&nbsp;
    <router-link to="/about">关于</router-link>
    <hr />

    <!-- 路由的占位符 -->
    <router-view></router-view>
  </div>
</template>
3.4 创建路由模块

在项目中创建 router.js 路由模块,在其中按照如下 4 个步骤创建并得到路由的实例对象:

① 从 vue-router 中按需导入两个方法

② 导入需要使用路由控制的组件

③ 创建路由实例对象

④ 向外共享路由实例对象

⑤ 在 main.js 中导入并挂载路由模块

// 1.从 vue-router 中按需导入两个方法
// createRouter 方法用于创建路由的实例对象
// createWebHashHistory 用于指定路由的工作模式(hash 模式)
import { createRouter, createWebHashHistory } from 'vue-router'
// 2.导入组件,这些组件将要以路由的方式,来控制它们的切换
import Home from './MyHome.vue'
import Movie from './MyMovie.vue'
import About from './MyAbout.vue'
// 3.创建路由实例对象
const router = createRouter({
  // 3.1 通过 history 属性指定路由的工作模式
  history: createWebHashHistory(),
  // 3.2 通过 routes 数组,声明路由的匹配规则
  routes: [
    // path 是 hash 地址,component 是要展示的组件
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    { path: '/about', component: About },
  ],
})
// 4.向外共享路由实例对象
// 供其他模块导入并使用
export default router
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
// 5.导入路由模块
import router from './router'

const app = createApp(App)

// 6.挂载路由模块
// app.use() 方法用来挂载“第三方的插件模块”
app.use(router)

app.mount('#app')

三十三、vue-router 的高级用法

1.路由重定向

路由重定向指的是:用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面。通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向:

// 创建路由对象
const router = createRouter({
  // 指定路由的工作模式
  history: createWebHashHistory(),
  // 声明路由的匹配规则
  routes: [
  	// 其中,path 表示需要被重定向的“原地址”,redirect 表示将要被重定向到的“新地址”
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    { path: '/about', component: About },
  ],
})

2.路由高亮

可以通过如下的两种方式,将激活的路由链接进行高亮显示:

① 使用默认的高亮 class 类

自定义路由高亮的 class 类

2.1 默认的高亮 class 类

被激活的路由链接,默认会应用一个叫做 router-link-active 的类名。开发者可以使用此类名选择器,为激活的路由链接设置高亮的样式:

/* 在 index.css 全局样式表中,重新设置 router-link-active 的样式 */
.router-link-active {
  background-color: red;
  color: white;
  font-weight: bold;
}
2.2 自定义路由的高亮的 class 类

在创建路由的实例对象时,开发者可以基于 linkActiveClass 属性,自定义路由链接被激活时所应用的类名:

// 创建路由对象
const router = createRouter({
  // 指定路由的工作模式
  history: createWebHashHistory(),
  // 指定被激活的路由链接,会应用 router-active 这个类名
  // 默认的 router-link-active 类名会被覆盖掉
  // 自定义路由高亮的 class 类
  linkActiveClass: 'active-router',
  // 声明路由的匹配规则
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    { path: '/about', component: About },
  ],
})

3.嵌套路由

通过路由实现组件的嵌套展示,叫做嵌套路由。

① 声明子路由链接和子路由占位符

② 在父路由规则中,通过 children 属性嵌套声明子路由规则

3.1 声明子路由链接和子路由占位符

在 About.vue 组件中,声明 tab1 和 tab2 的子路由链接以及子路由占位符:

<template>
  <div>
    <h3>MyAbout 组件</h3>

    <!-- 声明子路由链接 -->
    <router-link to="/about/tab1">Tab1</router-link>&nbsp;
    <router-link to="/about/tab2">Tab2</router-link>
    <hr>

    <!-- 声明子路由占位符 -->
    <router-view></router-view>
  </div>
</template>
3.2 通 children 属性声明子路由规则

在 router.js 路由模块中,导入需要的组件,并使用 children 属性声明子路由规则:

// 创建路由对象
const router = createRouter({
  // 指定路由的工作模式
  history: createWebHashHistory(),
  // 自定义路由高亮的 class 类
  linkActiveClass: 'active-router',
  // 声明路由的匹配规则
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/movie', component: Movie },
    {
      path: '/about',
      component: About,
      // 嵌套路由的重定向
      redirect: '/about/tab1',
      // 通过 children 属性嵌套声明子级路由规则
      children: [
        { path: 'tab1', component: Tab1 },
        { path: 'tab2', component: Tab2 },
      ],
    },
  ],
})

4.动态路由匹配

4.1 动态路由的概念

动态路由指的是:把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。在 vue-router 中使用英文的冒号(:)来定义路由的参数项:

// 路由中的动态参数以 : 进行声明,冒号后面的是动态参数的名称
{ path: './movie/:id', component: Movie }

// 将以下 3 个路由规则,合并成了一个,提高了路由规则的复用性
{ path: './movie/1', component: Movie }
{ path: './movie/2', component: Movie }
{ path: './movie/3', component: Movie }
4.2 $route.params 参数对象

通过动态路由匹配的方式渲染出来的组件中,可以使用 $route.params 对象访问到动态匹配的参数值。

<template>
  <div>
  	<!-- $route.params 是路由的"参数对象" -->
    <h3>MyMovie 组件 --- {{ $route.params.id }}</h3>
  </div>
</template>

<script>
export default {
  name: 'MyMovie',
}
</script>
4.3 使用 props 接收路由参数

为了简化路由参数的获取形式,vue-router 允许在路由规则中开启 props 传参:

// 1.定义路由规则时,声明 props: true 选项
// 即可在 Movie 组件中,以 props 的形式接收到路由规则匹配到的参数项
{ path: '/movie/:mid', component: Movie, props: true },
<template>
  <div>
    <!-- 3.直接使用 props 中接收的路由参数 -->
    <h3>MyMovie 组件 --- {{ id }}</h3>
  </div>
</template>

<script>
export default {
  name: 'MyMovie',
  // 2.使用 props 接收路由匹配规则中匹配到的参数项
  props: ['id'],
}
</script>

5.编程式导航

通过调用 API 实现导航的方式,叫做编程式导航。与之对应的,通过点击链接实现导航的方式,叫做声明式导航。例如:

  • 普通网页中点击 链接、vue 项目中点击 都属于声明式导航
  • 普通网页中调用 location.href 跳转到新页面的方式,属于编程式导航
5.1 vue-router 中的编程式导航 API

vue-router 提供了许多编程式导航的 API,其中最常用的两个 API 分别是:

① this.$router.push(‘hash 地址’) 跳转到指定 Hash 地址,从而展示对应的组件

② this.$router.go(数值 n) 实现导航历史的前进、后退

5.2 $router.push

调用 this.$router.push() 方法,可以跳转到指定的 hash 地址,从而展示对应的组件页面:

<template>
  <div>
    <h3>MyHome 组件</h3>
    <button type="button" class="btn btn-primary" @click="goToMovie(3)">导航到Movie页面</button>
  </div>
</template>

<script>
export default {
  name: 'MyHome',
  methods: {
    goToMovie(id) {
      this.$router.push('/movie/' + id)
    },
  },
}
</script>
5.3 $router.go

调用 this.$router.go() 方法,可以在浏览历史中进行前进和后退:

<template>
	<h3>MyMovie --- {{id}}</h3>
	<button @click="goBack">后退</button>
</template>

<script>
export default {
    props: ['id'],
    methods:{
        // 后退到之前的组件页面
        goBack() {
            this.$router.go(-1)
        }
    },
}
</script>

6.命名路由

通过 name 属性为路由规则定义名称的方式,叫做命名路由

{
    path: '/movie/:id',
    // 使用 name 属性为当前的路由规则定义一个“名称”
    name: 'mov',
    component: Movie,
    props: true,
}

注意:命名路由的 name 值不能重复必须保证唯一性

6.1 使用命名路由实现声明式导航

为 标签动态绑定 to 属性的值,并通过 name 属性指定要跳转到的路由规则。期间还可以用 params 属性指定跳转期间要携带的路由参数:

<router-link :to="{ name: 'mov', params: { mid: 2 } }">go to movie</router-link>
6.2 使用命名路由实现编程式导航

调用 push 函数期间指定一个配置对象,name 是要跳转到的路由规则、params 是携带的路由参数:

<template>
  <div>
    <h3>MyHome 组件</h3>
    <!-- 命名路由声明式导航 -->
    <router-link :to="{ name: 'mov', params: { mid: 2 } }">go to movie</router-link>
    <!-- 命名路由编程式导航 -->
    <button type="button" class="btn btn-primary" @click="goToMovie(1)">go to movie</button>
  </div>
</template>

<script>
export default {
  name: 'MyHome',
  methods: {
    goToMovie(id) {
      this.$router.push({
        name: 'mov',
        params: {
          mid: id,
        },
      })
    },
  },
}
</script>

7.导航守卫

导航守卫可以控制路由的访问权限:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kPBMcWfs-1685004795674)(./assets/Snipaste_2023-05-21_16-43-40.png)]

7.1 如何声明全局导航首位

全局导航守卫会拦截每个路由规则,从而对每个路由进行访问权限的控制。可以按照如下的方式定义全局导航守卫:

// 声明全局的导航守卫
// 调用路由实例对象的 beforeEach 函数,声明“全局前置守卫”
// fn 必须是一个函数,每次拦截到路由的请求,都会调用 fn 进行处理
// 因此 fn 叫做 “守卫方法”
router.beforeEach( () => {
  console.log('ok')
})
7.2 守卫方法的 3 个形参

全局导航守卫的守卫方法中接收 3 个形参,格式为:

// 声明全局的导航守卫
router.beforeEach((to, from, next) => {
	// to 目标路由对象
    // from 当前导航正要离开的路由对象
    // next 是一个函数,表示放行
})

注意:

① 在守卫方法中如果不声明 next 形参,则默认允许用户访问每一个路由

② 在守卫方法中如果声明了 next 形参,则必须调用 next() 函数否则不允许用户访问任何一个路由

7.3 next 函数的 3 中调用方式

参考示意图,分析 next 函数的 3 种调用方式最终导致的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L274eskD-1685004795675)(./assets/Snipaste_2023-05-21_16-47-39.png)]

直接放行:next()

强制其停留在当前页面:next(false)

强制其跳转到登录页面:next(‘/login’)

// 创建路由对象
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/main', component: Main },
    { path: '/login', component: Login },
  ],
})

// 全局路由导航守卫
router.beforeEach((to, from, next) => {
  if (to.path === '/main') {
    // 证明用户要访问后台主页
    next('/login')
  } else {
    // 访问的不是后台主页
    next()
  }
})
7.4 结合 token 控制后台主页的访问权限
// 创建路由对象
const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home },
    { path: '/main', component: Main },
    { path: '/login', component: Login },
  ],
})

// 全局路由导航守卫
router.beforeEach((to, from, next) => {
// 1.读取 token
const tokenStr = localStorage.getItem('token')
// 2.想要访问 “后台主页” 且 token 值不存在
  if (to.path === '/main' && !tokenStr) {
  	// 3.1 不允许跳转
	// next(false)
	// 3.2 强行跳转到 “登录页面”
    next('/login')
  } else {
    // 3.3 直接放行,允许访问 “后台主页” 
    next()
  }
})

三十四、第六个案例——后台管理案例

1.案例效果

2.案例用到的知识点

  • 命名路由
  • 路由重定向
  • 导航守卫
  • 嵌套路由
  • 动态路由匹配
  • 编程式导航

3.具体实现

  1. 安装并配置 vue-router 4.x

  2. 展示 Login.vue 登录组件

  3. 模拟并实现登录功能

  4. 通过路由渲染 Home.vue

  5. 实现退出登录的功能

  6. 全局控制路由的访问权限

  7. 将左侧菜单改造为路由链接

  8. 渲染用户管理页面的数据

  9. 实现跳转到用户详情页的功能

  10. 开启路由的 props 传参

  11. 通过编程式导航实现后退功能

3.1 安装并配置 vue-router 4.x

1.运行如下的安装,安装 vue-router :

npm i vue-router@next -S

2.在 src 目录下新建 router.js 路由模块:

// 1. 按需导入对应的函数
import { createRouter, createWebHashHistory } from 'vue-router'

// 2. 创建路由对象
const router = createRouter({
	history: createWebHashHistory(),
	routes: [],
})

// 3. 向外共享路由实例对象
export default router

3.在 main.js 入口文件中导入并挂载路由对象:

// 1.导入路由模块
import router from './router'

const app = createApp(App)

// 2.挂载路由对象
app.use(router)

app.mount('#app')
3.2 展示 Login.vue 登录组件

1.在 router.js 模块中导入 Login.vue 组件:

import Login from './components/MyLogin.vue'

2.声明路由规则如下:

routes: [
    // 路由重定向
    { path: '/', redirect: '/login' },
    { path: '/login', component: Login },
]

3.在 App.vue 组件中声明路由占位符

<template>
    <!-- 路由的占位符 -->
    <router-view></router-view>
</template>

<script>
export default {
	name: 'MyApp',
}
</script>

<style lang="less" scoped>
</style>
3.3 模拟并实现登录功能

1.在 MyLogin.vue 组件中声明如下的 data 数据:

data() {
	return {
		username: '',
		password: '',
	}
}

2.为用户名密码的文本框进行 v-model 双向数据绑定:

<!-- 登录名称 -->
<div class="form-group form-inline">
    <label for="username">登录名称</label>
    <input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" autocomplete="off" v-model="username">
</div>

<!-- 登录密码 -->
<div class="form-group form-inline">
    <label for="password">登录密码</label>
    <input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model="password">
</div>

3.为 登录按钮 绑定点击事件处理函数:

<button type="button" class="btn btn-primary" @click="onLoginClick">登录</button>

4.在 methods 中声明 onLoginClick 事件处理函数如下:

methods: {
    onLoginClick() {
        // 判断用户名和密码是否正确
        if (this.username === 'admin' && this.password === '123456') {
            // 登录成功,跳转到后台主页
            this.$router.push('/home')
            // 模拟存储 Token 的操作
            return localStorage.setItem('token', 'Bearer xxx')
        }
        // 登录失败,清除 Token
        localStorage.removeItem('token')
    },
},
3.4 通过路由渲染 Home.vue

1.在 router.js 中导入 Home.vue 组件:

import Home from './components/MyHome.vue'

2.在 routes 路由规则的数组中,声明对应的路由规则:

routes: [
    { path: '/', redirect: '/login' },
    { path: '/login', component: Login },
    // Home 组件的路由规则
    { path: '/home', component: Home },
]

3.渲染 Home.vue 组件的基本结构:

<template>
    <div class="home-container">
    	<!-- 头部组件 -->
    	<my-header></my-header>
    	<!-- 主体区域 -->
    	<div class="home-main-box">
            <!-- 左侧边栏区域 -->
            <my-aside></my-aside>
            <!-- 右侧内容主体区域 -->
            <div class="home-main-body"></div>
       </div>
    </div>
</template>
3.5 实现退出登录的功能

1.在 MyHeader.vue 组件中,为 退出登录 按钮绑定 click 事件处理函数:


2.在 methods 中声明如下的事件处理函数:


3.6 全局控制路由的访问权限

1.在 router.js 模块中,通过 router 路由实例对象,全局挂载路由导航守卫:

// 全局路由导航守卫
router.beforeEach((to, from, next) => {
    // 如果用户访问的是登录页面,直接放行
    if (to.path === '/login') return next()
    // 获取 Token 值
    const token = localStorage.getItem('token')
    if (!token) {
        // Token 值不存在,强制跳转到登录页面
        return next('/login')
    }
    // 存在 Token 值,直接放行
    next()
})
3.7 将左侧菜单改造为路由链接

1.打开 MyAside.vue 组件,把 li 内部的纯文本升级改造为 组件:

<template>
    <div class="layout-aside-container">
        <!-- 左侧边栏列表 -->
        <ul class="user-select-none menu">
            <li class="menu-item">
            	<router-link to="/home/users">用户管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/rights">权限管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/goods">商品管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/orders">订单管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/settings">系统设置</router-link>
        	</li>
        </ul>
    </div>
</template>

2.打开 Home.vue 组件,在 右侧内容主体区域 中声明子路由的占位符:

<template>
    <div class="home-container">
        <!-- 头部组件 -->
        <my-header></my-header>
        <!-- 主体区域 -->
        <div class="home-main-box">
            <!-- 左侧边栏区域 -->
            <my-aside></my-aside>
            <!-- 右侧内容主体区域 -->
            <div class="home-main-body">
                <!-- **子路由的占位符** -->
                <router-view></router-view>
            </div>
        </div>
    </div>
</template>

3.在 router.js 中导入左侧菜单对应的组件:

import Users from './components/menus/MyUsers.vue'
import Rights from './components/menus/MyRights.vue'
import Goods from './components/menus/MyGoods.vue'
import Orders from './components/menus/MyOrders.vue'
import Settings from './components/menus/MySettings.vue'

4.通过 children 属性,为 home 规则定义子路由规则如下:

{
    path: '/home',
    component: Home,
    // 用户访问 /home 时,重定向到 /home/users
    redirect: '/home/users',
    // 子路由规则
    children: [
        { path: 'users', component: Users },
        { path: 'rights', component: Rights },
        { path: 'goods', component: Goods },
        { path: 'orders', component: Orders },
        { path: 'settings', component: Settings },
    ],
},
3.8 渲染用户管理页面的数据

1.在 MyUsers.vue 组件中,通过 v-for 指令循环渲染用户列表的数据:


3.9 实现跳转到用户详细页的功能

1.在 MyUsers.vue 组件中,渲染详情页的路由链接如下:

<td>
	<router-link :to="'/home/users/' + item.id">详情</router-link>
</td>

2.在 router.js 中导入用户详情页组件:

import UserDetail from './components/user/MyUserDetail.vue'

3.在 home 规则的 children 节点下,声明 用户详情页 的路由规则:

{
    path: '/home',
    component: Home,
    redirect: '/home/users',
    children: [
        { path: 'users', component: Users },
        { path: 'rights', component: Rights },
        { path: 'goods', component: Goods },
        { path: 'orders', component: Orders },
        { path: 'settings', component: Settings },
        // 用户详情页的路由规则
        { path: 'users/:id', component: UserDetail },
    ],
},
3.10 开启路由的 props 传参

1.在 router.js 模块中,为 用户详情页 的路由规则开启 props 传参:

{ path: 'users/:id', component: UserDetail, props: true },

2.在 MyUserDetail.vue 组件中声明 props 参数:

export default {
    name: 'MyUserDetail',
    props: ['id'],
}

3.在 MyUserDetail.vue 组件的结构中直接使用路由参数:

<template>
    <button type="button" class="btn btn-light btn-sm">后退</button>
    <h4 class="text-center">用户详情 --- {{id}}</h4>
</template>
3.11 通过编程式导航实现后退功能

1.在 MyUserDetail.vue 组件中,为后退按钮绑定点击事件处理函数:

<template>
    <button type="button" class="btn btn-light btn-sm" @click="goBack">后退</button>
    <h4 class="text-center">用户详情 --- {{id}}</h4>
</template>

2.在 methods 中声明 goBack 事件处理函数如下:

export default {
    name: 'MyUserDetail',
    props: ['id'],
    methods: {
        // 编程式导航实现后退功能
        goBack() {
            this.$router.go(-1)
        },
    },
}

三十五、路由总结

① 能够知道如何在 vue 中配置路由

  • createRouter、app.use(router)

② 能够知道如何使用嵌套路由

  • 通过 children 属性进行路由嵌套、子路由的 hash 地址不要以 / 开头

③ 能够知道如何实现动态路由匹配

  • 使用冒号声明参数项、this.$route.params、props: true

④ 能够知道如何使用编程式导航

  • this. r o u t e r . p u s h 、 t h i s . router.push、this. router.pushthis.router.go(-1)

⑤ 能够知道如何使用全局导航守卫

  • 路由实例.beforeEach((to, from, next) => { })
3.3 模拟并实现登录功能

1.在 MyLogin.vue 组件中声明如下的 data 数据:

data() {
	return {
		username: '',
		password: '',
	}
}

2.为用户名密码的文本框进行 v-model 双向数据绑定:

<!-- 登录名称 -->
<div class="form-group form-inline">
    <label for="username">登录名称</label>
    <input type="text" class="form-control ml-2" id="username" placeholder="请输入登录名称" autocomplete="off" v-model="username">
</div>

<!-- 登录密码 -->
<div class="form-group form-inline">
    <label for="password">登录密码</label>
    <input type="password" class="form-control ml-2" id="password" placeholder="请输入登录密码" v-model="password">
</div>

3.为 登录按钮 绑定点击事件处理函数:

<button type="button" class="btn btn-primary" @click="onLoginClick">登录</button>

4.在 methods 中声明 onLoginClick 事件处理函数如下:

methods: {
    onLoginClick() {
        // 判断用户名和密码是否正确
        if (this.username === 'admin' && this.password === '123456') {
            // 登录成功,跳转到后台主页
            this.$router.push('/home')
            // 模拟存储 Token 的操作
            return localStorage.setItem('token', 'Bearer xxx')
        }
        // 登录失败,清除 Token
        localStorage.removeItem('token')
    },
},
3.4 通过路由渲染 Home.vue

1.在 router.js 中导入 Home.vue 组件:

import Home from './components/MyHome.vue'

2.在 routes 路由规则的数组中,声明对应的路由规则:

routes: [
    { path: '/', redirect: '/login' },
    { path: '/login', component: Login },
    // Home 组件的路由规则
    { path: '/home', component: Home },
]

3.渲染 Home.vue 组件的基本结构:

<template>
    <div class="home-container">
    	<!-- 头部组件 -->
    	<my-header></my-header>
    	<!-- 主体区域 -->
    	<div class="home-main-box">
            <!-- 左侧边栏区域 -->
            <my-aside></my-aside>
            <!-- 右侧内容主体区域 -->
            <div class="home-main-body"></div>
       </div>
    </div>
</template>
3.5 实现退出登录的功能

1.在 MyHeader.vue 组件中,为 退出登录 按钮绑定 click 事件处理函数:


2.在 methods 中声明如下的事件处理函数:


3.6 全局控制路由的访问权限

1.在 router.js 模块中,通过 router 路由实例对象,全局挂载路由导航守卫:

// 全局路由导航守卫
router.beforeEach((to, from, next) => {
    // 如果用户访问的是登录页面,直接放行
    if (to.path === '/login') return next()
    // 获取 Token 值
    const token = localStorage.getItem('token')
    if (!token) {
        // Token 值不存在,强制跳转到登录页面
        return next('/login')
    }
    // 存在 Token 值,直接放行
    next()
})
3.7 将左侧菜单改造为路由链接

1.打开 MyAside.vue 组件,把 li 内部的纯文本升级改造为 组件:

<template>
    <div class="layout-aside-container">
        <!-- 左侧边栏列表 -->
        <ul class="user-select-none menu">
            <li class="menu-item">
            	<router-link to="/home/users">用户管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/rights">权限管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/goods">商品管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/orders">订单管理</router-link>
            </li>
            <li class="menu-item">
            	<router-link to="/home/settings">系统设置</router-link>
        	</li>
        </ul>
    </div>
</template>

2.打开 Home.vue 组件,在 右侧内容主体区域 中声明子路由的占位符:

<template>
    <div class="home-container">
        <!-- 头部组件 -->
        <my-header></my-header>
        <!-- 主体区域 -->
        <div class="home-main-box">
            <!-- 左侧边栏区域 -->
            <my-aside></my-aside>
            <!-- 右侧内容主体区域 -->
            <div class="home-main-body">
                <!-- **子路由的占位符** -->
                <router-view></router-view>
            </div>
        </div>
    </div>
</template>

3.在 router.js 中导入左侧菜单对应的组件:

import Users from './components/menus/MyUsers.vue'
import Rights from './components/menus/MyRights.vue'
import Goods from './components/menus/MyGoods.vue'
import Orders from './components/menus/MyOrders.vue'
import Settings from './components/menus/MySettings.vue'

4.通过 children 属性,为 home 规则定义子路由规则如下:

{
    path: '/home',
    component: Home,
    // 用户访问 /home 时,重定向到 /home/users
    redirect: '/home/users',
    // 子路由规则
    children: [
        { path: 'users', component: Users },
        { path: 'rights', component: Rights },
        { path: 'goods', component: Goods },
        { path: 'orders', component: Orders },
        { path: 'settings', component: Settings },
    ],
},
3.8 渲染用户管理页面的数据

1.在 MyUsers.vue 组件中,通过 v-for 指令循环渲染用户列表的数据:


3.9 实现跳转到用户详细页的功能

1.在 MyUsers.vue 组件中,渲染详情页的路由链接如下:

<td>
	<router-link :to="'/home/users/' + item.id">详情</router-link>
</td>

2.在 router.js 中导入用户详情页组件:

import UserDetail from './components/user/MyUserDetail.vue'

3.在 home 规则的 children 节点下,声明 用户详情页 的路由规则:

{
    path: '/home',
    component: Home,
    redirect: '/home/users',
    children: [
        { path: 'users', component: Users },
        { path: 'rights', component: Rights },
        { path: 'goods', component: Goods },
        { path: 'orders', component: Orders },
        { path: 'settings', component: Settings },
        // 用户详情页的路由规则
        { path: 'users/:id', component: UserDetail },
    ],
},
3.10 开启路由的 props 传参

1.在 router.js 模块中,为 用户详情页 的路由规则开启 props 传参:

{ path: 'users/:id', component: UserDetail, props: true },

2.在 MyUserDetail.vue 组件中声明 props 参数:

export default {
    name: 'MyUserDetail',
    props: ['id'],
}

3.在 MyUserDetail.vue 组件的结构中直接使用路由参数:

<template>
    <button type="button" class="btn btn-light btn-sm">后退</button>
    <h4 class="text-center">用户详情 --- {{id}}</h4>
</template>
3.11 通过编程式导航实现后退功能

1.在 MyUserDetail.vue 组件中,为后退按钮绑定点击事件处理函数:

<template>
    <button type="button" class="btn btn-light btn-sm" @click="goBack">后退</button>
    <h4 class="text-center">用户详情 --- {{id}}</h4>
</template>

2.在 methods 中声明 goBack 事件处理函数如下:

export default {
    name: 'MyUserDetail',
    props: ['id'],
    methods: {
        // 编程式导航实现后退功能
        goBack() {
            this.$router.go(-1)
        },
    },
}

三十五、路由总结

① 能够知道如何在 vue 中配置路由

  • createRouter、app.use(router)

② 能够知道如何使用嵌套路由

  • 通过 children 属性进行路由嵌套、子路由的 hash 地址不要以 / 开头

③ 能够知道如何实现动态路由匹配

  • 使用冒号声明参数项、this.$route.params、props: true

④ 能够知道如何使用编程式导航

  • this. r o u t e r . p u s h 、 t h i s . router.push、this. router.pushthis.router.go(-1)

⑤ 能够知道如何使用全局导航守卫

  • 路由实例.beforeEach((to, from, next) => { })
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IndulgeBack

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值