《Vue入门到精通》最强Vue教程,附带经典案例,万字详解,干货十足!

目录:

一、前言

在初步学习前端课程的时候,相信大家都知道Vue这个名字,至于Vue到底是什么却不得而知,又或者知道Vue是前端的一大流行框架,那么它到底是用来干啥的呢,对于其他框架而言Vue又有什么优势和特点,学习前端的人为什么都要去学习Vue呢?

对于这些答案,相信大家都想去亲自揭开它的谜底,但是如果你未学过JavaScriptjQuery那么,请先不要学习Vue,因为没有根基是非常晦涩难学的。反之学习的时候会特别轻松,所以希望大家学习Vue的时候,尽量都有javascriptjQuery库类的基础。

二、Vue是什么?

1、简介

在这里插入图片描述

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

什么是渐进式框架

说白了,就是框架分层。那是如何分层的呢?就像《功夫》里面黄圣依手里拿的棒棒糖一样:最核心的是视图层渲染,然后往外是组件机制,在此基础上再加入路由机制,再加入状态管理,最外层是构建工具,vue和react都是如此。

Vue分层:声明式渲染>组件系统>客户端路由>集中式状态管理>项目构建

2、发展史

创始人:尤雨溪

照 片:在这里插入图片描述

官 网:https://cn.vuejs.org/

安 装:https://cn.vuejs.org/v2/guide/installation.html =》点击开发版本

  1. 2013年,在 Google 工作的尤雨溪,受到 Angular 的启发,开发出了一款轻量框架,最初命名为 Seed 。
  2. 2013年12月,更名为 Vue,图标颜色是代表勃勃生机的绿色,版本号是 0.6.0。
  3. 2014.01.24,Vue 正式对外发布,版本号是 0.8.0。
  4. 2014.02.25,0.9.0 发布,有了自己的代号:Animatrix,此后,重要的版本都会有自己的代号。
  5. 2015.06.13,0.12.0,代号Dragon Ball,Laravel 社区(一款流行的 PHP 框架的社区)首次使用 Vue,Vue 在 JS 社区也打响了知名度。
  6. 2015.10.26,1.0.0 Evangelion 是 Vue 历史上的第一个里程碑。同年,vue-router、vuex、vue-cli 相继发布,标志着 Vue从一个视图层库发展为一个渐进式框架。
  7. 2016.10.01,2.0.0 是第二个重要的里程碑,它吸收了 React 的虚拟 Dom 方案,还支持服务端渲染。自从Vue 2.0 发布之后,Vue 就成了前端领域的热门话题。
  8. 2019.02.05,Vue 发布了 2.6.0 ,这是一个承前启后的版本,在它之后,将推出 3.0.0。
  9. 2019.12.05,在万众期待中,尤雨溪公布了 Vue 3 源代码,目前 Vue 3 处于 Alpha 版本。

3、为什么要学习Vue?

  1. 易用:熟悉 HTML 、 CSS 、 JavaScript 知识后,可快速上手 Vue
  2. 灵活:在一个库和一套完整框架之间自如伸缩
  3. 高效: 20kB 运行大小,超快虚拟 DOM

4、jQuery、javascript、Vue的区别

  1. Vue是框架而jQuery顶多算个库类

    ​ 在Vue还未诞生之前,jQuey可以说是一个前端的流行框架,操作DOM元素非常方便,缺点是要大量的获取和操作DOM元素。在 React、Vue、Angular前端三大主流框架出现后,jQuey就被降级了,所以顶多算个库类。而在使用Vue等其他主流框架的时候,我们不需要一直关注DOM元素,因为在Vue中DOM是虚拟的,我们不需要去获取,这样就减轻了前端开发人员的工作量。

  2. 前端渲染方式

    在这里插入图片描述

    • 原生js字符串拼接ES6新语法……等,显得极为麻烦
    • 使用 template-web.js 模板引擎,但没有提供专门的事件机制
    • Vue提供了一套极为标准的模板,包括插值、指令、事件、属性、样式、分支循环……等,可以说我们只使用Vue就可以搭建并完成一整套的网页应用

5、Vue框架构造

Vue程序结构框架

在这里插入图片描述

Vue.js是典型的MVVM框架,什么是MVVM框架,介绍之前我们先介绍下什么是MVC框架

MVC是后端分层开发的思想 即 Model-View-Controller 的缩写,就是 模型-视图-控制器 , 也就是说一个标准的Web 应用程序是由这三部分组成的:

View 用来把数据以某种方式呈现给用户。

Model 其实就是数据。

Controller 接收并处理来自用户的请求,并将 Model 返回给用户。

MVC框架对于简单的应用处理是可以的,也符合软件架构的分层思想。但随着H5 的不断发展,人们更希望使用H5 开发的应用能和Native 媲美,或者接近于原生App 的体验效果,于是前端应用的复杂程度已不同往日,今非昔比。这时前端开发就暴露出了三个痛点问题:

  • 开发者在代码中大量调用相同的 DOM API, 处理繁琐 ,操作冗余,使得代码难以维护。
  • 大量的DOM 操作使页面渲染性能降低,加载速度变慢,影响用户体验。
  • 当 Model 频繁发生变化,开发者需要主动更新到View ;当用户的操作导致 Model 发生变化,开发者同样需要将变化的数据同步到Model 中,这样的工作不仅繁琐,而且很难维护复杂多变的数据状态。

其实,早期jquery 的出现就是为了前端能更简洁的操作DOM 而设计的,但它只解决了第一个问题,另外两个问题始终伴随着前端一直存在。随着智能手机,平板电脑的流行,多终端开始流行。

MVVM框架开始流行,应用场景:

  • 针对具有复杂交互逻辑的前端应用
  • 提供基础的架构抽象
  • 提供ajax数据持久化,保证前端用户体验

在这里插入图片描述

框架数据分层情况

  • M-Model:数据模型层,可以在Model中定义数据修改和操作的业务逻辑, Vue中数据层都放在data里面

  • V-View:视图层,它负责将数据模型转化成UI 展现出来

  • VM-ViewModel:是控制器,将视图和数据之间建立联系,或者说是一个同步视图和数据的对象,即Vue对象

三、Vue的使用

1、Vue环境搭建

  1. 引入Vue.js
  2. 给HTML元素即视图添加id属性
  3. 创建Vue对象,并搭建Vue环境
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue差值表达式</title>
</head>
<body>
    <div id="app"></div>
  	// 导入vue
    <script src="./js/vue.js"></script>
    <script>
      // 创建Vue环境
        let vm = new Vue({
        		// 将该DOM元素内部搭建为Vue环境  
            el: "#app",
          	// 模型数据
            data: {
                name: "张三",
                sex: "男"
            }
        })
    </script>
</body>
</html>

注意❗

  • el 元素的挂载位置(值可以是 CSS 选择器或者 DOM 元素),但一般都使用CSS选择器
  • data :模型数据(值必须是一个对象)

2、插值表达式

template-web.js模板引擎绑定数据的语法类似,都是使用双大括号的文本插值<span>{{ msg }}</span>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue差值表达式</title>
</head>
<body>
    <div id="app">
    		// 从data模型数据中获取
        <div>姓名:{{name}}</div>
        <div>性别:{{sex}}</div>
    </div>
    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
          	// 模型数据
            data: {
                name: "张三",
                sex: "男"
            }
        })
    </script>
</body>
</html>

注意❗

  1. 插值可以使用变量名的形式
  2. 也可以使用表达式的形式,即对这个数据进行一些了的操作,假如这个数据是个数组,那么就可以在插值的地方进行数组的分割、合并……等操作
  3. 在使用插值表达式后,会出现数据闪动的情况,不过可以使用指令的方式修复
  4. 当然也可以不使用插值表达式的形式进行绑定数据,换成使用指令

3、指令(Directives)

1、什么是指令
  • 指令是一个带有v-前缀的特殊属性,那么指令的本质就是自定义属性
  • 指令的格式,以v-开头
  • 指令可带参数,也可不带参数,比如v-cloak指令就属于无参数的指令
2、指令API
1、v-cloak 解决插值闪动
  • 不需要表达式

  • 用法

    ​ 这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。

  • 功能:解决插值表达式存在的闪动问题

  • 原理:先隐藏,替换好值之后,再显示最终的值

  • 示例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Vue差值表达式</title>
        <style>
            /* 1.通过属性选择器,选择到带有v-cloak的元素,让其隐藏 */
            
            [v-cloak] {
                display: none;
            }
        </style>
    </head>
    
    <body>
        <div id="msg">
            <!-- 2.让带有差值语法的元素添加上v-cloak属性
                在数据渲染完成之后v-cloak属性会被自动移除,
                v-cloak一旦移除就相当于没有这个属性,属性选择器也就不会再选择到该标签
             -->
            <div v-cloak>姓名:{{name}}</div>
            <div v-cloak>性别:{{sex}}</div>
        </div>
    
        <script src="./js/vue.js"></script>
        <script>
            let vm = new Vue({
                el: "#msg",
                data: {
                    name: "张三",
                    sex: "男"
                }
            })
        </script>
    </body>
    
    </html>
    
2、v-text 填充纯文本
  • 预期string

  • 详细

    更新元素的 文本内容。如果要更新部分的 文本内容,则需要使用 {{ Mustache }} 插值表达式。

  • 示例

    <!-- 更新元素的文本内容 -->
    <span v-text="msg"></span>
    <!-- 插值表达式 -->
    <span>{{msg}}</span>
    
  • 注意

    1. 相比插值表达式更加简洁
    2. 指令用于将数据填充到标签中,作用与差值表达式类似,但是其直接没有闪动
    3. 如果数据中有html标签则会将html标签一并输出
    4. 此处为单向绑定,数据对象上的值改变,差值会改变,但是差值改变并不会影响数据对象的改变
3、v-html :填充HTML片段
  • 预期string

  • 详细

    更新元素的 innerHTML注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译,并且可识别HTML标签

  • 注意

    1. 存在安全问题,一般只在可信任内容上使用v-html。永远不要在用户提交的内容上使用
    2. 和v-text区别在于v-text输出的是纯文本内容,而v-html会对内容进行html解析然后输出
    3. 本网站内部数据可以使用,来自第三方的数据不可以用
  • 示例

    <!--html是data中的属性,并且该属性可以带有html标签,可识别 -->
    <div v-html="html"></div>
    
4、v-pre 填充原始信息
  • 不需要表达式

  • 用法

    跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。

  • 注意

    1. 显示原始信息,跳过vue编译过程(分析编译过程)
    2. 一些静态的内容不需要编译加这个指令可以加快渲染效果
  • 示例

    <!-- 插值中的数据不会被编译,但是HTML标签会识别 -->
    <span v-pre>{{ 这将不会被编译 }}</span>
    
5、v-once 只编译一次
  • 不需要表达式

  • 详细

    只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

  • 注意

    1. 显示内容之后不再具有数据响应式功能
    2. 什么是数据响应式
      • HTML响应式即媒体查询,屏幕尺寸的变化导致样式的改变
      • Vue数据响应式则是数据的变化导致页面内容的变化
  • 示例

    <!-- 单个元素 -->
    <span v-once>这永远不会改变: {{msg}}</span>
    <!-- 有子元素 -->
    <div v-once>
      <h1>comment</h1>
      <p>{{msg}}</p>
    </div>
    <!-- 组件 -->
    <my-component v-once :comment="msg"></my-component>
    <!-- `v-for` 指令-->
    <ul>
      <li v-for="i in list" v-once>{{i}}</li>
    </ul>
    
  • 参考

6、v-model 双向数据绑定
  • 预期:随表单控件类型不同而不同。

  • 限制

    • <input>
    • <select>
    • <textarea>
    • components
  • 修饰符

    • .lazy - 取代 input 监听 change 事件
    • .number - 输入字符串转为有效的数字
    • .trim - 输入首尾空格过滤
  • 用法

    1. 在表单控件或者组件上创建双向绑定
    2. 什么是双向数据绑定
      • 当数据对象发生改变的时候,视图也会跟着发生改变
      • 当视图发生改变的时候,数据对象也会跟着发生改变
  • 示例

     <div id="app">
       			<!-- 改变后div中的内容也随之发生改变 -->
            <div v-model>{{msg}}</div>
       			<!-- 用户可在input输入框中输入内容来改变msg的值,不输入的话则是msg默认的值 -->
            <input type="text" v-model="msg">
        </div>
        <script src="./js/vue.js"></script>
        <script>
            let vm = new Vue({
                el: "#app",
                data: {
                    msg: "你好世界"
                }
            })
        </script>
    
7、v-on 事件绑定
  • 预期Function | 内联声明 | Object

  • 参数event

  • 修饰符

    • .stop - 调用 event.stopPropagation()
    • .prevent - 调用 event.preventDefault()
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
    • .native - 监听组件根元素的原生事件。
    • .once - 只触发一次回调。
    • .left - (2.2.0) 只当点击鼠标左键时触发。
    • .right - (2.2.0) 只当点击鼠标右键时触发。
    • .middle - (2.2.0) 只当点击鼠标中键时触发。
    • .passive - (2.3.0) 以 { passive: true } 模式添加侦听器
    • .enter - 按下回车键
    • .esc - 按下退出键
  • 自定义事件修饰符

    全局 config.keyCodes 对象,112等是按键对应的ASCII码值

    // shoot实际按修饰父的名称
    Vue.config.keyCodes.shoot = 112
    
  • 用法

    1. 绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字 || 方法调用或一个内联语句,如果没有修饰符也可以省略。
    2. 用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件
    3. 在监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event 属性:v-on:click="handle('ok', $event)"
    4. 2.4.0 开始,v-on 同样支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰器的。
  • 事件对象

    1. 如果事件参数为函数名称,则函数内就默认拥有一个event事件对象,但是函数的形参中则必须写一个事件对象参数event
    2. 如果事件参数为函数调用,并且要传递一个实参,那么最后一个参数可以是事件对象$event,固定语法。那么函数形参就有两个参数,一个是自定义参数,另一个是事件对象
  • 示例

    <!-- 方法处理器 -->
    <button v-on:click="doThis"></button>
    
    <!-- 动态事件 (2.6.0+) -->
    <button v-on:[event]="doThis"></button>
    
    <!-- 内联语句 -->
    <button v-on:click="doThat('hello', $event)"></button>
    
    <!-- 缩写 -->
    <button @click="doThis"></button>
    
    <!-- 动态事件缩写 (2.6.0+) -->
    <button @[event]="doThis"></button>
    
    <!-- 停止冒泡 -->
    <button @click.stop="doThis"></button>
    
    <!-- 阻止默认行为 -->
    <button @click.prevent="doThis"></button>
    
    <!-- 阻止默认行为,没有表达式 -->
    <form @submit.prevent></form>
    
    <!--  串联修饰符 -->
    <button @click.stop.prevent="doThis"></button>
    
    <!-- 键修饰符,键别名 -->
    <input @keyup.enter="onEnter">
    
    <!-- 键修饰符,键代码 -->
    <input @keyup.13="onEnter">
    
    <!-- 点击回调只会触发一次 -->
    <button v-on:click.once="doThis"></button>
    
    <!-- 对象语法 (2.4.0+),绑定多个事件 -->
    <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
    
8、v-bind 属性绑定
  • 缩写:

  • 预期any (with argument) | Object (without argument)

  • 参数attrOrProp (optional)

  • 用法

    1. 动态地绑定一个或多个 属性,或一个组件 prop 到表达式。
    2. 在绑定 classstyle 属性时,支持其它类型的值,如数组或对象。可以通过下面的教程链接查看详情。
    3. 在绑定 prop 时,prop 必须在子组件中声明。
  • 写法

    <!-- 绑定一个 attribute -->
    <img v-bind:src="imageSrc">
    
    <!-- 动态 attribute 名 (2.6.0+) -->
    <button v-bind:[key]="value"></button>
    
    <!-- 缩写 -->
    <img :src="imageSrc">
    
    <!-- 动态 attribute 名缩写 (2.6.0+) -->
    <button :[key]="value"></button>
    
    <!-- 内联字符串拼接 -->
    <img :src="'/path/to/images/' + fileName">
    
    <!-- class 绑定 -->
    <!-- isRed是true,而red是类名 -->
    <div :class="{ red: isRed }"></div>
    <!--数组里边都是类名称-->
    <div :class="[classA, classB]"></div>
    <!-- 上述组合写法 -->
    <div :class="[classA, { classB: isB, classC: isC }]">
    
    <!-- style 绑定 -->
    <!-- fontSize样式名称 -->
    <div :style="{ fontSize: size + 'px' }"></div>
    <!-- 数组中的是样式对象,包括css样式 -->
    <div :style="[styleObjectA, styleObjectB]"></div>
    
  • 示例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>样式的绑定</title>
        <style>
            .big {
                width: 100px;
                height: 100px;
                background-color: pink;
            }
            
            .small {
                border: 1px solid black;
            }
        </style>
    </head>
    
    <body>
        <div id="app">
            <!-- Class绑定 -->
            <!-- 对象方式 -->
            <div v-bind:class="{big:isActive,small:isActive}"></div>
            <!-- 数组方式 -->
            <div :class="[big,small]"></div>
            <!-- style绑定 -->
            <!-- 组合对象方式 -->
            <div v-bind:style="{width:size,height:size,background:activeColor}"></div>
            <!-- 对象方式 -->
            <div :style="myselfStyle"></div>
            <!-- 数组方式 -->
            <div :style="[style01,style02]"></div>
        </div>
    
        <script src="./js/vue.js"></script>
        <script>
            let vm = new Vue({
                el: "#app",
                data: {
                    // 对象绑定的数据
                    isActive: true,
                    // 数组绑定的数据
                    big: "big",
                    small: "small",
                    // style方式绑定的数据
                    size: "100px",
                    activeColor: "green",
                    myselfStyle: {
                        width: "150px",
                        height: "150px",
                        background: "blue"
                    },
                    style01: {
                        width: "100px",
                        height: "100px"
                    },
                    style02: {
                        background: "red"
                    }
                }
            })
        </script>
    </body>
    
    </html>
    
9、v-if 分支结构
v-if
  • 预期any
  • 应用常见
    1. 多个元素通过条件判断展示或者隐藏某个元素,或者多个元素
    2. 进行两个视图之间的切换
  • 用法
    1. 根据表达式的值的 truthiness 来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。如果元素是 <template>,将提出它的内容作为条件块。
    2. 当条件变化时该指令触发过渡效果。
    3. 当和 v-if 一起使用时,v-for 的优先级比 v-if 更高。
v-else
  • 预期:不需要表达式

  • 限制:前一兄弟元素必须有 v-if 或 v-else-if。

  • 用法

  1. 为 v-if 或者 v-else-if 添加“else 块”。
  • 示例

    <div v-if="Math.random() > 0.5">
      Now you see me
    </div>
    <div v-else>
      Now you don't
    </div>
    
v-else-if
  • 类型any

  • 限制:前一兄弟元素必须有 v-ifv-else-if

  • 用法

    表示 v-if 的“else if 块”。可以链式调用。

  • 示例

    <div v-if="type === 'A'">
      A
    </div>
    <div v-else-if="type === 'B'">
      B
    </div>
    <div v-else-if="type === 'C'">
      C
    </div>
    <div v-else>
      Not A/B/C
    </div>
    
10、v-show 元素的显示与隐藏
  • 预期any

  • 用法

    1. 根据表达式之真假值,切换元素的CSS样式的 display 属性。
    2. 当条件变化时该指令触发过渡效果。
  • v-showv-if的区别

    1. v-if 控制元素是否渲染到页面,本质是动态的向DOM内部添加或者删除DOM元素。v-if切换有一个局部编译/卸载的过程,在切换过程中合适地销毁或重建内部的事件及子组件
    2. v-show 控制元素是否显示(已经渲染到了页面)。v-show只编译一次,之后的改变全部都是控制css中的display选项为none或者display
  • 示例

     <li class="li">
       <!-- v-show的值可以是boolean值,为true就显示,反之则隐藏 -->
        <p @click="display" v-show="flag">{{value}}</p>
        <input type="text" v-model="val" v-show="!flag">
     </li>
    
11、v-for 遍历
  • 预期Array | Object | number | string | Iterable (2.6 新增)

  • 用法

    1. 基于源数据多次渲染元素或模板块。此指令之值,必须使用特定语法 变量名 in 表达式,为当前遍历的元素提供别名:

      <div v-for="item in items">
        {{ item.text }}
      </div>
      
    2. 另外也可以为数组索引指定别名 (或者用于对象的键):

      <div v-for="(item, index) in items"></div>
      <div v-for="(val, key) in object"></div>
      <div v-for="(val, name, index) in object"></div>
      
    3. v-for 的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 属性 key 来提供一个排序提示,目的是帮助 Vue 区分不同的元素,从而提高性能

      <div v-for="item in items" :key="item.id">
        {{ item.text }}
      </div>
      
    4. 当和 v-if 一起使用时,v-for 的优先级比 v-if 更高。

12、自定义指令

除了核心功能默认内置的指令 (v-modelv-show),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。例如在表单中获取焦点:

当页面加载时,该元素将获得焦点 (注意:autofocus 在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。现在让我们用指令来实现这个功能:

  • 全局指令

    // 注册一个全局自定义指令 `v-focus`
    Vue.directive('focus', {
      // 当被绑定的元素插入到 DOM 中时……
      inserted: function (el) {
        // 聚焦元素
        el.focus()
      }
    })
    
  • 局部指令

    // 在实例化的Vue对象中选项中写,组件也可以
    directives: {
      // 创建一个局部的自定义组件focus
      focus: {
        // 指令的定义,并获取挂载的元素
        inserted: function (el) {
          // 对元素进行获取焦点
          el.focus()
        }
      }
    }
    

    然后你可以在模板中任何元素上使用新的 v-focus 属性,如下:

    <input v-focus>
    
3、案例
1、计算机案例(实现简单的加法运算)

在这里插入图片描述

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>加法计算器案例</title>
</head>

<body>
    <div id="app">
        <form action="#">
            <h2>简单计算器</h2>
            <div>
                <label for="num1">数值A:</label>
                <input type="text" v-model.trim.number="one" id="num1">
            </div>
            <div>
                <label for="num2">数值B:</label>
                <input type="text" v-model.trim.number="two" id="num2">
            </div>
            <div>
                <input type="button" @click="value" value="计算">
            </div>
        </form>
        <div>
            计算结果:<span v-text="result"></span>
        </div>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                one: 0,
                two: 0,
                result: 0
            },
            methods: {
                value: function() {
                    this.result = this.one + this.two;
                }
            }
        })
    </script>
</body>

</html>
2、原淘宝选项卡案例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>淘宝选项卡</title>
    <style>
        * {
            padding: 0;
            margin: 0;
        }
        
        #app>#img {
            width: 590px;
            height: 470px;
            background-color: #ccc;
            margin: 20px auto;
            display: none;
        }
        
        #app>div img {
            width: 100%;
        }
        
        #app {
            position: relative;
        }
        
        #left,
        #right {
            position: absolute;
            top: 50%;
            transform: translateY(-30px);
            cursor: pointer;
        }
        
        li {
            width: 100px;
            height: 30px;
            line-height: 30px;
            text-align: center;
            color: #fff;
            background-color: pink;
            list-style: none;
            margin-top: 10px;
        }
        
        #left {
            left: 150px;
        }
        
        #right {
            right: 150px;
        }
        
        #app ul li.active,
        #app ol li.active {
            background-color: skyblue;
            border-color: #f1f1f1;
        }
        
        #app div.current {
            display: block;
        }
    </style>
</head>

<body>
    <div id="app">
        <div id="left">
            <ul>
                <li @click="change(index)" :class="currentIndex===index?'active':''" :key="item.id" v-if="item.id<3" v-for="(item,index) in list">{{item.title}}</li>
            </ul>
        </div>
        <div id="img" :style="currentIndex===index?{display:show}:''" v-for="(item,index) in list">
            <img :src="item.src" alt="">
        </div>
        <div id="right">
            <ol>
                <li @click="change(index)" :class="currentIndex===index?'active':''" :key="item.id" v-if="item.id>2" v-for="(item,index) in list">{{item.title}}</li>
            </ol>
        </div>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                // 样式需要属性
                show: "block",
                // 记录索引号
                currentIndex: 0,
                // 数据
                list: [{
                    id: 1,
                    title: "笔记本",
                    src: "./img/1.jpg"
                }, {
                    id: 2,
                    title: "酒水",
                    src: "./img/d.jpg"
                }, {
                    id: 3,
                    title: "手机",
                    src: "./img/q.jpg"
                }, {
                    id: 4,
                    title: "床垫",
                    src: "./img/c.jpg"
                }],
            },
            methods: {
                // 记录索引
                change: function(index) {
                    this.currentIndex = index;
                }
            }
        })
    </script>
</body>

</html>

4、表单输入绑定

你可以用 v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。

v-model 会忽略所有表单元素的 valuecheckedselected 属性 的初始值而总是将 Vue 实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data 选项中声明初始值。

基于Vue的表单操作

  • input 单行文本
  • textarea 多行文本
  • select 下拉多选
  • radio 单选框
  • checkbox 多选框

在这里插入图片描述

(1)单行文本 和 多行文本
  1. 设置一个v-model双向绑定一个值
  2. 不需要使用value属性
(2)单选按钮
  1. 两个或多个单选框需要同时通过v-model双向绑定一个值
  2. 每一个单选框必须要有value属性,并且value值不一样
  3. 当某一个单选框选中的时候v-model会将当前的value值设置为data中的对应的数据
  4. 单选不需要通过name属性进行设置单选关联只要有v-model即可
(3)复选按钮
  1. 复选框需要通过v-model双向绑定一个值
  2. 每一个复选框必须要有value属性,而且value的值不能一样
  3. 当某一个复选框选中的时候v-model会将当前的value值改变data中的数据
  4. 对于复选框的v-model的对应数据必须是数组类型,否则无法实现多选
(4)下拉菜单
  1. 需要给select通过v-model双向绑定一个值
  2. 每一个option必须要有value属性,并且value值不能一样
  3. 当某一个option选中的时候v-model会将当前的value值改变data中的数据
  4. 对应的data中的数据可以是单个数据(单选)也可以是一个数组类型(多选)
示例:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>表单元素</title>
</head>

<body>
    <div id="app">
    <!-- 表单域 -->
        <form action="" @submit.prevent="submit">
            <div>
                昵称:<input type="text" name="" id="" v-model="nick">
            </div>
            <div>
            <!-- 单选框 -->
                性别:<input type="radio" name="" id="man" v-model="sex" value="0"><label for="man"></label>
                <input type="radio" name="" id="woman" v-model="sex" value="1"><label for="woman"></label>
            </div>
            <div>
            <!-- 复选框 -->
                爱好:<input type="checkbox" name="football" id="football" v-model="hobby" value="0"><label for="football">足球</label>
                <input type="checkbox" name="football" id="basketball" v-model="hobby" value="1"><label for="basketball">篮球</label>
                <input type="checkbox" name="football" id="volleyball" v-model="hobby" value="2"><label for="volleyball">排球</label>
            </div>
            <div>
            <!-- 下拉列表 -->
                职业:
                <select name="" id="" v-model="job">
                    <option value="0">教师</option>
                    <option value="1">护士</option>
                    <option value="2">程序员</option>
                    <option value="0">医生</option>
                </select>
            </div>
            <div>
            <!-- 多行文本框 -->
                评价:<textarea name="" id="" cols="30" rows="10" v-model="evaluate"></textarea>
            </div>
            <div>
            <!-- 提交按钮 -->
                <input type="submit" value="提交">
            </div>
        </form>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                nick: "小明",
                // 性别
                sex: "0",
                // 爱好
                hobby: ["0", "1"],
                // 职业
                job: "2",
                evaluate: "请书写评价"
            },
            methods: {
                submit: function() {
                    console.log(this.nick);
                    console.log(this.sex);
                    console.log(this.hobby);
                    console.log(this.job);
                    console.log(this.evaluate);
                }
            }
        });
    </script>
</body>

</html>

5、计算属性(computed)

(1)为什么需要计算属性?

表达式的计算逻辑可能会比较复杂,使用计算属性可以使模板内容更加简洁

(2)计算属性的用法
// 定义计算属性的选项
computed: {
  // reversedMessage计算属性的名称,那接下来这个属性就可以像data中的数据一样直接在模板中使用了
	reversedMessage: function ()
  	// 对Vue内部数据进行一系列的操作
		return this.msg.split ('').reverse().join('')
	}
}
(3)计算属性与方法的区别
  • 计算属性是基于它们的依赖进行缓存的
  • 方法不存在缓存

6、侦听器

(1)为什么需要侦听器?

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

在这里插入图片描述

(2)侦听器的使用
// 注意:firstName和lastName都是已经存在的属性
watch: {
	firstName: function(val){
		// val表示变化之后的值
		this.fullName = val + this.lastName;
	},
	lastName: function(val) {
		this.fullName = this.firstName + val;
	}
}
案例:验证用户名是否存在
  1. 通过 v-model 实现数据绑定
  2. 需要提供提示信息
  3. 需要侦听器监听输入信息的变化
  4. 触发后修改值
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <title>验证用户名是否存在</title>
    <style>
        .green {
            color: green;
        }
        
        .red {
            color: red
        }
    </style>
</head>

<body>

    <div id="app">
        <!-- input双向数据绑定 -->
        用户名:<input v-model="name" v-focus v-on="{focus:getFocus,blur:lostFocus}" type="text" placeholder="请输入昵称">
        // 如果不存在则使用这个
        <span v-text="prompt" v-if="!flag" :class="green"></span>
				// 如果存在则使用这个
        <span v-text="prompt" v-else :class="red"></span>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                // 判断是否失去焦点
                isFocus: true,
                // 获取用户输入的数据
                name: "",
                green: "green",
                red: "red",
                flag: false,
                // 提示信息
                prompt: "",
                // 已经存在的昵称
                listNick: ["御帝哥謌", "孙悟空", "猪八戒", "沙悟净"]
            },
            // 事件函数
            methods: {
                // 失去焦点的事件
                lostFocus: function() {
                    // 修改focus属性
                    this.isFocus = false;
                },
                // 获取焦点的事件
                getFocus: function() {
                    // 修改focus属性
                    this.isFocus = true;
                },
              	// 检查的函数
                checkName: function(val) {
                    // 使用防抖来拿到最后一次输入
                    setTimeout(() => {
                        // 判断失去焦点
                        if (!this.isFocus) {
                          	// 判断是否存在
                            this.flag = this.listNick.some((item, index) => {
                              	// 如果存在
                                if (val === item) {
                                    this.prompt = '用户名已经存在,请更换一个';
                                    return true;
                                }
                            })
                          	// 如果不存在
                            if (!this.flag) {
                                this.prompt = '该用户名可用';
                            }

                        }
                    }, 1000);

                }
            },
            // 局部指令
            directives: {
                // 获取焦点的指令
                focus: {
                    inserted: function(el) {
                        el.focus();
                    }
                }
            },
            // 侦听器
            watch: {
                // val为获取到的值
                name: function(val) {
                    // 当name属性发生变化后,调用该函数
                    return this.checkName(val);
                }
            }
        })
    </script>
</body>

</html>

</html>

7、过滤器

(1)过滤器的作用是什么?

用户输入的一些数据它不符合我们数据的规则,影响视觉,那么就要格式化数据,比如:

  • 将字符串格式化为首字母大写
  • 将日期格式化为指定的格式等

在这里插入图片描述

(2)自定义过滤器
  • 全局过滤器

    // 定义一个全局的过滤器,val就是模板中默认传来的参数
    Vue.filter(('过滤器名称', function(val){
    	//过滤器业务逻辑
    })
    
  • 局部过滤器

    // 定义局部的过滤器,val就是模板中默认传来的参数 
    filters: {
       	 过滤器名称: function(val) {
             //过滤器业务逻辑
         }
     }
    
  • 带参数的过滤器

    ​ 一般情况下,过滤器都会默认自动带一个模板中传来的参数,但有些情况下我们需要再传递一个参数,比如传递一个转换格式字符串,这里以全局过滤器为例

    // 定义一个全局的过滤器,val就是模板中默认传来的参数,而format是人为传递的参数
    Vue.filter(('过滤器名称', function(val,format){
    	//过滤器业务逻辑
    })
    
  • 过滤器的使用

    1. 单个过滤器

      <div>{{msg | 过滤器名称}}</div>
      
    2. 多个过滤器

      <div>{{msg | 过滤器名称 | 过滤器名称(参数)}}</div>
      
案例:使用过滤器格式化日期
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>格式化日期</title>
</head>

<body>
    <div id="app">
        <!-- 对当前日期以"yyyy-MM-dd hh:mm:ss"格式进行过滤 -->
        当前日期:<span>{{time | formatDate("yyyy-MM-dd hh:mm:ss")}}</span>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        let vm = new Vue({
            el: "#app",
            data: {
                // 获取当前日期
                time: new Date()
            },
            filters: {
                formatDate: function(date, format) {
                    if (typeof date === "string") {
                        var mts = date.match(/(\/Date\((\d+)\)\/)/);
                        if (mts && mts.length >= 3) {
                            date = parseInt(mts[2]);
                        }
                    }
                    date = new Date(date);
                    if (!date || date.toUTCString() == "Invalid Date") {
                        return "";
                    }
                    var map = {
                        "M": date.getMonth() + 1, //月份 
                        "d": date.getDate(), //日 
                        "h": date.getHours(), //小时 
                        "m": date.getMinutes(), //分 
                        "s": date.getSeconds(), //秒 
                        "q": Math.floor((date.getMonth() + 3) / 3), //季度 
                        "S": date.getMilliseconds() //毫秒 
                    };

                    format = format.replace(/([yMdhmsqS])+/g, function(all, t) {
                        var v = map[t];
                        if (v !== undefined) {
                            if (all.length > 1) {
                                v = '0' + v;
                                v = v.substr(v.length - 2);
                            }
                            return v;
                        } else if (t === 'y') {
                            return (date.getFullYear() + '').substr(4 - all.length);
                        }
                        return all;
                    });
                    return format;
                }
            }
        })
    </script>
</body>

</html>

8、选项/生命周期钩子函数

所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是因为箭头函数绑定了父上下文(由于this的指向),因此 this 与你期待的 Vue 实例不同,this.fetchTodos 的行为未定义。

生命周期图示

在这里插入图片描述

(1)主要阶段
  • 挂载(初始化相关属性)
    ①beforeCreate
    ②created
    ③beforeMount
    ④mounted
  • 更新(元素或组件的变更操作)
    ①beforeUpdate
    ②updated
  • 销毁(销毁相关属性)
    ①beforeDestroy
    ②destroyed
(2)Vue实例的产生过程
  1. beforeCreate :在实例初始化之后,数据观测 (data observer) 和 event/watcher事件配置之前被调用。
  2. created :在实例创建完成后被立即调用。
  3. beforeMount :在挂载开始之前被调用。
  4. mounted:实例被挂载后调用。这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。
  5. beforeUpdate: 数据更新时调用,发生在虚拟 DOM 打补丁之前。
  6. updated :由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
  7. beforeDestroy: 实例销毁之前调用。
  8. destroyed :实例销毁后调用。

注意❗

  1. 一般情况下我们最注重mounted钩子
  2. 因为此时实例已经创建好了,但是数据还没渲染到模板中
  3. 那么在mounted钩子函数中我们就可以从服务器请求页面数据

9、Vue响应式数据

在vue对象中直接修改data对象属性内的对象或数组的值无法触发响应式(只有数据发生了改变,而页面内容不会发生更改,也就是没有及时的渲染)。而变异方法,可以保持数组方法的原有功能不变的前提并且对其功能进行拓展或删减,或者使用Vue内置的数据响应式方法set。

(1)数组响应式数据
  • 变异方法:修改原有数据
    • push():往数组中的最后面添加一个元素,成功返回当前数组的长度
    • pop():删除数组的最后一个元素,成功返回删除元素的值
    • shift():删除数组的第一个元素,成功返回删除元素的值
    • unshift():往数组的最前面添加一个元素,成功返回当前数组的长度
    • splice():有三个参数,第一个参数是想要删除的元素的下标(必填),第二个参数是想要删除的个数(必填),第三个参数是删除后想要在原位置替换的值(选填)
    • sort():对数组按照字符编码默认从小到大进行排序,成功返回排序后的数组
    • reverse():将数组进行反转,并返回反转之后的数组
  • 非变异方法:不对原有数据操作,而生成新的数据
    • filter():高阶函数,用于将符合条件的元素返回,并生成一个新数组,新数组中的元素是通过检查指定数组中符合条件的所有元素
    • concat():用于将多个数组进行连接,不会改变现有数组
    • slice():可以从已有的数组中返回选定的元素,该方法并不会修改数组,而是返回一个子数组
(2)Vue响应式数据
A、使用
  1. 全局数据响应

    即在没有创建或使用Vue对象,但是导入了Vue.js时使用,所以称之为全局的响应式数据

    Vue.set(target,propertyName/index, value);
    
  2. 局部数据响应

    即在创建并使用了Vue对象时,在Vue方法中就可以使用该方法,我们称之为局部的响应式数据

    vm.$set(target,propertyName/index, value);
    //等价于
    this.$set(target,propertyName/index, value);
    
B、说明
参数说明
target要处理的数组或对象名称
propertyName/index要处理的对象属性或数组索引
value要处理的对象或数组的值
C、注意

Vue响应式数据方法可以实现数据的修改,并且还可以实现添加数据的功能。

D、案例
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数组编译方法</title>
</head>

<body>
    <div id="app">
        <input type="text" v-model="item" @keydown="keydown">
        <button @click="add">提交</button>
        <button @click="amend">修改</button>
        <ul>
            <li v-for="item in list ">{{item}}</li>
        </ul>
    </div>

    <script src="./js/vue.js "></script>
    <script>
        let vm = new Vue({
            el: "#app ",
            data: {
                item: "css",
                list: ["C# ", "HTML ", "Java "]
            },
            methods: {
                // 键盘按下的事件
                keydown: function(e) {
                    // 打印事件对象
                    console.log(e);
                },
                // 添加元素的事件
                add: function() {
                    // 局部数据响应,添加一个数据
                    this.$set(this.list, this.list.length, this.item);
                },
                // 修改元素的事件
                amend: function() {
                    // 全局数据响应,修改一个数据
                    Vue.set(vm.list, 4, "css");
                }
            }
        })
    </script>
</body>

</html>

10、组件

(1)组件是什么?

首先,让我们一起来看一个Vue组件的实例:

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  // 组件数据
  data: function () {
    return {
      count: 0
    }
  },
  // 模板,点击后count++
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

组件是可复用的Vue实例,且带有一个名字:在这个例子中是<button-counter></button-counter>,我们可以在一个通过new Vue创建的Vue根实例中,把这个组件作为自定义元素来使用:

// vue环境
<div id="components-demo">
	// 自定义组件
  <button-counter></button-counter>
</div>

因为组件是可复用的Vue实例,所以它们与new Vue接收相同的选项,例如:datacomputedwatchmethods,以及生命周期钩子函数……等。仅有的例外是el这样跟实例特有的选项。

(2)组件的复用

你可以将组件进行任意多次的复用,因为它们之间的参数是相互独立的,但是内部参数相同。

<div id="components-demo">
  <button-counter></button-counter>
  <button-counter></button-counter>
  <button-counter></button-counter>
</div>

注意:当点击按钮时,每个组件都会各自独立维护它的 count。因为你每用一次组件,就会有一个它的新实例被创建。

(3)data 必须是一个函数

当我们定义这个 <button-counter> 组件时,你可能会发现它的 data 并不是像new Vue中的data这样直接提供一个对象:

data: {
  count: 0
}

取而代之的是,一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝:

data: function () {
  return {
    count: 0
  }
}

如果 Vue 没有这条规则,那么点击一个按钮就可能会出现组件中的数据变化是同步的,因为它们用的是一个数据,而不是相互独立的存在。

(4)组件的组织和注册

通常一个应用会以一棵嵌套的组件树的形式来组织:

在这里插入图片描述

例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。

为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册局部注册

  • 全局组件:全局注册的组件可以用在其被注册之后的任何 (通过 new Vue) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。

    Vue.component(
    	组件名称,
        {
    		data:组件数据(以对象为返回值的函数),
    		template:组件模板内容
        }
    )
    
    //例如 注册一个名为 button-counter 的新组件
    Vue.component('button-counter', {
    	data: function () {
    			return {count: 0}
    		},
    	template: '<button v-on:click="count++"> 点击了 {{ count 次 .</button>
    })
    
  • 局部组件:相反,而局部组件只能在当前Vue实例中使用,所以大多数情况下我们注册的都是全局组件

    // 因为组件内部本身就是以对象形式存在的,那么我们就可以这样定义一个局部的组件,但是必须在Vue环境中使用
    var ComponentA = { /* ... */ }
    var ComponentB = { /* ... */ }
    var ComponentC = { /* ... */ }
    // 当前Vue环境
    new Vue({
    	el: '#app'
      // 局部组件
    	components: {
      	// 使用局部组件
    		'component-a': ComponentA,
    		'component-b': ComponentB,
    		'component-c': ComponentC,
    	}
    })
    
  • 用法

    <div id="app">
      // 使用组件
    	<button-counter></button-counter>
    </div>
    
  • 组件注册注意事项

    1. data必须是一个函数

      分析函数与普通对象的对比

    2. 组件模板内容必须是单个根元素

      分析演示实际的效果

    3. 组件模板内容可以是模板字符串

      模板字符串需要浏览器提供支持( ES6 语法)

    4. 组件命名方式

      短横线方式

      Vue.component('my-component', { /* ... */ })
      

      驼峰方式

      Vue.component('MyComponent ', { /* ... */})
      

      注意:不论使用哪种命名法则,在使用的时候只能使用my-component标签名称,所以我建议大家在使用的时候尽量以短横线的方式进行命名。

(5)组件通信

由于组件间存在数据相互依赖的关系,所以学习Vue组件通信是必须的。比如子组件要使用父组件从服务器获取的数据,并对这个值进行处理在自己的模板中使用。又或者子组件修改了父组件从服务器获取的数据,那么为了实现数据响应,就必须将数据传输给父组件,让父组件进行处理。又或者两个子组件之间相互依赖对方的数据,那又该怎么办?

1、父组件向子组件传值

分析

  1. 首先父组件向子组件传递数据是通过子组件的props属性来实现的,那么也就是说子组件必须要有这个props属性。

    // 定义一个menu-item组件
    Vue.component('menu-item', {
      // props属性用于接收父组件传递的数据,该属性的值为一个数字
    	props: [],
      // 模板要使用父组件传递的数据title
    	template: '<div>{{ title }}</div>'
    })
    
  2. 那么此时父组件通过自定义属性的方式将数据传递给子组件的props属性,那子组件的props属性中当然要有一个变量来接收

    <menu-item title=" 来自父组件的数据 "></menu-item>
    <!-- 父组件通过title自定义属性将自己的数据title传递给子组件的props属性中的title变量 -->
    <menu-item :title="title"></menu-item>
    
  3. 那么此时就可以在子组件的props属性中定义一个包含变量的数组,数组中的值必须为字符串类型。那么数组的元素就是父组件传递过来的值。

    Vue.component('menu-item', {
      // props数组属性中title变量就是父组件通过自定义属性传来的值title
    	props: ['title'],
      // 模板中使用props中父组件传递的值title
    	template: '<div>{{ title }}</div>'
    })
    

注意

  1. props中的变量名称不能和data中的名称相同
  2. props和data中的数据一样都具有缓存,并且在methods函数中一样可以使用this.属性名的方式来获取和使用,而在template模板中与data的使用方式一样,直接书写名称。
  3. props属性名规则
    • 在 props 中使用驼峰形式,HTML模板中需要使用短横线的形式
    • 而在template选项的字符串形式的模板中没有这个限制
2、子组件向父组件传值

分析

  1. 父组件并没有像子组件一样的props属性。而Vue规定可以使用自定义事件,那么我们就可以让父组件自定义一个事件,并监听它的触发。

    <!-- 父组件创建并监听enlarge-text事件,而$event就是子组件传来的值 -->
    <menu-item v-on:enlarge-text='$event'></menu-item>
    <!-- 当然,你也可以简写,因为v-on:就是一个指令,可以简写为@ -->
    <menu-item @enlarge-text='$event'></menu-item>
    
  2. 那么此时子组件就要触发父组件创建并监听的事件,并将要传递的数据加上。

     // 创建全局的组件
    Vue.component('title-get', {
        // 组件数据
        data:function(){
                  return {
                        size: 0.1
                 }
        },
        // 字符串模板
        template: `
    		<!-- 点击按钮后触发click事件,并且触发父组件的enlarge-text事件,并将size数据传递过去,当前你也可以传递任意类型的参数,比如数组,对象……等 -->
        <button v-on:click='$emit("enlarge-text", size) '> 扩大字体 </button>
        `
    });
    
  3. 当子组件触发了父组件监听的事件,那么父组件就可以获取到子组件传递的参数

    <!-- 当子组件触发后,将传递的参数$event追加到父组件的fontSize属性中,当然你也可以直接赋值 -->
    <menu-item v-on:enlarge-text='fontSize+=$event'></menu-item>
    

注意

  1. 当子组件在函数中触发父组件事件的时候,就需要使用this.$emit("自定义事件名称", 参数)的形式,即用this点出来
  2. 子组件在传递参数时可以一次性传递多个参数,比如数组、对象……等,那么父组件就可以通过结构的方式拿到对应的数据
  3. 又或者父组件可以在触发事件后挂载一个函数,将子组件传递的参数$event作为函数的参数传递,那么就可以在父组件的函数中对子组件传递的参数进行一系列的复杂操作
3、兄弟组件传值

在这里插入图片描述

分析

  1. 要想实现兄弟组件间的通信,则必须要有事件中心,这个事件中心所起到的作用就是触发事件后通过事件中心传值

    // 创建一个事件中心
    let eventHub = new Vue();
    
  2. 在子组件创建后,但数据和模板还未进行渲染的情况下,使用事件中心的$on方法创建并监听自定义事件

     // 2.在组件内部使用mounted钩子函数,因为mounted钩子函数就是在组件创建后,但数据和模板未进行渲染的情况下带调用的
    mounted: function() {
        // A组件监听自己的事件,val为触发组件传递的数据
        eventHub.$on("a-event", (val) => {
          
        })
    },
    
  3. 当在某种情况下需要兄弟组件之间通讯时,使用事件中心的$emit()方法触发对方的事件,并将参数传递

    // 事件函数
    methods: {
      	// 定义handle函数
        handle: function() {
            // B组件触发兄弟组件的A的"a-event"事件,并将自己的参数传递
            eventHub.$emit("b-event", this.title)
        }
    }
    
  4. 那么此时兄弟组件监听的事件就会得到数据,并对数据进行操作,如果下次不需要传值了,那么可以在此销毁事件

     // 2.在组件内部使用mounted钩子函数,因为mounted钩子函数就是在组件创建后,但数据和模板未进行渲染的情况下带调用的
    mounted: function() {
        // A组件监听自己的事件,val为触发组件传递的数据
        eventHub.$on("a-event", (val) => {
          // 将接收的参数赋值给当前组件的属性
          this.content = val;
          // 销毁自定义事件
          eventHub.$off("a-event");
        })
    },
    

案例

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>兄弟组件之间传递参数</title>
</head>

<body>
    <div id="app">
        <item-a></item-a>
        <item-b></item-b>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        // 创建一个事件中心
        let eventHub = new Vue();

        // 组件A
        Vue.component(
            "item-a", {
                data: function() {
                    return {
                        title: "春天来了",
                        content: ""
                    }
                },
                template: `
                <div style="background-color:pink;" @click="handle">
                    <p>内容:{{content}}</p>
                </div>
                `,
                // 2.在子组件准备就绪之后,使用事件中心的$on函数创建并监听自定义事件,并接收一个参数
                mounted: function() {
                    // A组件监听自己的事件,如果自己的事件触发了则
                    eventHub.$on("a-event", (val) => {
                        // 将接收的参数赋值给当前组件的属性
                        this.content = val;
                        eventHub.$off("a-event");
                    })
                },
                // 3.如果当前子组件要传递给其他子组件数据,调用该函数
                methods: {
                    handle: function() {
                        // A组件触发兄弟组件的B的"b-event"事件,并将自己的参数传递
                        eventHub.$emit("b-event", this.title)
                    }
                }
            }
        )

        // 组件B
        Vue.component("item-b", {
            data: function() {
                return {
                    title: "",
                    content: "明年春暖花开之日,就是我们见面之时。。。"
                }
            },
            template: `
                <div style="background-color:blue;" @click="handle">
                    <p>标题:{{title}}</p>
                </div>
            `,
            // 在子组件准备就绪之后,使用事件中心的$on函数创建并监听自定义事件,并接收一个参数
            mounted: function() {
                // B组件监听自己的事件,如果自己的事件触发了则
                eventHub.$on("b-event", (val) => {
                    // 将接收的参数赋值给当前组件的属性
                    this.title = val;
                    // 销毁事件
                    eventHub.$off("b-event");
                })
            },
            // 3.如果当前子组件要传递给其他子组件数据,调用该函数
            methods: {
                // B组件触发兄弟组件的A的"b-event"事件,并将自己的参数传递
                handle: function() {
                    eventHub.$emit("a-event", this.content)
                }
            }
        })

        let vm = new Vue({
            el: "#app",
            data: {

            }
        })
    </script>
</body>

</html>

11、插槽

在这里插入图片描述

(1)插槽是什么?

插槽就是子组件中的提供给父组件使用的一个占位符,用<slot></slot> 表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的<slot></slot>标签。

有些时候,我们需要父组件向子组件中填充一些数据,那么假如子组件没有使用插槽,父组件如果需要往子组件中填充模板或者html代码块, 是没办法做到的。

(2)插槽的基本使用
  1. 子组件插槽占位符

    // 全局的子组件alert-box
    Vue.component('alert-box', {
    	template: `<div class="demo-alert-box">
    		<strong>Error!</strong>
    		<!-- 插槽占位符,插槽中可以默认填充数据 -->
    		<slot></slot>
    	</div>`
    })
    
  2. 父组件向插槽中填充内容

    <div id="app">
      	<!-- 父组件向子组件的插槽中传递数据 -->
    		<alert-box>Something bad happened.</alert-box>
    </div>
    
(3)具名插槽
  1. 描述

    ​ 具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中。一个不带 name<slot> 出口会带有隐含的名字“default”。

  2. 使用

    • 子组件的模板

      slot标签一个name属性,那么它就是具名插槽,而不带name属性的插槽则为默认插槽

      <div class="container">
        <header>
          <!-- 具名插槽 -->
          <slot name="header"></slot>
        </header>
        <main>
          <!-- 默认插槽 -->
          <slot></slot>
        </main>
        <footer>
          <!-- 具名插槽 -->
          <slot name="footer"></slot>
        </footer>
      </div>
      
    • 父组件传值

      在给具名插槽传值的时候必须使用template标签,并使用v-slot指令冒号后为插槽名称。而默认插槽则以默认的方式传递,或者以defaulte为name值传递。

      <!-- 使用组件 -->
      <base-layout>
        <!-- 给header插槽传值 -->
        <template v-slot:header>
          <h1>Here might be a page title</h1>
        </template>
      	<!-- 给默认插槽传值 -->
        <p>A paragraph for the main content.</p>
        <p>And another one.</p>
      	<!-- 给footer插槽传值 -->
        <template v-slot:footer>
          <p>Here's some contact info</p>
        </template>
      </base-layout>
      
  3. 注意

    • 父级的填充内容如果指定到子组件的没有对应名字插槽,那么该内容不会被填充到默认插槽中。

    • 如果子组件没有默认插槽,而父级的填充内容指定到默认插槽中,那么该内容就“不会”填充到子组件的任何一个插槽中。

    • 如果子组件有多个默认插槽,而父组件所有指定到默认插槽的填充内容,将“” “全都”填充到子组件的每个默认插槽中。

(4)作用域插槽
  1. 描述

    ​ 作用域插槽其实就是带数据的插槽,即带参数的插槽,简单的来说就是子组件提供给父组件的参数,该参数仅限于插槽中使用,父组件可根据子组件传过来的插槽数据来进行不同的方式展现和填充插槽内容。

  2. 使用

    • 子组件模板

      给插槽上绑定一个属性(包含数据),使得父组件可以获取到。绑定在 <slot> 元素上的 属性 被称为插槽 prop

      <ul>
        <!--for遍历list中的数据-->
      	<li v-for= "item in list" :key= "item.id">
          <!--将每一个迭代的数据对象绑定到插槽上的一个属性中:这样父组件就可以访问到-->
      		<slot v-bind:item="item">
             <!--插槽默认要显示的内容-->
      			{{item.name}}
      		</slot>
      	</li>
      </ul>
      
    • 父组件传值

      现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

      <!--使用子组件,并给子组件传值-->
      <lug-list :list="list">
          <!--slotProps为从插槽中获取到的数据-->
        <template slot-scope="slotProps">
           <!--拿到插槽中的slotProps.info属性的id属性,判断是否为1,如果是则将这个html代码替换到插槽中-->
           <strong v-if='slotProps.info.id==1' class="current">
              {{slotProps.info.name}}
           </strong>
          <!--否则,则将下面这行html代码替换到插槽中-->
           <span v-else>
               {{slotProps.info.name}}
            </span>
         </template>
      </lug-list>
      
  3. 使用场景

    ​ 如果子组件中的某一部分的数据,每个父组件都会有自己的一套对该数据的不同的呈现方式,这时就需要用到作用域插槽。

    ​ 也就是说,要从外部控制模板插槽对应数据的样式的话,就用作用域插槽。

四、Vue Router(路由)

1、什么是路由?

路由是一个比较广义和抽象的概念,路由的本质就是对应关系。而在开发中,路由分为:后端路由前端路由

(1)后端路由
  • 概念:根据不同的用户 URL 请求,返回不同的内容

  • 本质:URL 请求地址与服务器资源之间的对应关系

    在这里插入图片描述

(2)前端路由
  • 概念:根据不同的用户事件,显示不同的页面内容

  • 本质:用户事件与事件处理函数之间的对应关系

    在这里插入图片描述

2、SPA

(1)前后端渲染的缺点
  • 后端渲染:存在性能问题,并且全部代码由服务器生成,浏览器只负责渲染
  • 前端渲染:前端通过Ajax技术从服务器拿到数据,并使用模板引擎将数据填充到页面结构中,最终交由浏览器渲染,并且支持页面的局部刷新。前端渲染提高性能,但是不支持浏览器的前进后退操作。
(2)什么是SPA

SPA(Single Page Application)单页面应用程序:整个网站只有一个页面,内容的变化通过Ajax局部更新实现、同时支持浏览器地址栏的前进和后退操作,并且地址栏会更新url,通过这个url就可以来到这个页面,还会刷新局部的内容

人话:意思就是说前端渲染实现的菜单栏点击后不会局部更新url地址,同时也不会拥有前进和后退的功能,那么前端路由就是为了解决这个问题来诞生的。

(3)实现简易的前端路由

需求:基于UR的hash实现菜单栏(点击菜单的时候改变URL的hash,根据hash的变化控制组件的切换)

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>原生js实现简易前端路由</title>
</head>

<body>
    <div id="app">
        <div>
            <ul>
                <!-- 切换组件的超链接 -->
                <!-- url中的hash -->
                <li><a href="#/my">我的页面</a></li>
                <li><a href="#/you">你的页面</a></li>
                <li><a href="#/your">你们的页面</a></li>
            </ul>
        </div>
        <!--根据:is属性指定的组件名称,把对应的组件渲染到component标签所在的位置 -->
        <!--可以把component标签当做是组件的占位符-->
        <div>
            <component :is="comName"></component>
        </div>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        //定义需要切换的四个组件
        //主页组件
        const my = {
            template: "<h1>我的页面</h1>"
        };
        const you = {
            template: `<h1>你的页面</h1>`
        };
        const your = {
            template: `<h1>你们的页面</h1>`
        };
        // 实例化Vue对象
        var vm = new Vue({
            el: "#app",
            // 默认值为my
            data: {
                // 设置comName属性的值为路由名称
                comName: "my"
            },
            //注册局部组件
            components: {
                my,
                you,
                your
            }
        });
        // 重点是通过window的onhashchange事件来监听浏览器hash值的变化
        window.onhashchange = function() {
            // 截取哈希值,从字符串索引为1的地方
            switch (location.hash.slice(1)) {
                //根据获取到的最新的hash值切换到显示的组件名称
                case "/my":
                    vm.comName = "my"
                    break;
                case "/you":
                    vm.comName = "you"
                    break;
                case "/your":
                    vm.comName = "your"
                    break;
            }
        }
    </script>
</body>

</html>

3、Vue Router

(1)简介

Vue Router(官网:https://router.vuejs.org/zh/)是 Vue.js 官方的路由管理器。 它和 Vue.js 的核心深度集成,可以非常方便的用于SPA应用程序的开发,可以说Vue Router让构建单页面应用变得易如反掌。

Vue Router 包含的功能有:

  1. 支持HTML5 历史模式或 hash 模式
  2. 支持嵌套路由
  3. 支持路由参数
  4. 支持编程式路由
  5. 支持命名路由
(2)基本使用
  1. 引入相关的库文件

    <!-- 导入 vue 文件,为全局 window 对象挂载 Vue 构造函数 --> 
    <script src="./vue.js"></script>
    <!-- 导入 vue-router 文件,为全局 window 对象挂载 VueRouter 构造函数 --> 
    <script src="./vue-router.js"></script>
    
  2. 添加路由链接

    <!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 -->
    <!-- to 属性默认会被渲染为 href 属性 --> 
    <!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 --> 
    <router-link to="/user">User</router-link> 
    <router-link to="/register">Register</router-link>
    
  3. 添加路由填充位

    <!-- 路由填充位(也叫做路由占位符) --> 
    <!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 --> 
    <router-view></router-view>
    
  4. 定义路由组件

    var User = { template: '<div>User</div>' } 
    var Register = { template: '<div>Register</div>' }
    
  5. 配置路由规则并创建路由实例

    // 创建路由实例对象 
    var router = new VueRouter({ 
    	// routes 是路由规则数组 
    	routes: [ 
    		// 每个路由规则都是一个配置对象,其中至少包含path和component 两个属性: 
    		// path 表示当前路由规则匹配的 hash 地址 
    		// component 表示当前路由规则对应要展示的组件
        {path:'/user',component: User}, 				
    		{path:'/register',component: Register} 
    	] 
    })
    
  6. 把路由挂载到 Vue 根实例中

    new Vue({ 
    	el: '#app',
    	// 为了能够让路由规则生效,必须把路由对象挂载到 vue 实例对象上 	
      router 
    });
    

使用 Vue Router 实现简易的菜单:

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vue路由的基本使用</title>
</head>

<body>
    <div id="app">
        <div>
            <ul>
                <li>
                    <!-- 添加路由链接 -->
                    <!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 -->
                    <!-- to 属性默认会被渲染为 href 属性 -->
                    <!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 -->
                    <router-link to="/my">我的</router-link>
                </li>
                <li>
                    <router-link to="/you">你的</router-link>
                </li>
                <li>
                    <router-link to="/your">你们的</router-link>
                </li>
            </ul>
        </div>
        <div>
            <!-- 添加路由填充位(路由占位符) -->
            <!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 -->
            <router-view></router-view>
        </div>
    </div>
    <!-- 1.引入相关的库文件 -->
    <script src="./js/vue.js"></script>
    <script src="./js/vue-router.js"></script>
    <script>
        // 创建并定义路由组件
        let my = {
            template: `<h1>我的主页</h1>`
        };
        let you = {
            template: `<h1>你的page</h1>`
        };
        let your = {
            template: `<h1>你们的page</h1>`
        };
        // 创建路由实例对象
        let router = new VueRouter({
            // 通过routes配置路由规则
            routes: [
                // 每个路由规则都是一个配置对象,其中至少包含 path 和component 两个属性: 
                // path 表示当前路由规则匹配的 hash 地址 
                // component 表示当前路由规则对应要展示的组件 
                {
                    path: "/my",
                    component: my
                }, {
                    path: "/you",
                    component: you
                }, {
                    path: "/your",
                    component: your
                }
            ]
        });
        // 实例化Vue对象
        let vm = new Vue({
            el: "#app",
            // 为了能够让路由规则生效,必须把路由对象挂载到vue实例对象上
            router
        });
    </script>
</body>

</html>
(3)路由重定向

路由重定向指:

  1. 用户在访问地址 A 的时候,强制用户跳转到地址 C ,从而展示特定的组件页面
  2. 通过路由规则的 redirect 属性,指定一个新的路由地址,可以很方便地设置路由的重定向
  3. 就比如访问主页的时候就让他跳转到主页所要展示的内容上,而不是说点击超链接以后才修改路由

使用

// 路由对象
var router = new VueRouter({
	routes: [
	  // 其中,path 表示需要被重定向的原地址,redirect 表示将要被重定向到的新地址
    // 也就是说访问/地址的时候,让其强制跳转到/user地址所对应的路由上
		{path:'/', redirect: '/user'},
		{path:'/user',component: User},
		{path:'/register',component: Register}
	]
})
(4)嵌套路由
1、简介:

嵌套路由是指:

  1. 点击父级路由链接显示模板内容
  2. 模板内容中又有子级路由链接
  3. 点击子级路由链接显示子级模板内容
2、实现:
  1. 父级路由组件模板

    • 父级路由链接

    • 父组件路由填充位

      <p>
        <!-- 父级路由链接 -->
      	<router-link to="/user">User</router-link>
      	<router-link to="/register">Register</router-link>
      </p>
      <div>
      	<!-- 父组件路由填充位,控制组件的显示位置 -->
      	<router-view></router-view>
      </div>
      
  2. 子级路由组件模板

    • 子级路由链接:在父级路由组件的模板中定义

    • 子级路由填充位:在父级路由组件的模板中定义

      const Register = {
      	template: `
      <div>
      		<h1>Register 组件</h1>
      		<hr/>
      		<!-- 子级路由链接 -->
      		<router-link to="/register/tab1">Tab1</router-link>
      		<router-link to="/register/tab2">Tab2</router-link>
      		<!-- 子路由填充位置 -->
      		<router-view/>
      </div>`
      }
      
  3. 嵌套路由配置

    • 父级路由通过children属性配置子级路由
    • 在路由对象的父级路由中配置子级路由
    const router = new VueRouter({ 
    	routes: [ 
    		{ path: '/user', component: User }, 
    		{ path: '/register', component: Register, 
    		// 通过 children 属性,为 /register 添加子路由规则 
    		children: [ { 
    			path: '/register/tab1', component: Tab1 }, 
    			{ path: '/register/tab2', component: Tab2 } 
    		]} 
    	] 
    })
    
(5)动态路由匹配
1、基本用法
  1. 通过动态路由参数的模式进行路由匹配,与后端路由类似,就是给路由传递参数,可以是查询参数,也可以是params参数

    var router = new VueRouter({ 
    	routes: [ 
    		// 动态路径参数 以冒号开头 
    		{ path: '/user/:id', component: User } 
    	]
    })
    
  2. 路由组件模板中通过$route.params获取路由参数

    const User = { 
    	// 路由组件中通过$route.params获取路由参数 
    	template: '<div>User {{ $route.params.id }}</div>' 
    }
    
2、路由组件传递参数

$route与对应路由形成高度耦合,不够灵活,所以可以使用props将组件和路由解耦

  1. props的值为布尔类型

    const router = new VueRouter({
    	routes: [
    		// 如果props被设置为true,那么route.params 将会被设置为组件属性
    		{ path: '/user/:id', component: User, props: true }
    	]
    })
    const User = {
    	props: ['id'], 
    	// 使用 props 接收路由参数
        template: '<div>用户ID:{{ id }}</div>' // 使用路由参数
    }
    
  2. props的值为对象类型

    const router = new VueRouter({ 
        routes: [{ 
            path: '/user/:id', 
            component: User, 
          	// 如果 props 是一个对象,它会被按原样设置为组件属性 
            props: { uname: 'lisi', age: 12 }
        }] 
    }) 
    const User = { 
        props: ['uname', 'age'], 
        template: `<div>用户信息:{{ uname + '---' + age}}</div>' 
    }
    
  3. props的值为函数类型

    const router = new VueRouter({
    		routes: [{ 
          	path: '/user/:id',
    				component: User,
        		// 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参
    				props: route => ({ 
                uname: 'zs', 
                age: 20, 
                id: route.params.id 
          	})
      	}]
    });
    const User = {
    	props: ['uname', 'age', 'id'],
    	template: `<div>用户信息:{{ uname + '---' + age + '---' +'---'id}}</div>'
    }
    
3、vue-router命名路由

简介:为了更加方便的表示路由的路径,可以给路由规则起一个别名,即为“命名路由”。

  1. 在路由对象配置中可以使用name属性给所在的路由起一个名字

    const router = new VueRouter({
    	routes: [{
    		path: '/user/:id',
        // 给所在路由起名字,即为命名式路由
    		name: 'user',
    		component: User
      }]
    })
    
  2. 在路由链接中就可以使用直接使用对象的形式来获取name所代表路由,并传递参数

    <!-- 使用对象中配置name和params来给指定路由传参 -->
    <router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
    
4、编程式导航
(1)页面导航的两种方式
  1. 声明式导航:通过点击链接实现导航的方式,叫做声明式导航

    例如:普通网页中的 < a></a> 链接 或 vue 中的 < router-link>< /router-link>

  2. 编程式导航:通过调用JavaScript形式的API实现导航的方式,叫做编程式导航

    例如:普通网页中的 location.href

(2)常用的编程式导航 API 如下:
  1. this.$router.push('hash地址')
  2. this.$router.go(n)
(3)编程式导航参数规则
// 字符串(路径名称) 
router.push('/home');
// 对象:命名路由 
router.push({ path: '/home' }) 
// 命名的路由(传递参数) 
router.push({ name: '/user', params: { userId: 123 }}) 
// 带查询参数,变成 /register?uname=lisi 
router.push({ path: '/register', query: { uname: 'lisi' }})

五、Vue CLI (脚手架)

1、简介

vue脚手架指的是vue-cli,它是一个专门为单页面应用快速搭建繁杂的脚手架,它可以轻松的创建新的应用程序而且可用于自动生成vue和webpack的项目模板。

2、安装

安装 3.x 版本的 Vue 脚手架:

npm install -g @vue/cli

3、基于3.x版本的脚手架创建vue项目

  • 基于交互式命令行的方式,创建 新版 vue 项目

    vue create my-project
    
  • 基于 图形化界面 的方式,创建 新版 vue 项目

    vue ui
    
  • 基于 2.x 的旧模板,创建 旧版 vue 项目

    npm install -g @vue/cli-init 
    vue init webpack my-project
    

4、Vue 脚手架生成的项目结构分析

在这里插入图片描述

5、Vue 脚手架的自定义配置

  1. 通过 package.json 配置项目

    // 必须是符合规范的json语法
    "vue": {
    	"devServer": {
    		"port": "8888",
    		"open" : true
    	}
    },
    

    注意:不推荐使用这种配置方式。因为 package.json 主要用来管理包的配置信息;为了方便维护,推荐将 vue 脚手架相关的配置,单独定义到 vue.config.js 配置文件中。

  2. 通过单独的配置文件配置项目

    • 在项目的跟目录创建文件 vue.config.js

    • 在该文件中进行相关配置,从而覆盖默认配置

      // vue.config.js 
      module.exports = { 
      	devServer: { 
      		port: 8888 
      	}
      }
      

六、Vux 集中状态管理

1、回顾Vue组件通信

  • 父向子传值:v-bind 属性绑定
  • 子向父传值:v-on 自定义事件绑定
  • 兄弟组件之间共享数据: EventBus(事件中心)
    1. $on 接收数据的那个组件
    2. $emit 发送数据的那个组件

分析:试想一下,如果我们在开发中使用这种方法来共享数据未免有点太麻烦了,是不是影响我们的开发效率,而且代码不容易看懂

2、Vuex 是什么

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

人话:Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。图示如下:

在这里插入图片描述

分析

  • 左图:使用Vue组件通信传递数据,我们发现杂乱无章,不好看懂,数据究竟在哪里?
  • 右图:使用Vuex集中管理数据,我们可以很清楚的看到数据是从STORE(状态)集中管理获取的,当前也可以修改,修改后又返回STORE(状态)中

3、什么样的数据适合存储到Vuex中?

  • 一般情况下,只有组件之间共享的数据,才有必要存储到 vuex 中
  • 而对于组件中的私有数据,依旧存储在组件自身的 data 中即可
  • 比如几个组件的数据之间都互相依赖,那么就可以把这些依赖的数据提取出来统一放到Vuex中进行保管

4、Vuex 的基本使用

  1. 安装 vuex 依赖包

    npm install vuex --save
    
  2. 导入 vuex 包

    import Vuex from 'vuex'
    Vue.use(Vuex)
    
  3. 创建 store 对象

    const store = new Vuex.Store({
    	// state 中存放的就是全局共享的数据
    	state: { count: 0 }
    })
    
  4. 将 store 对象挂载到 vue 实例中

    new Vue({ 
    	el: '#app', 
    	render: h => h(app), 
    	router, 
    	// 将创建的共享数据对象,挂载到 Vue 实例中 
    	// 所有的组件,就可以直接从 store 中获取全局的数据了 
    	store 
    })
    

注意:当然,你也可以使用脚手架使用图形化界面来创建项目,并选择安装Vuex,那么你就不需要配置这些东西,直接使用即可

5、Vuex 的核心概念

(1)State(状态)
  • State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 的 State 中进行存储。

    // 创建store数据源,提供唯一公共数据
    const store = new Vuex.Store({
    	state: { count: 0 }
    })
    
  • 组件访问 State 中的数据

    1. 方法1:this.$store.state.全局数据名称;

    2. 方法2:

      • 按需导入

        // 1. 从 vuex 中按需导入 mapState 函数
        import { mapState } from 'vuex'
        
      • 通过刚才导入的 mapState 函数,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性,那么就可以直接使用

        // 2. 将全局数据,映射为当前组件的计算属性 
        computed: { 
        	...mapState(['count']) 
        }
        
(2)Mutation(变化)
1、简介
  • 作用:Mutation 用于变更 Store中 的数据。
  • 原因
    1. 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据
    2. 通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化
2、使用:

在 mutations 属性中 定义修改状态的方法

const store = new Vuex.Store({ 
	state: { 
		count: 0 
	}, 
	mutations: {
    	// 更改state状态的方法,当然也可以传递其他的参数
    	add(state,...) { 
    		// 变更状态 
    		state.count++ 
    	} 
  } 
})
  • 注意:方法中第一个参数必须是state,因为要对其中的内容进行修改
  • 注意:当然也可以带有其他的由调用时传递的参数
3、在组件中调用mutations中的方法
  • 方法1:this.$store.commit('addN', 参数1...)

    // 触发mutation 
    methods: { 
    	handle2() { 
    		// 在调用 commit 函数, 
    		// 触发 mutations 时携带参数 
    		this.$store.commit('addN', 3) 
    	}
    }
    
  • 方法2:按需导入

    1. 从 vuex 中按需导入 mapMutations 函数

      import { mapMutations } from 'vuex'
      
    2. 通过刚才导入的 mapMutations 函数,将需要的 mutations 函数,映射为当前组件的 methods 方法

      methods: {
      	...mapMutations(['add', 'addN'])
      }
      
  1. 这样你就可以使用this.方法名的方式去调用它了
(3)Action
1、简介

Action 用于处理异步任务,比如计时器……等。如果通过异步操作变更数据,必须通过 Action,而不能使用 Mutation,但是在 Action 中还是要通过触发Mutation 的方式间接变更数据。

人话:意思就是说 Action 用于处理 Mutation 中带有异步事件的方法,或者是如果你想在 Mutation 中定义带有异步事件的方法,那么你就要在Mutation中定义同步方法,然后在Action中使用异步事件调用即可。

2、使用
  1. 在 mutations 属性中 定义修改状态的同步方法

    // 定义 Action
    const store = new Vuex.Store({ 
    	// ...省略其他代码 
    	mutations: { 
    		add(state) { 
    			state.count++ 
    		} 
    	}
    })
    
  2. 在 Action 中定义一个方法,使用异步事件调用 mutations 中的同步方法

    // 定义 Action
    const store = new Vuex.Store({ 
    	// ...省略其他代码 
    	mutations: { 
    		add(state) { 
    			state.count++ 
    		} 
    	}, 
    	actions: { 
    		addAsync(context) { 
    			setTimeout(() => { 
    				context.commit('add') 
    				}, 1000) 
    		} 
    	} 
    })
    
    • 注意:方法中第一个参数必须是context,因为要通过它调用mutations中定义的方法
    • 注意:所有的异步操作必须出现在Action中,而不是mutations
    • 注意:当然,它也可以传递参数,与mutations中大同小异
3、组件中调用
  • 方法1:this.$store.dispatch('addAsync')

  • 方法2:按需导入

    1. 从 vuex 中按需导入 mapActions 函数

      import { mapActions } from 'vuex';
      
    2. 通过刚才导入的 mapActions 函数,将需要的 actions 函数,映射为当前组件的 methods 方法

      methods: {
      	...mapActions(['addASync', 'addNASync'])
      }
      
(4)Getter
1、简介

Getter 用于对 Store 中的数据进行加工处理形成新的数据。

注意

  1. Getter 可以对 Store 中已有的数据加工处理之后形成新的数据,类似 Vue 的计算属性。
  2. Store 中数据发生变化,Getter 的数据也会跟着变化。
2、使用

注意:因为是要对state中的数据进行加工,那么第一个参数必须是state

const store = new Vuex.Store({
	state: {
		count: 0
	},
	getters: {
    // 创建一个方法,对state中的数据进行加工,并返回一个加工后的数据
		showNum: state => {
			return '当前最新的数量是【'+ state.count +'】'
		}
	}
})
3、在组件中使用
  • 在插值表达式中使用:this.$store.getters.名称的方式直接调用

  • 按需导入

    1. 从 vuex 中按需导入 mapGetters 函数

      import { mapGetters } from 'vuex'
      
    2. 通过刚才导入的 mapGetters 函数,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性

      computed: {
          ...mapGetters(['showNum'])
      }
      
    3. 接下来直接使用计算属性的方式调用即可

七、Element-UI

Element-UI:一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。

官网地址:https://element.eleme.cn/#/zh-CN

安装

  • 基于命令行方式手动安装

    • 安装依赖包

      npm i element-ui –S
      
    • ②导入 Element-UI 相关资源

      // 导入组件相关样式
      import 'element-ui/lib/theme-chalk/index.css';
      // 配置 Vue 插件
      Vue.use(ElementUI);
      
  • 基于图形化界面自动安装

    1. 运行 vue ui 命令,打开图形化界面
    2. 通过 Vue 项目管理器,进入具体的项目配置面板
    3. 点击 插件 -> 添加插件,进入插件查询面板
    4. 搜索 vue-cli-plugin-element 并安装
    5. 配置插件,实现按需导入,从而减少打包后项目的体积

八、经典案例

1、图书管理系统

需求:使用Vue指令、计算属性、侦听器、过滤器……等实现简单的图书管理(增删改)功能,并使用文件操作(读写)来存储数据和获取数据。

步骤

  1. 过滤器(格式化日期)
  2. 自定义指令(获取表单焦点)
  3. 计算属性(统计图书数量)
  4. 侦听器(验证图书存在性)
  5. 生命周期(图书数据处理)

效果展示

在这里插入图片描述

源码奉上

  • 前台代码

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>图书列表</title>
        <link rel="stylesheet" href="./css/bootstrap.css">
        <style>
            * {
                padding: 0;
                margin: 0;
            }
            
            #app {
                width: 60%;
                margin: 50px auto;
            }
            
            [v-cloak] {
                display: none;
            }
        </style>
    </head>
    
    <body>
        <div id="app" class="panel panel-default">
            <!-- Default panel contents -->
            <div class="panel-heading">图书管理列表</div>
    
            <!-- 操控区域 -->
            <div class="panel-body form-inline">
                <div class="input-group">
                    <span class="input-group-addon" id="basic-addon1">编号</span>
                    <input type="text" class="form-control" placeholder="请输入编号" :disabled="disable" aria-describedby="basic-addon1" v-model="id" v-focus>
                </div>
                <div class="input-group">
                    <span class="input-group-addon" id="basic-addon1">书名</span>
                    <input type="text" class="form-control" placeholder="请输入书名" aria-describedby="basic-addon1" v-model="name">
                </div>
                <button type="button" class="btn btn-primary" @click="submit">提交</button>
                <span>图书总数:</span><span v-cloak>{{total}}</span>
            </div>
    
            <!-- 表格数据区域 -->
            <table class="table">
                <tr>
                    <th>编号</th>
                    <th>书名</th>
                    <th>时间</th>
                    <th>操作</th>
                </tr>
                <tr :key="item.id" v-for="(item,index) in books">
                    <td v-cloak>{{item.id}}</td>
                    <td v-cloak>{{item.name}}</td>
                    <td v-cloak>{{item.date | format("yyyy-mm-dd hh:mm:ss")}}</td>
                    <td>
                        <a href="" @click.prevent="deleted(item.id)">删除</a>
                        <span>|</span>
                        <a href="" @click.prevent="updata(item.id)">修改</a>
                    </td>
                </tr>
            </table>
        </div>
    
        <script src="./js/vue.js"></script>
        <script src="./js/time.js"></script>
        <script src="./js/axios.min.js"></script>
        <script>
            const beforURL = "http://127.0.0.1:8024/api";
            // 自定义全局指令:获取表单焦点
            Vue.directive("focus", {
                inserted: function(el) {
                    el.focus();
                }
            });
            // 全局过滤器
            Vue.filter("format", function(val, arg) {
                return dateFormat(val, arg)
            })
            let vm = new Vue({
                el: "#app",
                data: {
                    disable: false,
                    sign: true,
                    // 编号
                    id: "",
                    // 书名
                    name: "",
                    // 书的数据(暂时为空)
                    books: []
                },
                mounted: function() {
                    // 调用ajax,获取数据
                    axios.get(beforURL + "/getData").then(val => {
                        // 获取到的数据
                        let data = val.data;
                        // 判断获取到的状态码
                        if (data.status !== 0) return alert("数据获取失败,请联系管理员!");
                        // 将数据填充到vue数据中
                        this.books = data.message;
                    })
                },
                methods: {
                    // 提交事件
                    submit: function() {
                        // 修改图书
                        if (this.disable) {
                            // 判断该书名是否存在的结果
                            this.books.some((item) => {
                                // 编号不同,但是书名相同的是否存在
                                if (item.id != this.id && item.name == this.name) {
                                    // 提示并清空数据
                                    alert("该图书已被占用!");
                                    this.id = "";
                                    this.name = "";
                                    return true
                                }
                            });
                            // 修改的方式其实就是通过id去更新id对应的数据
                            // some方法也能够响应数据
                            this.books.some((item, index) => {
                                // 如果遍历到的id与当前表单中的id相同,那么就是我们要修改的数据
                                if (item.id == this.id) {
                                    axios.put(beforURL + "/upData", {
                                        // 索引
                                        index: index,
                                        // 数据
                                        data: {
                                            id: this.id,
                                            name: this.name,
                                            date: item.date
                                        }
                                    }).then(val => {
                                        // 获取服务器响应的数据
                                        let data = val.data;
                                        if (data.status != 0) return alert("数据修改失败,请联系管理员!");
                                        // 将返回的数据在次替换到原始数据中
                                        this.books = data.data;
                                        return true;
                                    });
                                }
                            });
                            this.disable = false;
                        } else {
                            // 判断该编号或书名是否存在的结果
                            this.books.some((item, index) => {
                                // 如果编号或者书名存在
                                if (item.id == this.id || item.name == this.name) {
                                    // 提示并清空数据
                                    alert("该编号或图书已被占用!");
                                    this.id = "";
                                    this.name = "";
                                    return true
                                }
                            });
                            // 调用ajax并传递数据
                            axios.post(beforURL + "/addData", {
                                ele: {
                                    id: this.id,
                                    name: this.name,
                                    date: new Date()
                                }
                            }).then(val => {
                                // 获取服务器响应的数据
                                let data = val.data;
                                if (data.status != 0) return alert("数据修改失败,请联系管理员!");
                                // 将返回的数据在次替换到原始数据中
                                this.books = data.data;
                            });
                        }
                        // 清空表单
                        this.id = "";
                        this.name = "";
                    },
                    // 点击修改后,将数据显示到表单中,并修改标志
                    updata: function(id) {
                        this.sign = false;
                        // filter方法是根据内部的回调函数进行筛选符合项,并返回数组中符合项的元素
                        var book = this.books.filter((item) => {
                            // 返回books中id属性于id属性相同的元素:对象数组
                            return item.id == id;
                        });
                        // 修改vm双向绑定数据中的对应数据,即input中的值
                        this.id = book[0].id;
                        this.name = book[0].name;
                        // 修改属性
                        this.disable = true;
                    },
                    // 删除数据
                    deleted: function(id) {
                        // 遍历原始数据中每一个元素的id不等于当前id的元素,将其都赋值给原始数组,其实是改变元素数组
                        // this.books = this.books.filter((item) => {
                        //     return item.id != id
                        // });
                        // 在元素数据中遍历查找与当前id值相同的item元素,并返回索引值
                        var index = this.books.findIndex((item) => {
                            return item.id == id
                        });
                        // 调用ajax,删除数据
                        axios.delete(beforURL + "/delData", {
                            params: {
                                index: index
                            }
                        }).then(val => {
                            // 获取服务器响应的数据
                            let data = val.data;
                            // 如果状态不为0
                            if (data.status != 0) return alert("数据修改失败,请联系管理员!");
                            // 将返回的数据在次替换到原始数据中
                            this.books = data.data;
                        })
                    }
                },
                // 计算属性
                computed: {
                    // 返回书的个数
                    total: function() {
                        return this.books.length;
                    }
                },
                // 侦听器:监听输入框中的数值是否存在
                watch: {
                    id: function(val) {
                        // 遍历原始数组,判断遍历的元素的id属性是否和当前的name相同
                        let flag = this.books.some((item) => {
                            if (item.id == this.id) {
                                return true
                            }
                        });
                        // 如果相同则。。。
                        if (flag && this.sign) {
                            alert("该编号已被占用!");
                            this.id = "";
                        }
                    },
                    name: function(val) {
                        // 遍历原始数组,判断遍历的元素的id属性是否和当前的name相同
                        let flag = this.books.some((item) => {
                            if (item.name == this.name) {
                                return true
                            }
                        });
                        // 如果相同则。。。
                        if (flag && this.sign) {
                            alert("该图书已被占用!");
                            this.name = "";
                        }
                    }
                }
            })
        </script>
    </body>
    
    </html>
    
  • 服务器代码

    // 导入express模块
    const express = require("express");
    // 导入cors模块
    const cors = require("cors");
    // 导入路由模块
    const router = require("./route/actionRoute");
    // 创建服务器
    const app = express();
    // 使用一个自定义中间件
    app.use(function(req, res, next) {
        // 对send函数进行封装
        res.dispatch = function(error, status = 1) {
            // 返回数据
            res.send({
                status: status,
                message: (error instanceof Error) ? error.message : error
            });
        };
        next();
    });
    // 使用跨域请求
    app.use(cors());
    // 可解析json数据
    app.use(express.json());
    // 配置可解析表单数据的参数
    app.use(express.urlencoded({ extended: false }));
    // 使用路由模块
    app.use("/api", router);
    oldApi();
    // 监听并开启服务器
    app.listen(8024, "127.0.0.1", function() {
        console.log("127.0.0.1:8024 服务器开启成功!");
    });
    
  • 路由接口代码

    // 导入express模块
    const express = require("express");
    // 导入fs模块
    const fs = require("fs");
    // 导入path模块
    const path = require("path");
    // 创建路由对象
    const router = express.Router();
    // 文件路径
    let filePath = path.join(__dirname, "../db/data.txt");
    // 获取数据模块
    router.get("/getData", function(req, res) {
        // 调用读取文件的函数,并以响应数据作为参数进行传递
        fs.readFile(filePath, { encoding: "utf8" }, (err, val) => {
            // 如果err不为空,则文件读取异常
            if (err) return res.dispatch(err);
            res.send({
                status: 0,
                message: JSON.parse(val)
            });
        });
    });
    // 添加数据
    router.post("/addData", function(req, res) {
        // 获取传进来的数据
        let ele = req.body.ele;
        // 读数据
        fs.readFile(filePath, { encoding: "utf8" }, (err, val) => {
            // 如果err不为空,则文件读取异常
            if (err) return res.dispatch(err);
            // 将获取的数据转化为数组
            let oldData = JSON.parse(val);
            // 向数组的末尾添加数据(对象)
            oldData.push(ele);
            // 对数组中的元素根据id进行排序
            sortByArray(oldData);
            // 将添加后的数组转化为json字符串
            let newData = JSON.stringify(oldData);
            // 开始文件写入
            write(res, newData, "添加成功!");
        });
    });
    // 修改数据模块
    router.put("/upData", function(req, res) {
        // 通过解构,获取值
        let {
            index,
            data
        } = req.body;
        // 文件读取
        fs.readFile(filePath, { encoding: "utf8" }, (err, val) => {
            // 如果err不为空,则文件读取异常
            if (err) return res.dispatch(err);
            // 将获取的数据转化为数组
            let oldData = JSON.parse(val);
            // 向数组中添加数据
            oldData.splice(index, 1, data);
            // 将删除后的数组转化为json字符串
            let newData = JSON.stringify(oldData);
            // 开始文件写入
            write(res, newData, "修改成功!");
        });
    });
    // 删除数据的路由接口
    router.delete("/delData", function(req, res) {
        //获取要删除的索引
        let index = req.query.index;
        // 首先进行文件读取
        fs.readFile(filePath, { encoding: "utf8" }, (err, val) => {
            // 如果err不为空,则文件读取异常
            if (err) return res.dispatch(err);
            // 将获取的数据转化为数组
            let oldData = JSON.parse(val);
            // 删除数组中指定下标的元素
            oldData.splice(index, 1);
            // 对数组中的元素根据id进行排序
            sortByArray(oldData);
            // 将删除后的数组转化为json字符串
            let newData = JSON.stringify(oldData);
            // 开始文件写入
            write(res, newData, "删除成功!");
        });
    });
    // 对数组排序,默认为升序:小=》大,第一个参数为数组
    function sortByArray(array, model = 0) {
        // 小=》大
        if (model === 0) {
            array.sort((a, b) => {
                return a.id - b.id;
            });
            // 大=》小
        } else {
            array.sort((a, b) => {
                return b.id - a.id;
            });
        }
    }
    // 写文件的函数:参数(res:响应对象,data:写入的数据,message:提示信息)
    function write(res, data, message) {
        fs.writeFile(filePath, data, { encoding: "utf8" }, err => {
            // 如果err不为空,则文件读取异常
            if (err) return res.dispatch(err);
            // 成功后,响应数据
            return res.send({
                status: 0,
                message: message,
                data: JSON.parse(data)
            });
        });
    }
    // 共享路由对象
    module.exports = router;
    

2、购物车

需求:使用Vue组件化的开发思想实现简单的购物车案例,包含获取商品列表、增加/减少商品的数量,并计算总价、删除商品后总价扣除……等功能

分析

  1. 标题组件(父子组件传值)
  2. 商品列表组件(父子传值+子父传值)包含有删除数据的功能
  3. 商品总价+数量(兄弟组件传值)

效果展示

在这里插入图片描述

源码奉上

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>购物车案例</title>
    <link rel="stylesheet" href="./css/bootstrap.css">
    <style>
        * {
            padding: 0;
            margin: 0;
        }
        
        #app {
            width: 60%;
            margin: 30px auto;
        }
        
        .panel-heading {
            text-align: center;
        }
        
        #app ul li {
            display: flex;
            justify-content: space-between;
        }
        
        #app ul li div {
            /* width: 80px; */
            height: 33px;
            line-height: 33px;
        }
        
        #app ul li div:nth-child(2) {
            width: 90px;
        }
        
        #app .panel-footer {
            display: flex;
            justify-content: flex-end;
        }
        
        #app .panel-footer div {
            padding-right: 30px;
        }
        
        #app .btn-group button:nth-child(2) {
            width: 50px;
        }
        
        #app .panel-footer div span {
            display: inline-block;
            width: 50px;
            text-align: center;
        }
    </style>
</head>

<body>
    <div id="app" class="panel panel-default panel-primary">
        <!-- 头部 -->
        <page-top :name="name"></page-top>
        <!-- 列表 -->
        <page-content v-on:updata-val="list=$event" :key="item.id" :list="list" :id="item.id" :name="item.name" :price="item.price" v-for="item in list"></page-content>
        <!-- 脚注 -->
        <page-footer :count="count" :price="price"></page-footer>
    </div>

    <script src="./js/vue.js"></script>
    <script>
        // 创建事件中心,用户兄弟组件之间的数据传输
        let eventHub = new Vue();
        // 创建一个顶部的组件
        Vue.component("page-top", {
            props: ["name"],
            template: `  <div class="panel-heading">{{name}}的购物车</div>`
        });

        // 创建一个内容列表组件
        Vue.component("page-content", {
            data: function() {
                return {
                    count: 0,
                };
            },
            props: ["id", "name", "price", "list"],
            template: ` <ul class="list-group">
            <li class="list-group-item">
                <div class="id">编号:{{id<10?"0"+id:id}}</div>
                <div class="name">名称:{{name}}</div>
                <div class="price">价格:{{price}}元</div>
                <div class="btn-group" role="group" aria-label="...">
                    <button type="button"  class="btn btn-default" :count="count"  @click="setValue(true,price,$event)">+</button>
                    <button class="btn btn-default">{{count}}</button>
                    <button type="button" class="btn btn-default" :count="count" @click="setValue(false,price,$event)">-</button>
                </div>
                <button type="button" class="btn btn-danger" @click="updata([id,count,price])">x</button>
            </li>
        </ul>`,
            methods: {
                // 删除商品的事件
                updata: function(val) {
                    // 寻找数组中是否有相同id的元素
                    let index = this.list.findIndex((item) => {
                        // 判断id是否相同
                        return item.id == val[0];
                    });
                    // 那么就删除
                    this.list.splice(index, 1);
                    // 触发父组件的事件:更新数据
                    this.$emit("updata-val", this.list);
                    //触发兄弟组件底部的事件
                    eventHub.$emit("get-data", [2, this.count, this.price])
                },
                // 添加/删除商品的事件
                setValue: function(isAdd, val, event) {
                    // 获取触发事件的自定义属性
                    let length = event.target.getAttribute("count");
                    // 如果为添加
                    if (isAdd) {
                        // 自身属性++
                        this.count++;
                        // 触发兄弟组件底部的事件
                        eventHub.$emit("get-data", [0, 1, this.price]);
                        // 否则就是减少
                    } else {
                        // 如果触发事件自定义属性大于0的时候才能减少
                        if (length > 0) {
                            this.count <= 0 ? 0 : this.count--;
                            // 触发兄弟组件底部的事件
                            eventHub.$emit("get-data", [1, 1, this.price]);
                        }

                    }

                }
            }
        });

        // 创建底部组件
        Vue.component("page-footer", {
            data: function() {
                return {
                    length: 0,
                    money: 0
                }
            },
            props: ["count", "price"],
            template: `
            <div class="panel-footer">
                <div>商品数量:<span>{{length}}</span>件</div>
                <div>商品总价:<span>{{money}}</span>元</div>
            </div>
            `,
            // 该组件准备就绪后,使用事件中心的$on函数创建监听自己的自定义事件,并接受一个参数
            mounted: function() {
                // 该组件监听自己的事件,如果自己的事件触发了,则
                eventHub.$on("get-data", (val) => {
                    // 如果增加
                    if (val[0] === 0) {
                        // 修改数量和总价
                        this.length += val[1];
                        this.money += val[2];
                        // 如果减少
                    } else if (val[0] === 1) {
                        // 修改数量和总价
                        this.length <= 0 ? 0 : this.length -= val[1];
                        this.money <= 0 ? 0 : this.money -= val[2];
                        // 否则就是删除
                    } else {
                        // 修改数量和总价
                        this.length -= val[1];
                        this.money -= val[1] * val[2];
                    }
                });
            }
        })

        // vue环境
        let vm = new Vue({
            el: "#app",
            data: {
                name: "张三",
                count: 0,
                price: 0,
                list: [{
                    id: 1,
                    name: "方便面",
                    price: 20
                }, {
                    id: 2,
                    name: "辣条",
                    price: 12
                }, {
                    id: 3,
                    name: "生菜",
                    price: 10
                }, {
                    id: 4,
                    name: "螺狮粉",
                    price: 28
                }]
            }
        })
    </script>
</body>

</html>

3、ToDoList待办事项列表

需求:使用Vue组件化的开发思想、指令……等知识,实现ToDoList案例,包含增加待办、删除待办、修改待办、获取待办以及更改待办状态……等功能。

分析

  1. 搜索框组件
  2. 正在进行标题和总数组件
  3. 正在进行列表组件
  4. 已完成标题和总数组件
  5. 已完成列表组件
  6. 删除全部待办的组件

注意

  1. 修改事项时点击p标签,则input文本框出现,并将原有数据显示
  2. 修改完成,点击Vue父组件,则使用ref直接修改子组件的数据
  3. 最后将数据保存到父组件中,并将p标签显示,input文本框消失

ref的使用

  1. ref属性用于父组件访问子组件的实例和数据
  2. 你可以给子组件上挂载一个ref="名称"的属性
  3. 那么你在父组件的函数中就可以使用this.$refs.名称的方式将带有ref="名称"属性并且名称一致的的子组件中的实例数据拿到,拿到的是一个对象
  4. 如果一个页面中有多个this.$refs.名称,那么名称必须不能相同,所以是唯一的
  5. 如果this.$refs.名称v-for指令一起使用,那么从父组件拿到的子组件数据将是一个对象数组,因为你的这个组件使用了for遍历,那么也就有多个ref名称相同,所以返回的就为数组
  6. 那么如果你要在数组中找对应的数据,那么你就要拿索引来操作了

效果展示

在这里插入图片描述

源码奉上

  • html和Vue操作

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,user-scalable=no,initial-scale=1.0,
        maximum-scale=1.0,minimum-scale=1.0">
        <title>ToDoList</title>
        <!-- 移动端布局样式初始化的插件 -->
        <link rel="stylesheet" href="./css/normalize.css ">
        <link rel="stylesheet" href="css/style.css">
    
    </head>
    
    <body>
        <div id="app" @click.self="hide">
            <!-- 顶部导航 -->
            <page-header @add-value="list=$event" :list="list"></page-header>
    
            <!-- 内容区域 -->
            <section>
                <!-- 正在进行 -->
                <div>
                    <!-- 数量标题组件 -->
                    <titele-proceed :list="list"></titele-proceed>
                    <!-- 列表 -->
                    <ol>
                        <list-proceed ref="child" @alter-val="[id,isFinish]=$event" @del-value="list=$event" v-if="!item.checked" :list="list" :value="item.value" :id="item.id" :key="item.id" v-for="(item,index) in list"></list-proceed>
                    </ol>
                </div>
    
                <!-- 已经完成 -->
                <div>
                    <!-- 数量标题组件 -->
                    <titele-finish :list="list"></titele-finish>
                    <!-- 列表 -->
                    <ul>
                        <list-finish ref="childtwo" @alter-val="[id,isFinish]=$event" @del-value="list=$event" v-if="item.checked" :list="list" :value="item.value" :id="item.id" :key="item.id" v-for="(item,index) in list"></list-finish>
                    </ul>
                </div>
            </section>
    
            <!-- 页脚 -->
            <page-footer @clear-val="list=$event"></page-footer>
        </div>
    
        <script src="./js/vue.js"></script>
        <script>
            // 全局获取焦点的指令
            Vue.directive(
                "focus", {
                    inserted: function(el) {
                        el.focus();
                    }
                }
            );
    
            // 顶部组件
            Vue.component("page-header", {
                data: function() {
                    return {
                        value: ""
                    }
                },
                props: ["list"],
                template: `
                <header>
                <form action="" @submit.prevent="">
                    <label for="title">ToDoList</label>
                    <input v-on:keyup.prevent.enter="enter"  type="text" id="title" class="title" v-focus v-model="value" placeholder="添加ToDo" required>
                </form>
            </header>
            `,
                methods: {
                    enter: function() {
                        // 如果为空
                        if (this.value == "") {
                            alert("不能为空");
                        }
                        let flag = false;
                        // 判断是否和列表中的重复
                        flag = this.list.some((item) => {
                            // 如果有重复
                            if (this.value.trim() == item.value) {
                                // 创建一个新对象,并添加到数组中
                                return true;
                            }
                        });
                        if (!flag) {
                            this.list.push({
                                checked: false,
                                id: this.list.length + 1,
                                value: this.value.trim()
                            });
                            // 触发父组件的事件
                            this.$emit("add-value", this.list);
                        } else {
                            alert("不能重复");
                        }
                        this.value = "";
    
                    }
                }
            });
    
            // 正在进行标题组件
            Vue.component("titele-proceed", {
                props: ["list"],
                template: `
                        <h2 class="one">
                            正在进行
                            <span>{{count()}}</span>
                        </h2>
                `,
                methods: {
                    // 数量
                    count: function() {
                        let num = 0;
                        for (let i = 0; i < this.list.length; i++) {
                            if (!this.list[i].checked) {
                                num++;
                            }
                        }
                        return num
                    }
                }
            });
    
            // 正在进行列表组件
            Vue.component("list-proceed", {
                data: function() {
                    return {
                        isFinish: false,
                        flag: true,
                        index: 0,
                        val: this.value
                    }
                },
                props: ["value", "id", "list"],
                template: `
                            <li class="li" >
                                <input type="checkbox" @click="checked">
                                <a href="javascript:;" @click="updata">-</a>
                                <p @click="display" v-show="flag">{{value}}</p>
                                <input type="text" v-model="val" v-show="!flag">
                            </li>
                `,
                methods: {
                    // 更新数据
                    updata: function() {
                        // 拿到要删除的元素的索引
                        let index = this.list.findIndex((item) => {
                            return item.id == this.id
                        });
                        // 删除元素的索引
                        this.list.splice(index, 1);
                        // 触发父组件的事件
                        this.$emit("del-value", this.list);
                    },
                    // 修改为已完成
                    checked: function(e) {
                        // 获取事件对象的checked属性,是否被选中
                        if (e.currentTarget.checked) {
                            let index = this.list.findIndex((item) => {
                                return item.id == this.id;
                            });
                            // 修改数据
                            this.list[index].checked = true;
                            // 将数据返回给父组件
                            this.$emit("del-value", this.list);
                        }
                    },
                    // 修改数据
                    display: function() {
                        // 修改显示与隐藏的值
                        this.flag = false;
                        // 拿到id
                        this.index = this.id;
                        // 给父组件传递id和标志
                        this.$emit("alter-val", [this.index, this.isFinish]);
                    }
                }
            });
    
            // 已完成标题组件
            Vue.component("titele-finish", {
                props: ["list"],
                template: `
                        <h2 class="two">
                            已完成
                            <span>{{count()}}</span>
                        </h2>
                `,
                methods: {
                    // 数量
                    count: function() {
                        let num = 0;
                        for (let i = 0; i < this.list.length; i++) {
                            if (this.list[i].checked) {
                                num++;
                            }
                        }
                        return num
                    }
                }
            });
    
            // 已完成列表组件
            Vue.component("list-finish", {
                data: function() {
                    return {
                        isFinish: true,
                        flag: true,
                        index: 0,
                        val: this.value
                    }
                },
                props: ["value", "id", "list"],
                template: `
                            <li class="li">
                                <input type="checkbox" checked @click="checked">
                                <a href="javascript:;" @click="updata">-</a>
                                <p @click="display" v-show="flag">{{value}}</p>
                                <input type="text" v-model="val" v-show="!flag">
                            </li>
                `,
                methods: {
                    // 更新数据
                    updata: function() {
                        // 拿到要删除的元素的索引
                        let index = this.list.findIndex((item) => {
                            return item.id == this.id
                        });
                        // 删除元素的索引
                        this.list.splice(index, 1);
                        // 触发父组件的事件
                        this.$emit("del-value", this.list);
                    },
                    // 修改为进行中
                    checked: function(e) {
                        // 获取事件对象的checked属性,是否被取消选中
                        if (!e.currentTarget.checked) {
                            let index = this.list.findIndex((item) => {
                                return item.id == this.id;
                            });
                            // 修改数据
                            this.list[index].checked = false;
                            // 将数据返回给父组件
                            this.$emit("del-value", this.list);
                        }
                    },
                    // 修改数据
                    display: function() {
                        // 修改显示与隐藏的值
                        this.flag = false;
                        // 拿到id
                        this.index = this.id;
                        // 给父组件传递id和标志
                        this.$emit("alter-val", [this.index, this.isFinish]);
                    }
                }
            });
            // 底部组件
            Vue.component("page-footer", {
                template: `
                <footer>
                    Copyright © 2014 todolist.cn
                    <a href="javascript:;" @click="$emit('clear-val',[])">clear</a>
                </footer>
                `
            });
    
            // Vue环境
            let vm = new Vue({
                el: "#app",
                data: {
                    // 获取修改列表的id
                    id: 0,
                    isFinish: null,
                    list: [{
                        checked: true,
                        id: 1,
                        value: "6:00 早晨起床"
                    }, {
                        checked: false,
                        id: 2,
                        value: "8:00 吃早饭"
                    }, {
                        checked: false,
                        id: 3,
                        value: "9:00 去上班"
                    }, {
                        checked: false,
                        id: 4,
                        value: "12:00 去吃午饭"
                    }, {
                        checked: false,
                        id: 5,
                        value: "2:00 去上班"
                    }, {
                        checked: false,
                        id: 6,
                        value: "6:00 下班回家"
                    }]
                },
                methods: {
                    // 点击该组件后,input消失
                    hide: function() {
                        let n = false;
                        // 如果不是完成发来的数据
                        if (!this.isFinish) {
                            // 注意:this.$refs.child拿过来是一个全新的数组,所以我们不知道是要修改哪个元素,但是它们各自身上都有id,那么我们可以根据id来判断是这个数组中的第几个元素
                            let index = this.$refs.child.findIndex((item) => {
                                return item.id == this.id;
                            });
                            // 修改flag隐藏与展示的值
                            this.$refs.child[index].flag = true;
                            // 判断和自己编号不同,但是值一样的是否存在
                            let n = this.list.some((item) => {
                                if (item.id != this.id && item.value == this.$refs.child[index].val) {
                                    return true
                                }
                            });
                            // 存在
                            if (n) {
                                alert("查找到有相同项,请修改!");
                                // 将本身的值再重新赋过去
                                this.$refs.child[index].val = this.list[this.id - 1].value;
                                // 不存在
                            } else {
                                // 修改文本值,id总比原始数组中的元素多1
                                this.list[this.id - 1].value = this.$refs.child[index].val;
                            }
    
                            // 如果是完成发来的数据
                        } else {
                            let index = this.$refs.childtwo.findIndex((item) => {
                                return item.id == this.id;
                            });
                            this.$refs.childtwo[index].flag = true;
                            // 判断和自己编号不同,但是值一样的是否存在
                            let n = this.list.some((item) => {
                                if (item.id != this.id && item.value == this.$refs.childtwo[index].val) {
                                    return true
                                }
                            });
                            // 存在
                            if (n) {
                                alert("查找到有相同项,请修改!");
                                // 将本身的值再重新赋过去
                                this.$refs.childtwo[index].val = this.list[this.id - 1].value;
                                // 不存在
                            } else {
                                // 修改文本值,id总比原始数组中的元素多1
                                this.list[this.id - 1].value = this.$refs.childtwo[index].val;
                            }
    
                        }
                    },
                }
    
            });
        </script>
    </body>
    
    </html>
    
  • css样式

    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    
    
    /* 当设备宽度大于750px时 */
    
    @media screen and (min-width:750px) {
        /* html采用75px的字体大小 */
        html {
            font-size: 75px !important;
        }
    }
    
    body {
        background: #CDCDCD;
    }
    
    header {
        height: .666667rem;
        background: rgba(47, 47, 47, 0.98);
    }
    
    header form {
        width: 10rem;
        margin: 0 auto;
        display: flex;
        align-items: center;
    }
    
    header label {
        flex: 1;
        width: 1.333333rem;
        height: .666667rem;
        line-height: .666667rem;
        color: #ffffff;
        font-size: .32rem;
        cursor: pointer;
    }
    
    header input {
        flex: 1;
        width: 4.826667rem;
        height: .346667rem;
        padding-left: .2rem;
        border: none;
        border-radius: .066667rem;
        box-shadow: 0 1px rgba(255, 255, 255, 0.24), 0 1px 6px rgba(0, 0, 0, 0.45) inset;
        font-size: .16rem;
        margin-right: 10px;
        /* 点击表单后,表单边框消失 */
        outline: none;
    }
    
    section,
    footer {
        min-width: 320px;
        max-width: 750px;
        width: 10rem;
        margin: 0 auto;
        font-family: Arial, Helvetica;
        background: #CDCDCD;
    }
    
    section h2 {
        height: .413333rem;
        margin: .266667rem 0;
        font-size: .333333rem;
    }
    
    section span {
        float: right;
        width: .266667rem;
        height: .266667rem;
        line-height: .266667rem;
        text-align: center;
        border-radius: 50%;
        background: #E6E6FA;
        font-size: .186667rem;
        color: #666666;
        margin-right: .133333rem;
    }
    
    ul,
    ol {
        list-style: none;
    }
    
    footer {
        color: #666666;
        font-size: .186667rem;
        text-align: center;
    }
    
    footer a {
        text-decoration: none;
        color: #999999;
    }
    
    .li {
        height: .426667rem;
        background: #ffffff;
        margin-bottom: .133333rem;
        border-radius: .04rem;
        border-left: .066667rem solid #629A9C;
        box-shadow: 0 .013333rem .026667rem rgba(0, 0, 0, 0.07);
        /* 弹性布局 */
        display: flex;
        /* 侧轴居中 */
        align-items: center;
        /* 两边贴边,再平分剩余空间 */
        justify-content: space-around;
        /* 相对定位 */
        position: relative;
        cursor: pointer;
    }
    
    .li input {
        position: absolute;
        width: .293333rem;
        height: .293333rem;
        left: .2rem;
    }
    
    .li p {
        display: inline-block;
        width: 85%;
        height: 100%;
        line-height: .426667rem;
        font-size: .186667rem;
    }
    
    .li input[type=text] {
        display: inline-block;
        width: 80%;
        height: 75%;
        margin-left: 38px;
        padding-left: 5px;
        line-height: .426667rem;
        font-size: .186667rem;
    }
    
    .li a {
        font-size: 0.2rem;
        position: absolute;
        width: .346667rem;
        height: .32rem;
        line-height: .14rem;
        text-align: center;
        right: .106667rem;
        border-radius: 50%;
        border: .08rem double #ffffff;
        background: #cccccc;
        color: #ffffff;
    }
    
    .finish {
        opacity: 0.5;
        border-left: .066667rem solid #999999;
    }
    

4、简易计算器

1、需求:使用Vue单文件组件、Vuex、Vue脚手架……等知识完成一个可以二目运算的简单计算器

2、需求大体角度分析

  • 使用Vue脚手架创建项目
  • 不需要Router,但是需要Vuex
  • 选择单独的文件配置项目配置
  • 需要使用组件,并且不止一个

3、组件和Vuex角度分析

  • 组件强调复用性,所以组件可以是三个:输入框组件、运算符组件、按钮和结果组件(一个)
  • Vuex的状态有:数字1、数字2、运算符号、结果
  • 那么只要组件中的值变化了就要修改状态,所有需要watch侦听器来实时监测,并调用Vuex中的方法
  • 按钮和结果组件中:按钮需要点击事件,点击以后要调用Vuex中的方法,根据不同的运算符进行相应的运算,并将结果赋值到结果状态中
  • 结果的值直接绑定到结果组件的地方
  • 由于不需要异步操作和对结果进行加工,那么Vuex中就不需要Action、Getter

4、效果展示

在这里插入图片描述

5、源代码

  • Vuex中的数据

    import Vue from 'vue';
    import Vuex from 'vuex';
    
    Vue.use(Vuex);
    
    export default new Vuex.Store({
        state: { one: 0, two: 0, flag: 0, result: 0 },
        mutations: {
            // 获取文本框中的值,并将其赋值给公共数据
            getValue(state, arr) {
                // isFirstNum用来判断是否是第一个数字
                if (arr[0]) {
                    state.one = arr[1];
                } else {
                    state.two = arr[1];
                }
            },
            // 获取下拉菜单中的运算符号,并将其赋值给公共数据
            getFlag(state, symbol) {
                state.flag = symbol;
            },
            // 计算运算结果,并将其赋值给公共数据
            computation(state) {
                // 判断操作符的数字
                switch (Number(state.flag)) {
                    // +
                    case 0:
                        state.result = state.one + state.two;
                        break;
                        // -
                    case 1:
                        state.result = state.one - state.two;
                        break;
                        // *
                    case 2:
                        state.result = state.one * state.two;
                        break;
                        // ÷
                    case 3:
                        state.result = state.one / state.two;
                        break;
                        // %
                    case 4:
                        state.result = state.one % state.two;
                }
            }
        },
        actions: {},
        modules: {}
    });
    
  • 输入框组件中的数据

    <template>
        <input type="text" id="num1" v-model="num" value="num" >
    </template>
    
    <script>
    import {mapMutations} from "vuex";
    let timer = null;
    export default {
        //父组件给子组件传值,来判断是否是第一个输入框组件 
       props:['isFirstNum'],
       data(){
           return {num:0}
       },
       methods:{
           ...mapMutations(['getValue']),
        //  获取数据的函数
           getNum(flag){
               let arr=[flag,Number(this.num)];
            // 将标志和数字传入
               this.getValue(arr);
           }
       },
       watch:{
           num:function(){
               clearTimeout(timer);
               timer=setTimeout(()=>{
                    this.getNum(this.isFirstNum);
               },500)
           }
       }
    }
    </script>
    
    <style scoped>
    
    </style>
    
  • 运算符组件中的数据

    <template>
        <select name="" id="" v-model="flag">
            <option value="0">+</option>
            <option value="1">-</option>
            <option value="2">×</option>
            <option value="3">÷</option>
            <option value="4">%</option>
        </select>
    </template>
    
    <script>
    import {mapMutations} from 'vuex';
    export default {
       data(){
           return {flag:0}
       },
       methods:{
            ...mapMutations(['getFlag']),
            getValue(flag){
                this.getFlag(flag);
            }
       },
       watch:{
           flag:function(){
               this.getValue(this.flag);
           }
       }
    }
    </script>
    
    <style scoped>
    
    </style>
    
  • 按钮以及结果组件中的数据

    <template>
        <div>
            <button @click="getResult">=</button>
            <span>{{this.result}}</span>
        </div>
        
    </template>
    <script>
    import {mapState,mapMutations} from 'vuex';
    export default {
      data(){
          return {};
      },
      methods:{
          ...mapMutations(['computation']),
        // 获取结果的函数
          getResult(){
            // 调用vuex中的函数,并返回结果,赋值给num
            this.computation();
            // console.log(this.result);
          }
      },
      computed:{
            ...mapState(['result'])
      }
    }
    </script>
    
    <style scoped>
        div{
            display: inline-block;
        }
        span{
            display: inline-block;
            padding:0 20px;
            border: 1px solid #333;
        }
    </style>
    

九、总结

以上就是Vue框架的全部内容,当然还有很多内容没有延申拓展,因为本篇文章只是说让大家对Vue有一个基本的认识,方便初学者打好基础,那么在接下来的前端生涯中少走弯路。

当然如果本篇文章哪里讲的有不妥的地方,还请各位踊跃发言。那么下次博主将给大家带有React框架的全部内容,希望大家多多支持。

  • 68
    点赞
  • 487
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
Vue入门精通》是一本关于Vue.js前端框架的学习指南。本书分为入门、进阶和精通三个部分,帮助读者从零基础开始学习,并逐步掌握Vue.js的核心概念和应用技巧。 在入门部分,书中首先介绍了Vue.js的基本概念,如组件、数据绑定、指令等。读者将学会如何搭建Vue项目、编写Vue组件,并了解Vue的基本语法和工作原理。通过实例演示和练习题,读者可以巩固对Vue基础知识的理解和掌握,为后续的学习打下坚实的基础。 进阶部分则重点讲解了Vue的高级特性和常用技巧。读者将学会如何使用Vue Router进行路由管理、Vuex进行状态管理、以及Vue CLI进行项目构建。此外,该部分还包括了对Vue的响应式原理、性能优化和国际化等方面的深入讲解,帮助读者进一步提升开发能力。 在精通部分,书中通过案例实战、源码解析等方式,深入剖析Vue.js的内部机制和高级用法。读者将学会如何进行自定义指令、混入(mixin)、过滤器(filter)等高级扩展,以及如何优化大型项目的性能和可维护性。此外,书中还介绍了与第三方库、服务端渲染等相关的内容,帮助读者更进一步地掌握和应用Vue.js。 总之,《Vue入门精通》是一本循序渐进、实例丰富的Vue.js学习教材,适合从零开始学习Vue.js的前端开发者。通过系统地学习该书,读者可以逐步掌握Vue.js的核心知识和实际应用技巧,提升自己在前端开发领域的竞争力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

御弟謌謌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值