Vuejs知识

350 阅读47分钟

vue

一、了解Vuejs

Vuejs的特点

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

  • 渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,带来更丰富的交互体验。
  • 或者如果你希望将更多的业务逻辑使用Vue实现,那么Vue的核心库以及其生态系统。
  • 比如Core+Vue-router+Vuex,也可以满足你各种各样的需求。
    Vue有很多特点和Web开发中常见的高级功能
  • 解耦视图和数据
  • 可复用的组件
  • 前端路由技术
  • 状态管理
  • 虚拟DOM
    对于Vue是一套渐进式框架的理解

Vuejs的安装

  • 方式一:直接CDN引入
  • 方式二:下载和引入
  • 方式三:NPM安装

Vuejs初体验

<div id="app">{{message}}</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
    	message: 'Hello Vuejs'
    },
  })
</script>

创建Vue对象的时候,传入了一些options:{}

  • {}中包含了el属性:该属性决定了这个Vue对象挂载到哪一个元素上,很明显,我们这里是挂载到了id为app的元素上
  • {}中包含了data属性:该属性中通常会存储一些数据
    这些数据可以是我们直接定义出来的,比如像上面这样。
    也可能是来自网络,从服务器加载的。

MVVM架构

image.png

  • View层: 视图层
    在我们前端开发中,通常就是DOM层。
    主要的作用是给用户展示各种信息。
  • Model层: 数据层
    数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据。
    在我们计数器的案例中,就是后面抽取出来的obj,当然,里面的数据可能没有这么简单。
  • VueModel层: 视图模型层
    视图模型层是View和Model沟通的桥梁。
    一方面它实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中
    另一方面它实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情况下改变对应的Data。 MVVM(维基百科搜索)

创建Vue实例传入的options

  • el: ''
  • data: {}
  • methods: {}
  • computed: {}
  • 生命周期函数

Vue的生命周期

image.png

image.png

image.png

二、vue基础语法

1. 插值语法

Mustache语法(插值表达式)

<div id="app">
  <h2>Hello {{message}}</h2>
  <h2>{{firstName}} {{lastName}}</h2>
  <h2>{{number * 2}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: 'World',
      firstName: 'Kobe',
      lastName: 'Bryant',
      number: 100
    }
  })
</script>

v-once

  • 该指令后面不需要跟任何表达式(比如之前的v-for后面是由跟表达式的)
  • 该指令表示元素和组件(组件后面才会学习)只渲染一次,不会随着数据的改变而改变。
<div id="app">
  // 即使改变了message的值,此时该行在浏览器中的渲染不会变
  <h2 v-once>{{message}}</h2>
  <h2>{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
    	message: '今天天气不错!'
    }
  })
</script>

v-html

  • 该指令后面往往会跟上一个string类型
  • 会将string的html解析出来并且进行渲染
<div id="app">
  <div>{{link}}</div>
  <div v-html="link"></div>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
    	link: '<a href="http://www.baidu.com">百度一下</a>'
    }
  })
</script>

image.png

v-text

  • v-text作用和Mustache比较相似:都是用于将数据显示在界面中
<div id="app">
  <h2>{{message}}</h2>
  <h2 v-text="message"></h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
    	message: '你好啊'
    }
  })
</script>

v-pre

  • v-pre用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。
<div id="app">
  <!-- 第一个h2元素中的内容会被编译解析出来对应的内容 -->
  <!-- 第二个h2元素中会直接显示{{message}} -->
  <h2>{{message}}</h2>
  <h2 v-pre>{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
    	message: '你好啊'
    }
  })
</script>

v-clock

  • 在某些情况下,我们浏览器可能会直接显然出未编译的Mustache标签。
  • 避免网络请求慢的情况下,页面直接显示差值表达式,作用给其添加一个隐藏的样式,当数据出来后,该样式自动消失。
<head>
  <meta charset="UTF-8">
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
<body>

<div id="app">
  <h2 v-cloak>{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  setTimeout(() => {
      const app = new Vue({
              el: '#app',
              data: {
                      message: 'Hello World'
              }
      })
  }, 2000)
</script>

2. 绑定属性

v-bind的基础

前面我们学习的指令主要作用是将值插入到我们模板的内容当中。 但是,除了内容需要动态来决定外,某些属性我们也希望动态来绑定。

比如动态绑定a元素的href属性; 比如动态绑定img元素的src属性 这个时候,我们可以使用v-bind指令:

  • 作用:动态绑定属性
  • 缩写::

v-bind用于绑定一个或多个属性值,或者向另一个组件传递props值 在开发中,有哪些属性需要动态进行绑定呢? 还是有很多的,比如图片的链接src、网站的链接href、动态绑定一些类、样式等等 比如通过Vue实例中的data绑定元素的src和href,代码如下:

<div id="app">
  <img v-bind:src="imgSrc" alt="">
  <a v-bind:href="link">Vuejs官网</a>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      imgSrc: 'https://cn.vuejs.org/images/logo.png',
      link: 'https://cn.vuejs.org/'
    }
  })
</script>

v-bind语法糖

v-bind有一个对应的语法糖,也就是简写方式
在开发中,我们通常会使用语法糖的形式,因为这样更加简洁。 简写方式如下:

<div id="app">
  <img :src="imgSrc" alt="">
  <a :href="link">Vuejs官网</a>
</div>

v-bind动态绑定class

对象语法

对象语法,一个大括号就是一个对象,对象里是键值对,键是类名,值是布尔值。

对象语法的含义是: class后面跟的是一个对象。
对象语法有下面这些用法:

用法一:直接通过{}绑定一个类
<h2 :class="{'active': isActive}">Hello World</h2>

用法二:也可以通过判断,传入多个值
<h2 :class="{'active': isActive, 'line': isLine}">Hello World</h2>

用法三:和普通的类同时存在,并不冲突
注:如果isActive和isLine都为true,那么会有title/active/line三个类
<h2 class="title" :class="{'active': isActive, 'line': isLine}">Hello World</h2>

用法四:如果过于复杂,可以放在一个methods或者computed中
注:classes是一个计算属性
<h2 class="title" :class="classes">Hello World</h2>

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <style>
    .active {
      color: red;
    }
    .line {
      text-decoration: underline;
    }
  </style>
</head>
<body>

<div id="app">
  <!-- <h2 class="active">{{message}}</h2> -->
  <!-- <h2 :class="active">{{message}}</h2> -->
  
  <!-- <h2 v-bind:class="{'key1': value1, 'key2': value2}">{{message}}</h2> -->
  <!-- <h2 v-bind:class="{'类名1': boolean, '类名2': boolean}">{{message}}</h2> -->
  
  <h2 v-bind:class="{'active': isActive, 'line': isLine}">{{message}}</h2>
  <button v-on:click="btnClick">按钮</button>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isActive: true,
      isLine: true
    },
    methods: {
      btnClick: function () {
        this.isActive = !this.isActive
      }
    }
  })
</script>

</body>
</html>  
数组语法

数组语法,一个中括号就是一个数组,用的比较少。

数组语法的含义是: class后面跟的是一个数组。
数组语法有下面这些用法:

用法一:直接通过{}绑定一个类
<h2 :class="['active']">Hello World</h2>

用法二:也可以传入多个值
<h2 :class=“[‘active’, 'line']">Hello World</h2>

用法三:和普通的类同时存在,并不冲突
注:会有title/active/line三个类
<h2 class="title" :class=“[‘active’, 'line']">Hello World</h2>

用法四:如果过于复杂,可以放在一个methods或者computed中
注:classes是一个计算属性
<h2 class="title" :class="classes">Hello World</h2>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"> 
  <style>
    .active {
      color: red;
    }

    .line {
      text-decoration: underline;
    }

    .large {
      font-size: 50px;
    }
  </style>
</head>
<body>

<div id="app">
  <h2 :class="['active', 'line']">今天天气不错</h2>
  <h2 class="large" :class="['active', 'line']">今天天气不错</h2>
  <h2 class="large" :class="getClasses()">今天天气不错</h2>
</div>

<script src="../js/vue.js"></script>
<script>
    let vm = new Vue({
      el: '#app',
      data: {

      },
      methods: {
        getClasses() {
          return ['active', 'line']
        }
      }
    })
</script>

</body>
</html>

v-bind动态绑定style

对象语法

:style="{color: currentColor, fontSize: fontSize + 'px'}"
style后面跟的是一个对象类型
对象的key是CSS属性名称
对象的value是具体赋的值,值可以来自于data中的属性

<div id="app">
  <h2 :style="{key(属性名): value(属性值)}">今天天气不错</h2>
  // '50px'必须加上单引号,否则是当作一个变量去解析
  <h2 :style="{fontSize: '50px'}">今天天气不错</h2>
  <h2 :style="{color: dColor, fontSize: dFontSize + 'px'}">今天天气不错</h2>
  <h2 :style="getStyles()">今天天气不错</h2>
  <h2 :style="objStyle">今天天气不错</h2>
</div>

<script src="../js/vue.js"></script>
<script>
    let vm = new Vue({
        el: '#app',
        data: {
          message: '你好啊,李银河',
          dColor: 'red',
          dFontSize: 50,
          objStyle: {
            color: 'red',
            fontSize: '60px'
          }
        },
        methods: {
          getStyles: function () {
            return {color: this.dColor, fontSize: this.dFontSize + 'px'}
          }
        }
    })
</script> 
数组语法

<div v-bind:style="[baseStyles, overridingStyles]"></div>
style后面跟的是一个数组类型
多个值,分割即可

<div id="app">
  <h2 :style="[objStyle1, objStyle2]">今天天气不错</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      objStyle1: {
        fontSize: '60px',
        color: 'green'
      },
      objStyle2: {
        textDecoration: 'underline'
      }
    }
  })
</script>

3. 计算属性computed

计算属性的基本使用

  • 当我们需要对数据进行某种变化之后再使用的时候可以使用计算属性
    我们知道,在模板中可以直接通过插值语法显示一些data中的数据。
    但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示
    比如我们有firstName和lastName两个变量,我们需要显示完整的名称。
    但是如果多个地方都需要显示完整的名称,我们就需要写多个{{firstName}} {{lastName}}
    我们可以将上面的代码换成计算属性:
    OK,我们发现计算属性是写在实例的computed选项中的。
<div id="app">
  <h2>{{firstName}} {{lastName}}</h2>
  <h2>{{fullName}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      firstName: 'Kobe',
      lastName: 'Bryant'
    },
    computed: {
      fullName() {
        return this.firstName + ' ' + this.lastName
      }
    },
    // methods: {
    //   fullName() {
    //     return this.firstName + ' ' + this.lastName
    //   }
    // }
  })
</script>

计算属性的复杂操作

计算属性computed有缓存,只需要调用一次;methods里面的方法没有缓存,调用一次就执行一次。性能较低。

<div id="app">
  <h2>总价值: {{totalPrice}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      books: [
        {name: 'Java编程思想', price: 99, count: 3},
        {name: 'Unix编程艺术', price: 118, count: 2},
        {name: 'Vuejs程序设计', price: 89, count: 1},
      ]
    },
    computed: {
      totalPrice() {
        // 1.第一种方式
        /*
        let total = 0
        this.books.forEach(book => {
          total += book.price * book.count
        })
        return total
        */

        // 2.第二种方式
        // let total = 0
        // for (let i in this.books) {
        //   const book = this.books[i]
        //   total += book.price * book.count
        // }
        // return total

        // 3.第三种方式
        // reduce()方法计算数组元素相加后的总和,作为一个高介函数
        // 第一个参数:初始值,或计算结束后的返回值
        // 第二个参数:当前元素
        return this.books.reduce((preValue, book) => {
          return preValue + book.price * book.count
        }, 0)
      }
    }
  })
</script>

计算属性的setter和getter

计算属性的完整写法是有set和get的,一般情况下set都不写,简洁的写法就是注释掉的三行。

<div id="app">
  <h2>{{fullName}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      firstName: 'Kobe',
      lastName: 'Bryant'
    },
    computed: {
      // fullName: function () {
      //   return this.firstName + ' ' + this.lastName
      // }
      // 一般不实现set方法,是个只读属性
      fullName: {
        set(newValue) {
          const names = newValue.split(' ')
          this.firstName = names[0]
          this.lastName = names[1]
        },
        // 返回值
        get() {
          return this.firstName + ' ' + this.lastName
        }
      },
    },
  })
</script>

通过set改变属性的值

image.png

计算属性和methods的对比

methods和computed看起来都可以实现我们的功能,
那么为什么还要多一个计算属性这个东西呢?

  • 原因:计算属性会进行缓存,如果多次使用时,计算属性只会调用一次。
<div id="app">
  // <!-- 1、直接拼接:语法过于繁琐 -->
  <h2>{{firstName}} {{lastName}}</h2>
  // <!-- 2、通过定义methods:每次使用都会被调用 -->
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  // <!-- 3、通过computed:只会计算一次,vue内部对计算属性做了缓存,如果每次调用值没有发生变化就会使用缓存里的数据 -->
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      firstName: 'LeBron',
      lastName: 'James'
    },
    computed: {
      fullName() {
        console.log('调用了computed中的fullName');
        return this.firstName + ' ' + this.lastName
      }
    },
    methods: {
      getFullName() {
        console.log('调用了methods中的getFullName');
        return this.firstName + ' ' + this.lastName
      }
    }
  })
</script>

4. 侦听器watch

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

具体案例在 父子组件传值案例

 watch: {
          dnumber1(newValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue);
          },
        }

5. 事件监听

v-on介绍

在Vue中如何监听事件呢?使用v-on指令
比如点击、拖拽、键盘事件等等
作用:绑定事件监听器
缩写:@

v-on基础

注:v-on也有对应的语法糖:
  v-on:click 可以写成 @click

<div id="app">
  <h2>{{counter}}</h2>
  //<!--<h2 v-bind:title></h2>-->
  //<!--<h2 :title></h2>-->
  // <button v-on:click="counter++">+</button>
  // <button v-on:click="counter--">-</button>
  // <button v-on:click="increment">+</button>
  // <button v-on:click="decrement">-</button>
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      counter: 0
    },
    methods: {
      increment() {
        this.counter++
      },
      decrement() {
        this.counter--
      }
    }
  })
</script>

v-on参数

  • 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。   但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
  • 情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。
<div id="app">
  //<!--1.事件调用的方法没有参数-->
  <button @click="btn1Click()">按钮1</button> // btn1Click
  <button @click="btn1Click">按钮1</button> // btn1Click

  //<!--2.在事件定义时, 写方法时省略了小括号, 但是方法本身是需要一个参数的, 
  //      这个时候, Vue会默认将浏览器生产的event事件对象作为参数传入到方法-->
  // <button @click="btn2Click(123)">按钮2</button> //123
  // <button @click="btn2Click()">按钮2</button> // undefined
  <button @click="btn2Click">按钮2</button> // MouseEvent {}

  //<!--3.方法定义时, 我们需要event对象, 同时又需要其他参数-->
  //<!-- 在调用方式, 如何手动的获取到浏览器产生的event对象: $event-->
  <button @click="btn3Click(abc, $event)">按钮3</button> // 123 MouseEvent {}
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      abc: 123
    },
    methods: {
      btn1Click() {
        console.log("btn1Click");
      },
      btn2Click(event) {
        console.log('--------', event);
      },
      btn3Click(abc, event) {
        console.log('++++++++', abc, event);
      }
    }
  })

  // 如果函数需要参数,但是没有传入, 那么函数的形参为undefined
  // function abc(name) {
  //   console.log(name);
  // }
  //
  // abc()
</script>

v-on修饰符

Vue提供了修饰符来帮助我们方便的处理一些事件:

  • .stop - 调用 event.stopPropagation()。阻止事件冒泡
  • .prevent - 调用 event.preventDefault()。阻止默认事件
  • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
  • .native - 监听组件根元素的原生事件。
  • .once - 只触发一次回调。
<div id="app"
  // <!--1. .stop修饰符的使用 -->
  // 如果不加.stop,点击按钮之后分别会打印btnClick divClick 这就是事件冒泡
  // 加了.stop就是阻止事件冒泡,当点击按钮时,只打印btnClick;点击aaaaa区域时,则会打印divClick
  <div @click="divClick">
    aaaaa
    <button @click.stop="btnClick">按钮</button>
  </div>
  
  // <!--2. .prevent修饰符的使用 -->
  // input的类型是提交,如果不加.prevent则点击提交的时候会自动提交
  // 加了.prevent则在点击提交的时候不会直接提交,可以在submitClick事件中去做一些数据的处理
  <br>
  <form action="baidu">
    <input type="submit" value="提交" @click.prevent="submitClick">
  </form>
  
  <!--3. .监听某个键盘的键帽 -->
  <!-- 监听enter回车键抬起的时候的事件,如果不加.enter则每个键抬起时都会触发keyUp事件 -->
  <input type="text" @keyup.enter="keyUp">
  
  <!--4. .native监听组件根元素的原生事件 -->
  <!-- 给自定义组件添加监听,如果不加.native则该事件不会被触发 -->
  <cpn @click.native="cpnClick"></cpn>
  
  <!--5. .once修饰符的使用-->
  <!--5. 加了.once该事件只触发一次,如只希望用户第一次点击的时候触发,后续再点击则不会触发-->
  <button @click.once="btn2Click">按钮2</button>
  
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    methods: {
      btnClick() {
        console.log("btnClick");
      },
      divClick() {
        console.log("divClick");
      },
      submitClick() {
        console.log('submitClick');
      },
      keyUp() {
        console.log('keyUp');
      },
      cpnClick() {
        console.log('cpnClick')
      },
      btn2Click() {
        console.log('btn2Click');
      }
    }
  })
</script>

6. 条件判断

v-if、v-else-if、v-else
这三个指令与JavaScript的条件语句if、else、else if类似。
Vue的条件指令可以根据表达式的值在DOM中渲染或销毁元素或组件

v-if的使用

<div id="app">
  <h2 v-if="isShow">
    <div>abc</div>
    {{message}}
  </h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isShow: true
    }
  })
</script>

v-if和v-else的使用

<div id="app">
  <h2 v-if="isShow">
    <div>abc</div>
    {{message}}
  </h2>
  // 当在开发者工具下输入 app.isShow = false 时,则显示h1标签
  <h1 v-else>isShow为false时, 显示我</h1>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isShow: true
    }
  })
</script>

v-if和v-else-if和v-else的使用

<div id="app">
  <h2 v-if="score>=90">优秀</h2>
  <h2 v-else-if="score>=80">良好</h2>
  <h2 v-else-if="score>=60">及格</h2>
  <h2 v-else>不及格</h2>

  <h1>{{result}}</h1>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      score: 99
    },
    computed: {
      // 如果判断条件逻辑较多的情况下,不建议在标签上面去写
      // 写在计算属性里直接返回
      result() {
        let showMessage = '';
        if (this.score >= 90) {
          showMessage = '优秀'
        } else if (this.score >= 80) {
          showMessage = '良好'
        }
        // ...
        return showMessage
      }
    }
  })
</script>

用户登录切换案例

  • 小问题:
    如果我们在有输入内容的情况下,切换了类型,我们会发现文字依然显示之前的输入的内容。
    但是按道理讲,我们应该切换到另外一个input元素中了。
    在另一个input元素中,我们并没有输入内容。
    为什么会出现这个问题呢?
  • 问题解答:
    这是因为Vue在进行DOM渲染时,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。
    在上面的案例中,Vue内部会发现原来的input元素不再使用,直接作为else中的input来使用了。
  • 解决方案:
    如果我们不希望Vue出现类似重复利用的问题,可以给对应的input添加key
    并且我们需要保证key的不同
<div id="app">
  <!-- 问题:当在用户账号的的input输入东西后,切换到用户邮箱的时候,输入的内容依然存在 -->
  <!-- 解决:添加key值避免复用,当key不一样的时候就不会去复用,会创建一个新的input -->
  <!-- 这两个label和input是互斥的,不会同时出现 -->
  <!-- 当在渲染的时候,发现渲染的东西是一样的,会将虚拟dom里的显示出来,不会重新创建新的 -->
  <!-- 在渲染的时候是复用的,但是会在虚拟dom的时候去对比,当发现不一样的,会去进行修改并渲染到dom上 -->
  <span v-if="isUser">
    <label for="username">用户账号</label>
    <input type="text" id="username" placeholder="用户账号" key="username">
  </span>
  <span v-else>
    <label for="email">用户邮箱</label>
    <input type="text" id="email" placeholder="用户邮箱" key="email">
  </span>
  <button @click="isUser = !isUser">切换类型</button>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      isUser: true
    }
  })
</script>

v-show的使用

v-show的用法和v-if非常相似,也用于决定一个元素是否渲染:
v-if和v-show对比
v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?
  v-if当条件为false时,压根不会有对应的元素在DOM中。
  v-show当条件为false时,仅仅是将元素的display属性设置为none而已。
开发中如何选择呢?
  当需要在显示与隐藏之间切片很频繁时,使用v-show
  当只有一次切换时,通过使用v-if

<div id="app">
  // <!--v-if: 当条件为false时, 包含v-if指令的元素, 根本就不会存在dom中-->
  <h2 v-if="isShow" id="aaa">{{message}}</h2>

  // <!--v-show: 当条件为false时, v-show只是给我们的元素添加一个行内样式: display: none-->
  <h2 v-show="isShow" id="bbb">{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isShow: true
    }
  })
</script>

7. 循环遍历

v-for遍历数组

格式:v-for="item in items"
拿到元素的索引值格式:v-for="(item, index) in items"

<div id="app">
  // <!--1.在遍历的过程中,没有使用索引值(下标值)-->
  <ul>
    <li v-for="item in names">{{item}}</li>
  </ul>

  // <!--2.在遍历的过程中, 获取索引值-->
  <ul>
    <li v-for="(item, index) in names">
      {{index+1}}.{{item}}
    </li>
  </ul>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      names: ['why', 'kobe', 'james', 'curry']
    }
  })
</script>

v-for遍历对象

获取value格式:v-for="item in info"
获取key和value 格式: v-for="(value, key) in info"
获取key和value和index 格式: v-for="(value, key, index) in info"

<div id="app">
  // <!--1.在遍历对象的过程中, 如果只是获取一个值, 那么获取到的是value-->
  <ul>
    <li v-for="item in info">{{item}}</li>
  </ul>


  // <!--2.获取key和value 格式: (value, key) -->
  <ul>
    <li v-for="(value, key) in info">{{value}}-{{key}}</li>
  </ul>


  // <!--3.获取key和value和index 格式: (value, key, index) -->
  <ul>
    <li v-for="(value, key, index) in info">{{value}}-{{key}}-{{index}}</li>
  </ul>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      info: {
        name: 'why',
        age: 18,
        height: 1.88
      }
    }
  })
</script>

image.png

v-for添加key属性

  • 官方推荐我们在使用v-for时,给对应的元素或组件添加上一个:key属性。
  • 当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点
    我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的。
    即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?
  • 所以我们需要使用key来给每个节点做一个唯一标识
    Diff算法就可以正确的识别此节点
    找到正确的位置区插入新的节点。
  • key的作用主要是为了高效的更新虚拟DOM。

保证key值和展示内容是一一对应的,保证key值的唯一性,绑定index是达不到想要的性能的。
在进行复用的时候:
  绑定了key的时候,优先会把要展示的东西一一对应直接复用,再插入新元素的时候,直接创建一个新的元素并把它插入到指定的位置。
  没有绑定key的时候,就依次往后排,将需要插入的元素放到指定的位置,后面的依次往后排,这样会造成很多数据的改变,没有达到复用的最佳性能。 image.png

app.letters.splice('2', 0, 'F')

<div id="app">
  <ul>
    <li v-for="item in letters" :key="item">{{item}}</li>
  </ul>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      letters: ['A', 'B', 'C', 'D', 'E']
    }
  })
</script>

哪些数组的方法是响应式的

因为Vue是响应式的,所以当数据发生变化时,Vue会自动检测数据变化,视图会发生对应的更新。
JavaScript Array 对象

push()

可向数组的末尾添加一个或多个元素,并返回新的长度。

pop()

用于删除数组的最后一个元素并返回该元素。

shift()

把数组的第一个元素从其中删除,并返回第一个元素的值。

unshift()

可向数组的开头添加一个或更多元素,并返回新的长度。

splice()

splice作用: 删除元素/插入元素/替换元素

sort()

对数组进行排序

reverse()

反转数组

Vue.set()
<div id="app">
  <ul>
    <li v-for="item in letters">{{item}}</li>
  </ul>
  <button @click="btnClick">按钮</button>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      letters: ['a', 'b', 'c', 'd']
    },
    methods: {
      btnClick() {
        // 1.push方法
        // this.letters.push('aaa')
        // this.letters.push('aaaa', 'bbbb', 'cccc')

        // 2.pop(): 删除数组中的最后一个元素
        // this.letters.pop();
        // console.log("this.letters.pop()", this.letters.pop())

        // 3.shift(): 删除数组中的第一个元素
        // this.letters.shift();

        // 4.unshift(): 在数组最前面添加元素
        // this.letters.unshift()
        // this.letters.unshift('aaa', 'bbb', 'ccc')

        // 5.splice作用: 删除元素/插入元素/替换元素
        // 第一个参数是:从第几个元素后面的元素开始操作
        // 删除元素: 第二个参数传入你要删除几个元素(如果没有传,就删除后面所有的元素)
        // 替换元素: 第二个参数, 表示我们要替换几个元素, 后面的参数是用于替换前面的元素
        // 插入元素: 第二个参数, 传入0, 并且后面的参数是要插入的元素
        // splice(start)
        // splice(start):
        // this.letters.splice(1, 3, 'm', 'n', 'l', 'x')
        // this.letters.splice(1, 0, 'x', 'y', 'z')

        // 5.sort() 传参可以传函数
        // this.letters.sort()

        // 6.reverse()
        // this.letters.reverse()

        // 注意: 通过索引值修改数组中的元素,无法响应式更新页面
        // this.letters[0] = 'bbbbbb';
        // 通过splice修改是可以的;vue的set方法也是可以的。
        // this.letters.splice(0, 1, 'bbbbbb')
        // set(要修改的对象, 索引值, 修改后的值)
        Vue.set(this.letters, 0, 'bbbbbb')
      }
    }
  })


  // function sum(num1, num2) {
  //   return num1 + num2
  // }
  //
  // function sum(num1, num2, num3) {
  //   return num1 + num2 + num3
  // }
  // 当需要传多个值的时候直接使用... 传进去的值打印出来是个数组
  // function sum(...num) {
  //   console.log(num); // [20, 30, 40, 50, 601, 111, 122, 33]
  // }
  //
  // sum(20, 30, 40, 50, 601, 111, 122, 33)

</script>

image.png

8. 阶段案例

过滤器的使用

自定义过滤器,可被用于一些常见的文本格式化。
过滤器可以用在两个地方:双花括号插值和 v-bind 表达式
过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:

// <!-- 在双花括号中 -->
{{ message | capitalize }}

// <!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

在组件中定义的局部过滤器

filters: {
  capitalize: function (value) {
    if (!value) return ''
    value = value.toString()
    return value.charAt(0).toUpperCase() + value.slice(1)
  }
}

在创建Vue实例之前定义全局过滤器

Vue.filter('capitalize', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.charAt(0).toUpperCase() + value.slice(1)
})

new Vue({
  // ...
})

当全局过滤器和局部过滤器重名时,会采用局部过滤器。

书籍购物车案例

image.png

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!-- <link rel="stylesheet" href="style.css"> -->
  <style>
    table {
      border: 1px solid #e9e9e9;
      border-collapse: collapse;
      border-spacing: 0;
    }

    th, td {
      padding: 8px 16px;
      border: 1px solid #e9e9e9;
      text-align: left;
    }

    th {
      background-color: #f7f7f7;
      color: #5c6b77;
      font-weight: 600;
    }
  </style>
</head>
<body>

<div id="app">
  <div v-if="books.length">
    <table>
      <thead>
      <tr>
        <th></th>
        <th>书籍名称</th>
        <th>出版日期</th>
        <th>价格</th>
        <th>购买数量</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(item, index) in books">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
        <td>{{item.date}}</td>
        <!-- 通过方法实现格式化 -->
        <!-- <td>{{getFinalPrice(item.price)}}</td> -->
        <!-- | 后面写上过滤器方法 -->
        <td>{{item.price | showPrice}}</td>
        <td>
          <button @click="decrement(index)" v-bind:disabled="item.count <= 1">-</button>
          {{item.count}}
          <button @click="increment(index)">+</button>
        </td>
        <td><button @click="removeHandle(index)">移除</button></td>
      </tr>
      </tbody>
    </table>
    <h2>总价格: {{totalPrice | showPrice}}</h2>
  </div>
  <h2 v-else>购物车为空</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      books: [
        {
          id: 1,
          name: '《算法导论》',
          date: '2006-9',
          price: 85.00,
          count: 1
        },
        {
          id: 2,
          name: '《UNIX编程艺术》',
          date: '2006-2',
          price: 59.00,
          count: 1
        },
        {
          id: 3,
          name: '《编程珠玑》',
          date: '2008-10',
          price: 39.00,
          count: 1
        },
        {
          id: 4,
          name: '《代码大全》',
          date: '2006-3',
          price: 128.00,
          count: 1
        },
      ]
    },
    methods: {
      // 给价格前面加符号保留两位小数点
      // getFinalPrice(price) {
      //   return '¥' + price.toFixed(2)
      // },
      decrement (index) {
        this.books[index].count--
      },
      increment (index) {
        this.books[index].count++
      },
      removeHandle (index) {
        this.books.splice(index, 1)
      }
    },
    computed: {
      totalPrice () {
        let totalPrice = 0
        for (let i = 0; i < this.books.length; i++) {
          totalPrice += this.books[i].price * this.books[i].count
        }
        return totalPrice
        //  通过高阶函数实现
        //  return this.books.reduce(function (preValue, book) {
        //   return preValue + book.price * book.count
        // }, 0)
      }
    },
    // 过滤器 有多个,是个函数
    filters: {
      showPrice (price) {
        return '¥' + price.toFixed(2)
      }
    }
  })
</script>
</body>
</html>

高阶函数

filter()
map()
reduce()

需求1:取出所有小于100的数字
需求2:将所有小于100的数字进行转化: 全部*2
需求3:将所有new2Nums数字相加,得到最终的结果

最初的实现方式:

const nums = [10, 20, 111, 222, 444, 40, 50]
// 1.需求: 取出所有小于100的数字
let newNums = []
for (let n of nums) {
  if (n < 100) {
    newNums.push(n)
  }
}

// 2.需求:将所有小于100的数字进行转化: 全部*2
let new2Nums = []
for (let n of newNums) {
  new2Nums.push(n * 2)
}
console.log(new2Nums);

// 3.需求:将所有new2Nums数字相加,得到最终的结果
let total = 0
for (let n of new2Nums) {
  total += n
}

console.log(total);

使用高阶函数

// filter中的回调函数有一个要求: 必须返回一个boolean值
// true: 当返回true时, 函数内部会自动将这次回调时传入的n加入到新的数组中 // 内部会创建一个新数组
// false: 当返回false时, 函数内部会过滤掉这次的n
const nums = [10, 20, 111, 222, 444, 40, 50]
let newNums = []
// filter内部会自动创建一个数组,这里只需要接收即可
// 如果函数里返回的是true,就会把这个n添加到数组里面;如果是false,就不会把这个n添加到数组里面
// 新的数组会作为filter这个大函数的返回值,只需要在前面接收一下就好
newNums = nums.filter(function (n) {
  // return true
  return n < 100
})
// 1.filter函数的使用
// 10, 20, 40, 50
let newNums = nums.filter(function (n) {
  return n < 100
})
console.log(newNums);

// 2.map函数的使用
// 如果想对每一个数字进行变化的时候使用map
// 20, 40, 80, 100
let new2Nums = newNums.map(function (n) { // 20
  return n * 2
})
console.log(new2Nums);

// 3.reduce函数的使用
// reduce作用对数组中所有的内容进行汇总
 // 240
// 有两个参数:参数一是函数,参数二可选 传递给函数的初始值
// 参数一中:preValue 是上一次return返回的值  n就是数组中的每一个值
let total = new2Nums.reduce(function (preValue, n) {
  return preValue + n
}, 0)
console.log(total);

// 第一次: preValue 0 n 20
// 第二次: preValue 20 n 40
// 第三次: preValue 60 n 80
// 第四次: preValue 140 n 100
// 240
// 将上面的三个高阶函数放在一起。函数式编程
// 240
let total = nums.filter(function (n) {
  return n < 100
}).map(function (n) {
  return n * 2
}).reduce(function (prevValue, n) {
  return prevValue + n
}, 0)
console.log(total);
// 将上面的三个高阶函数放在一起。函数式编程
// 240
let total = nums.filter(function (n) {
  return n < 100
}).map(function (n) {
  return n * 2
}).reduce(function (prevValue, n) {
  return prevValue + n
}, 0)
console.log(total);
// 通过箭头函数更加简洁
// 240
let total = nums.filter(n => n < 100).map(n => n * 2).reduce((pre, n) => pre + n);
console.log(total);

9. 表单绑定

v-model的基本使用

  • 表单控件在实际开发中是非常常见的。特别是对于用户信息的提交,需要大量的表单。
  • Vue中使用v-model指令来实现表单元素和数据的双向绑定。
  • 案例的解析:
  1. 当我们在输入框输入内容时
  2. 因为input中的v-model绑定了message,所以会实时将输入的内容传递给message,message发生改变。
  3. 当message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。
  4. 所以,通过v-model实现了双向的绑定。
  • 当然,我们也可以将v-model用于textarea元素
<div id="app">
  <input type="text" v-model="message">
  {{message}}
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    }
  })
</script>

v-model原理

v-model其实是一个语法糖,它的背后本质上是包含两个操作:
1.v-bind绑定一个value属性
2.v-on指令给当前元素绑定input事件

<div id="app">
  <!--<input type="text" v-model="message">-->
  <!-- v-on:input 是input内部的监听事件,当input发生改变的时候触发 -->
  <!--<input type="text" :value="message" @input="valueChange">-->
  <input type="text" :value="message" @input="message = $event.target.value">
  <h2>{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    methods: {
      valueChange(event) {
        this.message = event.target.value;
      }
    }
  })
</script>

v-model结合radio类型

当存在多个单选框时

<div id="app">
  <label for="male">
    <input type="radio" id="male" value="男" v-model="sex"></label>
  <label for="female">
    <input type="radio" id="female" value="女" v-model="sex"></label>
  <h2>您选择的性别是: {{sex}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      // 通过v-model,把radio选择的东西绑定到这个变量里,之后可以把这个发送到服务器
      // 当v-model绑定的是同一个变量的时候,name=“sex”可以没有。最初只有加上name单选才能互斥,现在绑定v-model也能达成互斥
      sex: '女'
    }
  })
</script>

v-model结合checkbox类型

  • 复选框分为两种情况:单个勾选框和多个勾选框
  • 单个勾选框:
  1. v-model即为布尔值。
  2. 此时input的value并不影响v-model的值。
  • 多个复选框:
  1. 当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
  2. 当选中某一个时,就会将input的value添加到数组中。
<div id="app">
  <!--1.checkbox单选框-->
  <label for="agree">
    <input type="checkbox" id="agree" v-model="isAgree">同意协议
  </label>
  <h2>您选择的是: {{isAgree}}</h2>
  <button :disabled="!isAgree">下一步</button>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isAgree: false, // 单选框
    }
  })
</script>
<div id="app">
  <!--2.checkbox多选框-->
  <input type="checkbox" value="篮球" v-model="hobbies">篮球
  <input type="checkbox" value="足球" v-model="hobbies">足球
  <input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
  <input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
  <h2>您的爱好是: {{hobbies}}</h2>
  
  
  <!-- 值绑定:就是动态的给value赋值
              上面的value中的值,都是在定义input的时候直接给定的。
              但是真实开发中,这些input的值可能是从网络获取或定义在data中的。
              所以我们可以通过v-bind:value动态的给value绑定值。 -->
  <label v-for="item in originHobbies" :for="item">
    <input type="checkbox" :value="item" :id="item" v-model="hobbies">{{item}}
  </label>

</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      hobbies: [], // 多选框,
      
      originHobbies: ['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
    }
  })
</script>

v-model结合select类型

<div id="app">
  <!--1.选择一个-->
  <select name="abc" v-model="fruit">
    <option value="苹果">苹果</option>
    <option value="香蕉">香蕉</option>
    <option value="榴莲">榴莲</option>
    <option value="葡萄">葡萄</option>
  </select>
  <h2>您选择的水果是: {{fruit}}</h2>

  <!--2.选择多个-->
  <!-- 加上multiple属性就可以选择多个 -->
  <select name="abc" v-model="fruits" multiple>
    <option value="苹果">苹果</option>
    <option value="香蕉">香蕉</option>
    <option value="榴莲">榴莲</option>
    <option value="葡萄">葡萄</option>
  </select>
  <h2>您选择的水果是: {{fruits}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      fruit: '香蕉',
      fruits: []
    }
  })
</script>

v-model修饰符的使用

lazy修饰符

默认情况下,v-model默认是在input事件中同步输入框的数据的。
也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。
lazy修饰符可以让数据在失去焦点或者回车时才会更新

number修饰符

默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。
但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。
number修饰符可以让在输入框中输入的内容自动转成数字类型

trim修饰符

如果输入的内容首尾有很多空格,通常我们希望将其去除
trim修饰符可以过滤内容左右两边的空格

<div id="app">
  <!--1.修饰符: lazy-->
  <input type="text" v-model.lazy="message">
  <h2>{{message}}</h2>


  <!--2.修饰符: number-->
  <!-- typeof检测数据类型 -->
  <input type="number" v-model.number="age">
  <h2>{{age}}-{{typeof age}}</h2>

  <!--3.修饰符: trim-->
  <input type="text" v-model.trim="name">
  <h2>您输入的名字:{{name}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      age: 0,
      name: ''
    }
  })
</script>

三、组件化开发

1. 认识组件化

什么是组件化

  • 组件化思想:
  1. 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
  2. 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。

image.png

image.png

Vue组件化思想

  • 组件化是Vue.js中的重要思想
    它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    任何的应用都会被抽象成一颗组件树。

image.png

  • 组件化思想的应用:
    1. 有了组件化的思想,我们在之后的开发中就要充分的利用它。
    2. 尽可能的将页面拆分成一个个小的、可复用的组件。
    3. 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
  • 所以,组件是Vue开发中,非常重要的一个篇章,要认真学习。

2. 组件化基础

注册组件

注册的基本步骤
  • 组件的使用分成三个步骤:
    1. 创建组件构造器
    2. 注册组件
    3. 使用组件。 image.png
  • 步骤解析
    1. Vue.extend():
      调用Vue.extend()创建的是一个组件构造器。
      通常在创建组件构造器时,传入template代表我们自定义组件的模板。
      该模板就是在使用到组件的地方,要显示的HTML代码。
      事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。
    2. Vue.component():
      调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
      所以需要传递两个参数:
      2-1、注册组件的标签名
      2-2、组件构造器
    3. 组件必须挂载在某个Vue实例下,否则它不会生效。
      我们来看下面我使用了三次 而第三次其实并没有生效:

image.png

组件化的基本使用
<div id="app">
  <!--3.使用组件-->
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>

  <div>
    <div>
      <my-cpn></my-cpn>
    </div>
  </div>
</div>

<my-cpn></my-cpn>

<script src="../js/vue.js"></script>
<script>
  // 1.创建组件构造器对象
  const cpnC = Vue.extend({
    template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容, 哈哈哈哈</p>
        <p>我是内容, 呵呵呵呵</p>
      </div>`
  })

  // 2.注册组件
  Vue.component('my-cpn', cpnC)

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    }
  })
</script>
全局组件和局部组件
  • 当我们通过调用Vue.component('cpn', cpnC)注册组件时,组件的注册是全局
      这意味着该组件可以在任意Vue示例下使用。
  • 如果我们注册的组件是挂载在某个实例中components: {cpn: cpnC}, 那么就是一个局部组件
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>

<div id="app2">
  <cpn></cpn>
</div>

<script src="../js/vue.js"></script>
<script>
  // 1.创建组件构造器
  const cpnC = Vue.extend({
    template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容,哈哈哈哈啊</p>
      </div>
    `
  })

  // 2.注册组件(全局组件, 意味着可以在多个Vue的实例下面使用)
  // Vue.component('cpn', cpnC)

  // 疑问: 怎么注册的组件才是局部组件了?

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      // cpn使用组件时的标签名
      cpn: cpnC
    }
  })

  const app2 = new Vue({
    el: '#app2'
  })
</script>
父组件和子组件
<div id="app">
  <cpn2></cpn2>
  <!--<cpn1></cpn1>-->
</div>

<script src="../js/vue.js"></script>
<script>
  // 1.创建第一个组件构造器(子组件)
  const cpnC1 = Vue.extend({
    template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
  })
    // 
  // 2.创建第二个组件构造器(父组件)
  const cpnC2 = Vue.extend({
    template: `
      <div>
        <h2>我是标题2</h2>
        <p>我是内容, 呵呵呵呵</p>
        <cpn1></cpn1>
      </div>
    `,
    // 将组件1作为组件2的子组件
    components: {
      cpn1: cpnC1
    }
  })

  // root组件
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn2: cpnC2,
      // cpn1: cpnC1
    }
  })
</script>
注册组件语法糖

Vue为了简化这个过程,提供了注册的语法糖
主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。

<div id="app">
  <cpn1></cpn1>
  <cpn2></cpn2>
</div>

<script src="../js/vue.js"></script>
<script>
  // 1.全局组件注册的语法糖
  // 1.创建组件构造器
  // const cpn1 = Vue.extend()

  // 2.注册全局组件的语法糖
  Vue.component('cpn1', {
    template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
  })

  // 2.注册局部组件的语法糖
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      'cpn2': {
        template: `
          <div>
            <h2>我是标题2</h2>
            <p>我是内容, 呵呵呵</p>
          </div>
    `
      }
    }
  })
</script>
组件模版的分离写法
  • 上面通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。
  • 如果能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。
  • Vue提供了两种方案来定义HTML模块内容:
    使用<script>标签
    使用<template>标签
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>

<!--1.script标签, 注意:类型必须是text/x-template-->
<script type="text/x-template" id="cpn">
<div>
  <h2>我是标题</h2>
  <p>我是内容,哈哈哈</p>
</div>
</script>

<!--2.template标签-->
<template id="cpn">
  <div>
    <h2>我是标题</h2>
    <p>我是内容,呵呵呵</p>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>

  // 1.注册一个全局组件
  Vue.component('cpn', {
    template: '#cpn'
  })

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    }
  })
</script>
组件的其他属性data(){}
  • 组件是一个单独功能模块的封装
    这个模块有属于自己的HTML模板,也应该有属性自己的数据data
  • 组件内部是不能直接访问vue实例里的数据
  • Vue组件应该有自己保存数据的地方
    组件自己的数据存放在哪里呢?
    组件对象也有一个data属性(也可以有methods等属性)
    只是这个data属性必须是一个函数
    而且这个函数返回一个对象,对象内部保存着数据
<div id="app">
  <cpn></cpn>
</div>
<template id="cpn">
  <div>
    <h2>{{title}}</h2>
    <p>我是内容,呵呵呵</p>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>

  // 1.注册一个全局组件
  // data必须是一个函数,然后返回一个对象
  Vue.component('cpn', {
    template: '#cpn',
    data() {
      return {
        title: 'abc'
      }
    }
  })

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      无法在组件中直接访问,会报错
      // title: '我是标题'
    }
  })
</script>
  • 组件中的data为什么必须是函数?
  1. 首先,如果不是一个函数,Vue直接就会报错。
  2. 其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
// 当改变函数里返回的对象里数据时,不会改变其他调用该函数的数据
<script>
  // const obj = {
  //   name: 'why',
  //   age: 18
  // }
  // 直接返回一个对象的话,会出现报错
  // function abc() {
  //   return obj
  // }
  function abc() {
    return {
      name: 'why',
      age: 18
    }
  }
  let obj1 = abc()
  let obj2 = abc()
  let obj3 = abc()
  
  obj1.name = 'kobe'
  console.log(obj1);
  console.log(obj2);
  console.log(obj3);
</script>

image.png

数据传递

如何进行父子组件间的通信呢?Vue官方提到:

  • 通过props向子组件传递数据
  • 通过事件向父组件发送消息
    image.png

在下面的代码中,我直接将Vue实例当做父组件,并且其中包含子组件来简化代码。
真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的

父传子
  • 在开发中,往往一些数据确实需要从上层传递到下层:
  1. 比如在一个页面中,从服务器请求到了很多的数据。
  2. 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
  3. 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)
  • 在组件中,使用选项props来声明需要从父级接收到的数据
  • props的值有两种方式:
    1、方式一:字符串数组,数组中的字符串就是传递时的名称。
    2、方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
数组形式
<div id="app">
  <!-- 2. 通过v-bind绑定变量,将父组件中的message数据绑定给子组件props接收时定义的变量cmessage -->
  <cpn v-bind:cmovies="movies" :cmessage="message"></cpn>
</div>

<template id="cpn">
  <div>
    <!-- <p>{{cmovies}}</p> -->
    <ul>
      <li v-for="item in cmovies">{{item}}</li>
    </ul>
    <!-- 3. 这样在子组件中就可以直接使用props中的变量了 -->
    <h2>{{cmessage}}</h2>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  // 父传子: props
  const cpn = {
    template: '#cpn',
    // 数组形式
    // 1. 通过pops定义变量接受父组件传的数据
    props: ['cmovies', 'cmessage'],
    data() {
      return {}
    },
    methods: {

    }
  }

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      movies: ['海王', '海贼王', '海尔兄弟']
    },
    components: {
      cpn
    }
  })
</script>

image.png

对象形式
<div id="app">
  <cpn :cmessage="message" :cmovies="movies"></cpn>
</div>

<template id="cpn">
  <div>
    <ul>
      <li v-for="item in cmovies">{{item}}</li>
    </ul>
    <h2>{{cmessage}}</h2>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  // 父传子: props
  const cpn = {
    template: '#cpn',
    // 对象形式
    props: {
      // 1.类型限制
      // cmovies: Array,
      // cmessage: String,

      // 2.提供type数据类型,
      //   默认值default在没有传值的情况下显示, 
      //   必传值required,如果其他地方使用cpn组件时,那么cmessage就是必须传的,不传会报错
      cmessage: {
        type: String,
        default: 'aaaaaaaa',
        required: true
      },
      // 类型是对象或者数组时, 默认值必须是一个函数。如果这么写默认值的话default: []会报错
      cmovies: {
        type: Array,
        default() {
          return []
        }
      }
    },
    data() {
      return {}
    },
    methods: {

    }
  }

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      movies: ['海王', '海贼王', '海尔兄弟']
    },
    components: {
      cpn
    }
  })
</script>
props数据验证

当需要对props进行类型等验证时,就需要对象写法了。
验证都支持以下数据类型

  1. String
  2. Number
  3. Boolean
  4. Array
  5. Object
  6. Date
  7. Function
  8. Symbol

image.png

有自定义构造函数时,验证也支持自定义的类型 image.png

props中的驼峰标识
<div id="app">
  <!-- 目前v-bind不支持驼峰:这里会显示个空对象 -->
  <!-- <cpn :cInfo="info"></cpn> -->
  <cpn :c-info="info" :child-my-message="message"></cpn>
</div>

<template id="cpn">
  <!-- 如果在组件里标签比较多的话,需要有一个根标签包含所有的标签 -->
  <div>
    <h2>{{cInfo}}</h2>
    <h2>{{childMyMessage}}</h2>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const cpn = {
    template: '#cpn',
    props: {
      cInfo: {
        type: Object,
        default() {
          return {}
        }
      },
      childMyMessage: {
        type: String,
        default: ''
      }
    }
  }

  const app = new Vue({
    el: '#app',
    data: {
      info: {
        name: 'why',
        age: 18,
        height: 1.88
      },
      message: 'aaaaaa'
    },
    components: {
      cpn
    }
  })
</script>
子传父
  • 什么时候需要自定义事件呢?
  1. 当子组件需要向父组件传递数据时,就要用到自定义事件了。
  2. 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
  • 自定义事件的流程:
  1. 在子组件中,通过$emit()来触发事件。
  2. 在父组件中,通过v-on来监听子组件事件。
<!--父组件模板-->
<div id="app">
  <!-- 2、父组件监听事件 -->
  <cpn @item-click="cpnClick"></cpn>
</div>

<!--子组件模板-->
<template id="cpn"> 
  <div>
    <button v-for="item in categories" @click="btnClick(item)">
      {{item.name}}
    </button>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>

  // 1.子组件
  const cpn = {
    template: '#cpn',
    data() {
      return {
        categories: [
          {id: 'aaa', name: '热门推荐'},
          {id: 'bbb', name: '手机数码'},
          {id: 'ccc', name: '家用家电'},
          {id: 'ddd', name: '电脑办公'},
        ]
      }
    },
    methods: {
      btnClick(item) {
        // 1、子组件发射事件: 自定义事件 
        // 参数一:自定义事件名字 参数二:自定义事件的参数
        this.$emit('item-click', item)
      }
    }
  }

  // 2.父组件
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn
    },
    methods: {
      cpnClick(item) {
        console.log('cpnClick', item);
      }
    }
  })
</script>
父子组件传值案例:

需求:

  1. props显示父组件传过来的值;
  2. data显示子组件自身data中的属性值;
  3. 输入框则改变输入内容时,改变子组件data里的值;也要改变子组件从父组件接收的props中的值;并且当第一个输入框值改变时,第二个数值是第一个数值的100倍,当第二个输入框值改变时,第一个数值是第二个数值的一百分之一。

image.png

<div id="app">
  <cpn :number1="num1" :number2="num2" @num1change="getnum1change" @num2change="getnum2change"></cpn>
</div>

<template id="cpn">
  <div>
    <h2>props: {{number1}}</h2>
    <h2>data: {{dnumber1}}</h2>
     <!-- 双向绑定控制台报错。要使用data中的属性,再去改变其值 -->
    <!-- <input type="text" v-model="dnumber1"> -->
    <input type="text" :value="dnumber1" @input="num1Input">
    
    <h2>props: {{number2}}</h2>
    <h2>data: {{dnumber2}}</h2>
    <!-- <input type="text" v-model="dnumber2"> -->
    <input type="text" :value="dnumber2" @input="num2Input">
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      num1: 1,
      num2: 2,
    },
    methods: {
      getnum1change (value) {
        console.log('getnum1change的值', value)
        this.num1 = parseFloat(value)
      },
      getnum2change (value) {
        this.num2 = parseFloat(value)
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        props: {
          number1: Number,
          number2: Number
        },
        data () {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        methods: {
          num1Input (event) {
            console.log('num1Input的event', event.target.value)
            this.dnumber1 = event.target.value
            // 将输入的值发射事件携带参数值给父组件
            this.$emit('num1change', this.dnumber1)
            // 当dnumber1改变时,dnumber2是dnumber1的 100倍;
            this.dnumber2 = this.dnumber1*100
            this.$emit('num2change', this.dnumber2)
          },
          num2Input (event) {
            this.dnumber2 = event.target.value
            this.$emit('num2change', this.dnumber2)
            // 当dnumber2改变时,dnumber1是dnumber2的 1/100
            this.dnumber1 = this.dnumber2/100
            this.$emit('num1change', this.dnumber1)
          }
        }
      }
    }
  })
</script>

通过watch侦听器实现:

<div id="app">
  <cpn :number1="num1" :number2="num2" @num1change="getnum1change" @num2change="getnum2change"></cpn>
</div>

<template id="cpn">
  <div>
    <h2>props: {{number1}}</h2>
    <h2>data: {{dnumber1}}</h2>
    <input type="text" v-model="dnumber1">
    
    <h2>props: {{number2}}</h2>
    <h2>data: {{dnumber2}}</h2>
    <input type="text" v-model="dnumber2">
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      num1: 1,
      num2: 2,
    },
    methods: {
      getnum1change (value) {
        this.num1 = parseFloat(value)
      },
      getnum2change (value) {
        this.num2 = parseFloat(value)
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        props: {
          number1: Number,
          number2: Number
        },
        data () {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },

        watch: {
          dnumber1 (newValue, oldValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue)
          },
          dnumber2 (newValue, oldValue) {
            this.dnumber1 = newValue / 100;
            this.$emit('num2change', newValue);
          }
        }
      }
    }
  })
</script>
父子组件的访问方式

  有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问g根组件。

  1. 父组件访问子组件:使用$children$refs
  2. 子组件访问父组件:使用$parent

父访子$children$refs
  • 通过$children访问子组件 this.$children是一个数组类型,它包含所有子组件对象
    我们这里通过一个遍历,取出所有子组件的message状态。

image.png

<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <button @click="btnClick">按钮</button>
</div>

<template id="cpn">
  <div>我是子组件</div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    methods: {
      btnClick() {
        // 可以拿到子组件,调用子组件中的方法;一般不使用它去拿子组件的东西,除非是去拿所有子组件的时候。
        // 因为当组件顺序发生变化时通过下标获取的组件不准确
        // 1.$children
        console.log(this.$children);
        for (let c of this.$children) {
          console.log(c.name);
          c.showMessage();
        }
        // console.log(this.$children[1].name);
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是子组件的name'
          }
        },
        methods: {
          showMessage() {
            console.log('showMessage');
          }
        }
      },
    }
  })
</script>
  • 通过$refs访问子组件 $children的缺陷:
  1. 通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
  2. 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
  3. 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs $refs的使用:
  4. $refsref指令通常是一起使用的。
  5. 首先,我们通过ref给某一个子组件绑定一个特定的ID。
  6. 其次,通过this.$refs.ID就可以访问到该组件了。

image.png

<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <my-cpn></my-cpn>
  <y-cpn></y-cpn>
  <!-- 首先,我们通过ref给某一个子组件绑定一个特定的ID -->
  <cpn ref="aaa"></cpn>
  <button @click="btnClick">按钮</button>
</div>

<template id="cpn">
  <div>我是子组件</div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    methods: {
      btnClick() {
         // 其次,通过`this.$refs.ID`就可以访问到该组件了。
        // 它可以拿到指定的某一个子组件,需要给子组件绑定 ref="自定义名字"
        // 2.$refs => 对象类型, 默认是一个空的对象 ref='bbb'
        console.log(this.$refs.aaa.name);
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是子组件的name'
          }
        },
        methods: {
          showMessage() {
            console.log('showMessage');
          }
        }
      },
    }
  })
</script>
子访父$parent

如果我们想在子组件中直接访问父组件,可以通过$parent
注意事项:

  1. 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。
  2. 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
  3. 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
  4. 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。
<div id="app">
  <cpn></cpn>
</div>

<template id="cpn">
  <div>
    <h2>我是cpn组件</h2>
    <ccpn></ccpn>
  </div>
</template>

<template id="ccpn">
  <div>
    <h2>我是子组件</h2>
    <button @click="btnClick">按钮</button>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是cpn组件的name'
          }
        },
        components: {
          ccpn: {
            template: '#ccpn',
            methods: {
              btnClick() {
                // 不建议子组件访问父组件,组件主要用于复用,当在此处使用取父组件中的name属性,可以取到;
                //  如果放到其他地方使用其父组件可能没有name属性,此时会报错。和父组件耦合度太高,复用性不强了。
                // 1.访问父组件$parent
                //   此时访问到vue组件 cpn组件
                // console.log(this.$parent);
                // console.log(this.$parent.name);

                // 2.访问根组件$root
                //  此时会访问到vue实例
                console.log(this.$root);
                console.log(this.$root.message);
              }
            }
          }
        }
      }
    }
  })
</script>

3. 组件化高级

插槽slot

为什么使用slot
  • slot翻译为插槽:
  1. 在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
  2. 插槽的目的是让我们原来的设备具备更多的扩展性
  3. 比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
  • 组件的插槽:
  1. 组件的插槽也是为了让我们封装的组件更加具有扩展性。
  2. 让使用者可以决定组件内部的一些内容到底展示什么。
  • 例子:移动网站中的导航栏。
  1. 移动开发中,几乎每个页面都有导航栏。
  2. 导航栏我们必然会封装成一个插件,比如nav-bar组件。
  3. 一旦有了这个组件,我们就可以在多个页面中复用了。
  4. 但是,每个页面的导航是一样的吗?No,我以京东M站为例

image.png

  • 如何去封装这类的组件呢?
  1. 它们也很多区别,但是也有很多共性。
  2. 如果,我们每一个单独去封装一个组件,显然不合适:比如每个页面都返回,这部分内容我们就要重复去封装。
  3. 但是,如果我们封装成一个,好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等。
  • 如何封装合适呢?抽取共性,保留不同
  1. 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
  2. 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
  3. 是搜索框,还是文字,还是菜单。由调用者自己来决定。
slot基本使用
  • 了解了为什么用slot,我们再来谈谈如何使用slot?
    • 在子组件中,使用特殊的元素就可以为子组件开启一个插槽。
    • 该插槽插入什么内容取决于父组件如何使用。
  • 我们通过一个简单的例子,来给子组件定义一个插槽:
    • 中的内容表示,如果没有在该组件中插入任何其他内容,就默认显示该内容
    • 有了这个插槽后,父组件如何使用呢?

image.png

具名插槽
  • 当子组件的功能复杂时,子组件的插槽可能并非是一个。
    • 比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
    • 那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
    • 这个时候,我们就需要给插槽起一个名字
  • 如何使用具名插槽呢?
    • 非常简单,只要给slot元素一个name属性即可
    • <slot name='myslot'></slot>
<div id="app">
  <cpn><span slot="center">标题</span></cpn>
  <cpn><button slot="left">返回</button></cpn>
  <cpn><button slot="left">返回</button><input value="请输入..." type="text" slot="center"></input><span slot="right">搜索</span></cpn>
</div>


<template id="cpn">
  <div>
    <slot name="left"><span>左边</span></slot>
    <slot name="center"><span>中间</span></slot>
    <slot name="right"><span>右边</span></slot>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn: {
        template: '#cpn'
      }
    }
  })
</script>

image.png

编译作用域
  • 官方给出了一条准则:
    父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
    • 而我们在使用的时候,整个组件的使用过程是相当于在父组件中出现的。
    • 那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
    • 因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。
<div id="app">
  <!-- 此时会使用Vue实例的属性 -->
  <!-- 在查找变量的时候,都会看一下在哪一个模版里,此时在Vue模板里 为true -->
  <cpn v-show="isShow"></cpn>
  <cpn v-for="item in names"></cpn>
</div>

<template id="cpn">
  <div>
    <h2>我是子组件</h2>
    <p>我是内容, 哈哈哈</p>
    <!-- 在自己作用域里去查找变量 为false -->
    <button v-show="isShow">按钮</button>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isShow: true
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            isShow: false
          }
        }
      },
    }
  })
</script>
slot作用域插槽
  • 总结:父组件替换插槽的标签,但是内容由子组件来提供。

image.png

<div id="app">
  <cpn></cpn>

  <!-- 父组件使用子组件时,对子组件中的内容展示不满意,想修改成自己所需的展示格式 -->
  <cpn>
    <!--必须加个template 目的是获取子组件中的pLanguages-->
    <!-- slot-scope="名字" 起个名字叫slot随意起,通过slot引用插槽对象,通过slot.data拿到数据 -->
    <template slot-scope="slot">
      <span>{{slot.data.join(' - ')}}</span>
    </template>
  </cpn>

  <cpn>
    <template slot-scope="slot">
      <span>{{slot.data.join(' * ')}}</span>
    </template>
  </cpn>
</div>

<template id="cpn">
  <div>
    <!-- :data为绑定的数据,:后可以随意起名。就可以在cpn组件中使用data -->
    <slot :data="pLanguages">
      <ul>
        <li v-for="item in pLanguages">{{item}}</li>
      </ul>
    </slot>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
          }
        }
      }
    }
  })
</script>

四、模块化开发

最初JavaScript只做一些简单的工作,随着代码量的增多,通常将代码组织在多个js文件中,会发现难以维护,如:全局变量同名问题,js文件引入顺序执行顺序的问题。 为了避免各种问题,便于维护,出现了模块化。

  • 前端模块化的一些方案:AMD、CMD、CommonJS、ES6。 前三个需要底层的支撑,比如在webpack中就可以使用,webpack就可以作为底层的支撑(支持各种模块化如以上三个),因为webpack会对其代码转成浏览器可以支持识别的代码;而ES6是浏览器可以直接解析的。

1. 匿名函数+模块出口

首先通过匿名函数解决重名问题;

image.png 其次当使用了匿名函数后,该js文件里的变量则为局部变量,其他文件无法访问。此时需要使用模块作为出口。

  1. 在匿名函数内部,定义一个对象。
  2. 给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可)。
  3. 最后将这个对象返回,并且在外面使用了一个MoudleA接受。

image.png

2. CommonJS

此语法在一些环境下才可使用,如:webpack

image.png

3. ES6的Modules(export/import)

export导出变量

image.png

export导出函数或类

image.png

export default 导入者自定义名称

image.png

import使用

image.png

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<!-- 类型需要设置为module,只有type="module"才支持这种导入导出语法 -->
<script src="aaa.js" type="module"></script>
<script src="bbb.js" type="module"></script>
<script src="mmm.js" type="module"></script>
</body>
</html>

aaa.js

var flag = true

function sum(num1, num2) {
  return num1 + num2
}

if (flag) {
  console.log(sum(20, 30));
}

// 1.导出方式一:
export { flag, sum }

// 2.导出方式二:
export var num1 = 1000;
export var height = 1.88

// 3.导出函数/类
export function mul(num1, num2) {
  return num1 * num2
}

export class Person {
  run() {
    console.log('在奔跑');
  }
}

// 5.export default 导入者自己命名
// export default在同一个模块中,不允许同时存在多个
export default function (argument) {
  console.log(argument);
}

bbb.js

// 1.导入的{}中定义的变量
import {flag, sum} from "./aaa.js";

if (flag) {
  console.log('小明是天才, 哈哈哈');
  console.log(sum(20, 30));
}

// 2.直接导入export定义的变量
import {num1, height} from "./aaa.js";

console.log(num1);
console.log(height);

// 3.导入 export的function/class
import {mul, Person} from "./aaa.js";

console.log(mul(30, 50));

const p = new Person();
p.run()

// 4.导入 export default中的内容
import addr from "./aaa.js";

addr('你好啊');

// 5.统一全部导入 通过*可以导入模块中所有的export变量;给*起一个别名,方便后续的使用
// import {flag, num, num1, height, Person, mul, sum} from "./aaa.js";

import * as aaa from './aaa.js'

console.log(aaa.flag);
console.log(aaa.height);

4. AMD、CMD

五、Vue CLI详解

1. webpack

在开发里不会从头到尾手写webpack的配置而是通过工具脚手架自动生成webpack的配置

2. Vue CLI

Vue CLI官方文档

Vue CLI是什么

  • 如果你在开发大型项目, 那么你需要, 并且必然需要使用Vue CLI
    • 使用Vue.js开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。
    • 如果每个项目都要手动完成这些工作,那无疑效率比较低效,所以通常我们会使用一些脚手架工具来帮助完成这些事情。
  • CLI是什么意思?
    • CLI是Command-Line Interface, 翻译为命令行界面, 但是俗称脚手架.
    • Vue CLI是一个官方发布 vue.js 项目脚手架
    • 使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置.

Vue CLI依赖环境

Vue CLI使用前提 - Node
  • 安装NodeJS

  • 检测安装的版本

    • 默认情况下自动安装Node和NPM
    • Node环境要求8.9以上或者更高版本
      node -v
      npm -v
  • 什么是NPM呢?

    • NPM的全称是Node Package Manager
    • 是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。
    • 后续我们会经常使用NPM来安装一些开发过程中依赖包.
  • cnpm安装

    • 由于国内直接使用 npm 的官方镜像是非常慢的,这里推荐使用淘宝 NPM 镜像。
    • 可以使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm:
      npm install -g cnpm --registry=https://registry.npm.taobao.org
    • 这样就可以使用 cnpm 命令来安装模块了:
      cnpm install
Vue CLI使用前提 - Webpack

因为脚手架自动生成webpack的相关配置,所以肯定也要依赖于webpack

  • Vue.js官方脚手架工具就使用了webpack模板
    • 对所有的资源会压缩等优化操作
    • 它在开发过程中提供了一套完整的功能,能够使得我们开发过程中变得高效。
  • Webpack的全局安装
    npm install webpack -g

Vue CLI的安装

  • 安装Vue脚手架
    npm install -g @vue/cli

    image.png
    注意:上面安装默认的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目是不可以的。

  • Vue CLI3初始化项目
    vue create my-project

  • Vue CLI2初始化项目
    vue init webpack my-project

    • 如果想使用Vue CLI2命令初始化项目需要:拉取 2.x 模板 (旧版本)
      npm install -g @vue/cli-init 此时再init即可初始化项目

      image.png

3. Vue CLI2的使用

node为js提供了一个运行环境;之前只能在浏览器上执行

(1)Vue CLI2 配置过程

  • 初始化项目

    image.png

(2)Vue CLI2 目录结构

  • 各文件对应的作用

    image.png

(3)入口文件详解

(3.1)Runtime-Compiler和Runtime-only的区别
  • 主要区别是main.js,Runtime-only会比较轻、性能更高。

    image.png

(3.2)render和template

image.png

(3.3)Vue渲染过程

image.png

(3.4)render函数的使用

image.png

image.png

(4)webpack配置解析

4. Vue CLI3的使用

  • vue-cli 3 与 2 版本有很大区别
    1. vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
    2. vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,build和config等目录
    3. vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
    4. 移除了static文件夹,新增了public文件夹,并且index.html移动到public中

(1)Vue CLI3 配置过程

image.png

(2)Vue CLI3 目录结构

脚手架3会默认创建一个.git的文件夹

image.png

image.png

image.png

(3)Vue CLI配置修改

Vue CLI3想要修改配置的话可以通过三种方案

  1. 通过Vue UI启动一个本地服务器,打开网页,就可以进行图形化界面的方式修改对应的相关配置、查看当前项目依赖什么、运行项目等。
  2. 可以在node_moudles/@vue/cli-service/lib/Service.js文件里,看到相关配置
  3. 在当前项目下创建一个文件vue.config.js,通过module.exports = {}导出配置,它会自动和默认的配置进行合并。
(3.1)启动配置服务器 Vue UI

通过vue ui启动本地服务视频讲解
在安装vue-cli脚手架的时候,npm install @vue/cli -g 同时还安装了vue 除了可以使用命令vue init 和vue creat之外 还可以执行vue ui,当执行该命令时,就会启动一个本地的服务,在浏览器打开后,通过导入本地项目可以图形化的管理、配置、启动等。

image.png

(3.2)配置存放路径

vue CLI3里将build和config隐藏了,可以在node_moudles/@vue/cli-service/lib/Service.js文件,在这里可以看到很多配置

image.png

(3.3)添加新的别名

如果确实需要修改一些配置,可以在当前项目下创建一个文件vue.config.js,通过module.exports = {}导出配置,它会自动和默认的配置进行合并。

image.png

六、Vue-Router

1. 认识路由的概念

什么是路由

  • 路由是一个网络工程里面的术语。
  • 路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动. --- 维基百科 在生活中, 路由器是做什么的?
  1. 路由器提供了两种机制: 路由和转送.
    • 路由是决定数据包从来源目的地的路径.
    • 转送将输入端的数据转移到合适的输出端.
  2. 路由中有一个非常重要的概念叫路由表.
    • 路由表本质上就是一个映射表, 决定了数据包的指向

后端路由阶段

image.png

  • 早期的网站开发整个HTML页面是由服务器来渲染的.
    • 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
  • 但是, 一个网站, 这么多页面服务器如何处理呢?
    • 一个页面有自己对应的网址, 也就是URL.
    • URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理.
    • Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
    • 这就完成了一个IO操作.
  • 上面的这种操作, 就是后端路由.
    • 当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
    • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
  • 后端路由的缺点:
    • 一种情况是整个页面的模块由后端人员来编写和维护的.
    • 另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码.
    • 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情.

前后端分离阶段

image.png

  • 前后端分离阶段:
    • 随着Ajax的出现, 有了前后端分离的开发模式.
    • 后端只提供API来返回数据, 前端通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中.
    • 这样做最大的优点就是前后端责任的清晰, 后端专注于数据上, 前端专注于交互和可视化上.
    • 并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可.
    • 目前很多的网站依然采用这种模式开发.

前端路由阶段

image.png

  • 单页面富应用阶段:
    • 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
    • 也就是前端来维护一套路由规则.

2. Vue-Router基本使用

前端路由的两种模式

改变url,页面不刷新

hash
  • 通过hash改变url,在前端的路由映射里,从映射关系里,取出对应的组件,把这个组件放到整个网页上显示。
  • URL的hash也就是锚点(#), 本质上是改变window.location的href属性.
  • 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新

image.png image.png

history

HTML5的history模式:pushState 在HTML5里有个history对象,通过这个对象修改url,网页不会有刷新

  • history接口是HTML5新增的, 它有五种模式改变URL而不刷新页面.
  • history.pushState()
history.pushState()

image.png image.png

image.png

history.replaceState()

image.png

image.png

history.go()和history.forward()

image.png

image.png

  1. 因为 history.back() 等价于 history.go(-1)
  2. history.forward() 则等价于 history.go(1)
  3. 这三个接口等同于浏览器界面的前进后退。

route-view基础

认识和安装vue-router
认识vue-router
  • vue-router
    • vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。
    • 我们可以访问其官方网站对其进行学习: router.vuejs.org/zh/
  • vue-router是基于路由和组件的
    • 路由用于设定访问路径, 将路径和组件映射起来.
    • 在vue-router的单页面应用中, 页面的路径的改变就是组件的切换.
安装
  • 安装步骤
    1. 步骤一: 安装vue-router
      npm install vue-router --save
    2. 步骤二: 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)
      • 第一步:导入路由对象,并且调用** Vue.use(VueRouter)**
      • 第二步:创建路由实例,并且传入路由映射配置
      • 第三步:在Vue实例挂载创建的路由实例
  • 使用vue-router的步骤:
    1. 第一步: 创建路由组件
    2. 第二步: 配置路由映射: 组件和路径映射关系
    3. 第三步: 使用路由: 通过和
vue-router基本使用

image.png

  • <router-link>: 该标签是一个vue-router中已经内置的组件, 它会被渲染成一个<a>标签.
  • <router-view>: 该标签会根据当前的路径, 动态渲染出不同的组件.
  • 网页的其他内容, 比如顶部的标题/导航, 或者底部的一些版权信息等会和<router-view>处于同一个等级.
  • 在路由切换时, 切换的是<router-view>挂载的组件, 其他内容不会发生改变.
vue-route细节补充
路由的默认路径

默认情况下, 进入网站的首页, 我们希望渲染首页的内容.
但是我们的实现中, 默认没有显示首页组件, 必须让用户点击才可以.

  • 如何可以让路径默认跳到到首页, 并且渲染首页组件呢?
    1. 在routes中多配置了一个映射.

    2. path配置的是根路径: /

    3. redirect是重定向, 也就是我们将根路径重定向到/home的路径下.

    image.png
HTML5的History模式
  • 我们前面说过改变路径的方式有两种:

    1. URL的hash;浏览器地址栏展示 localhost:8080/#/home
    2. HTML5的history;浏览器地址栏展示 localhost:8080/home
  • 默认情况下, 路径的改变使用的URL的hash;如果希望使用HTML5的history模式, 非常简单, 进行如下配置即可:

    image.png

router-link补充
  • 在前面的<router-link>中, 我们只是使用了一个属性:** to**, 用于指定跳转的路径.

  • <router-link>还有一些其他属性:

    1. tag: tag可以指定<router-link>之后渲染成什么组件, 比如下面的代码会被渲染成一个<button>元素, 而不是<a>
    2. replace: replace不会留下history记录, 所以指定replace的情况下, 后退键返回不能返回到上一个页面中
    3. active-class: 当<router-link>对应的路由匹配成功时, 会自动给当前元素设置一个router-link-active的class, 设置active-class可以修改默认的名称.
      • 在进行高亮显示的导航菜单或者底部tabbar时, 会使用到该类.
      • 但是通常不会修改类的属性, 会直接使用默认的router-link-active即可. 
      • 修改linkActiveClass,该class具体的名称也可以通过router实例的属性进行修改 
      image.png

    image.png

路由代码跳转
  • 有时候, 页面的跳转可能需要执行对应的JavaScript代码, 这个时候, 就可以使用第二种跳转方式了

    image.png

路由的懒加载

什么是路由懒加载
  • 当打包构建应用时,Javascript 包会变得非常大,影响页面加载。

  • 如果我们一次性从服务器请求下来这个页面, 可能需要花费一定的时间, 甚至用户的电脑上还出现了短暂空白的情况.

  • 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了

    为了避免该情况,使用路由懒加载

  • 路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块.

  • 只有在这个路由被访问到的时候, 才加载对应的组件

懒加载和非懒加载打包区别

image.png

image.png

懒加载的三种方式
  • 方式一: 结合Vue的异步组件和Webpack的代码分析.
    const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) })};
  • 方式二: AMD写法
    const About = resolve => require(['../components/About.vue'], resolve);
  • 方式三: 在ES6中, 我们可以有更加简单的写法来组织Vue异步组件和Webpack的代码分割(常用).
    const Home = () => import('../components/Home.vue')

3. Vue-Router动态路由使用

  • 在某些情况下,一个页面的path路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径:

    • /user/aaaa或/user/bbbb
    • 除了有前面的/user之外,后面还跟上了用户的ID
    • 这种path和Component的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)。

    image.png

    image.png

4. Vue-Router嵌套路由使用

认识嵌套路由

  • 嵌套路由是一个很常见的功能 比如在home页面中, 我们希望通过/home/news和/home/message访问一些内容. 一个路径映射一个组件, 访问这两个路径也会分别渲染两个组件.
  • 路径和组件的关系如下:
    • 实现嵌套路由有两个步骤:

      1. 创建对应的子组件, 并且在路由映射中配置对应的子路由.
      2. 在组件内部使用标签.

      image.png

嵌套路由实现

image.png

image.png

嵌套默认路径

image.png

5. Vue-Router参数传递

传递参数主要有两种类型: params和query

准备工作

第一步: 创建新的组件Profile.vue  第二步: 配置路由映射  第三步: 添加跳转的<router-link>

传递参数的两种类型

params类型
  1. 配置路由格式: /router/:id
  2. 传递的方式: 在path后面跟上对应的值
  3. 传递后形成的路径: /router/123, /router/abc

image.png

image.png

image.png

query类型
  1. 配置路由格式: /router, 也就是普通配置
  2. 传递的方式: 对象中使用query的key作为传递方式
  3. 传递后形成的路径: /router?id=123, /router?id=abc

image.png

传递参数的两种方式

方式一: <router-link>

image.png

方式二: JavaScript代码

image.png

image.png

获取参数

  • 获取参数通过$route对象获取的.

    • 在使用了 vue-router 的应用中,路由对象会被注入每个组件中,赋值为 this.$route ,并且当路由切换时,路由对象会被更新。
  • 通过$route获取传递的信息如下:

    image.png

$route$router的区别

  • $router为VueRouter实例,想要导航到不同URL,则使用$router.push方法

  • $route为当前router跳转对象里面可以获取name、path、query、params等

    image.png

    image.png

    image.png

6. Vue-Router导航守卫

监听路由跳转的过程,在跳转过程中做一些操作。

为什么使用导航守卫

  • 我们来考虑一个需求: 在一个SPA应用中, 如何改变网页的标题呢?
    • 网页标题是通过来显示的, 但是SPA只有一个固定的HTML, 切换不同的页面时, 标题并不会改变.
    • 但是我们可以通过JavaScript来修改的内容.window.document.title = '新的标题'.
    • 那么在Vue项目中, 在哪里修改? 什么时候修改比较合适呢?
  • 普通的修改方式:
    • 我们比较容易想到的修改标题的位置是每一个路由对应的组件.vue文件中.
    • 通过mounted声明周期函数, 执行对应的代码进行修改即可.
    • 但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码).
  • 有没有更好的办法呢? 使用导航守卫即可.
  • 什么是导航守卫?
    • vue-router提供的导航守卫主要用来监听路由的进入和离开的.
    • vue-router提供了beforeEachafterEach的钩子函数, 它们会在路由即将改变前和改变后触发.

导航守卫应用

image.png

导航守卫分类

  • 导航钩子的三个参数解析:
    • to: 即将要进入的目标的路由对象.
    • from: 当前导航即将要离开的路由对象.
    • next: 调用该方法后, 才能进入下一个钩子. 导航守卫官方文档
全局前置守卫beforeEach
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
  // ...
})
全局解析守卫beforeResolve

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。

全局后置钩子afterEach

不需要主动调用next()函数

router.afterEach((to, from) => {
  // ...
})
路由独享的守卫beforeEnter
const router = new VueRouter({
  routes: [
    {
      path: '/foo',
      component: Foo,
      beforeEnter: (to, from, next) => {
        // ...
        next()
      }
    }
  ]
})
组件内的守卫
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
const Foo = {
  template: `...`,
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
  }
}

7. keep-alive

认识keep-alive

  • 当离开/进入一个组件的时候,不让该组件频繁被销毁和创建,就需要使用keep-alive将该组件保留状态
  • 只有当某个组件使用了keep-alive时,就可以使用activateddeactivated这两个函数

遇见vue-router

  • keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。

    • 它们有两个非常重要的属性:
      1. include 字符串或正则表达,只有匹配的组件会被缓存
      2. exclude 字符串或正则表达式,任何匹配的组件都不会被缓存
  • router-view 是vue-router的一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存:

    image.png

    image.png

7. TabBar练习

七、Vuex详解

为什么使用Vuex

vux是做什么的

官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。 它采用 集中式存储管理 应用的所有组件的状态。

其实就是一个状态管理工具
当有多个组件时,希望某个组件内的某个状态(变量)共享,可以把该状态放到vuex中,相当于一个大管家把需要多个组件共享的变量全部存储在一个对象里面。

管理什么样的状态

  • 如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。
  • 比如用户的登录状态token、用户名称username、头像、地理位置信息等等。
  • 比如商品的收藏、购物车中的物品等等。
  • 这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的。

单页面状态管理

image.png

  • 在单个组件中进行状态管理是一件非常简单的事情
    1. State:不用多说,就是我们的状态。(你姑且可以当做就是data中的属性)
    2. View:视图层,可以针对State的变化,显示不同的信息。
    3. Actions:这里的Actions主要是用户的各种操作:点击、输入等等,会导致状态的改变。
    image.png

多页面状态管理

  • Vue已经帮我们做好了单个界面的状态管理,但是如果是多个界面呢?
    • 多个试图都依赖同一个状态(一个状态改了,多个界面需要进行更新)
    • 不同界面的Actions都想修改同一个状态(Home.vue需要修改,Profile.vue也需要修改这个状态)
  • 也就是说对于某些状态(状态1/状态2/状态3)来说只属于我们某一个视图,但是也有一些状态(状态a/状态b/状态c)属于多个视图共同想要维护的
    • 状态1/状态2/状态3你放在自己的房间中,你自己管理自己用,没问题。
    • 但是状态a/状态b/状态c我们希望交给一个大管家来统一帮助我们管理!!!
    • 没错,Vuex就是为我们提供这个大管家的工具。
  • 全局单例模式(大管家)
    • 我们现在要做的就是将共享的状态抽取出来,交给我们的大管家,统一进行管理。
    • 之后,你们每个试图,按照我规定好的规定,进行访问和修改等操作。
    • 这就是Vuex背后的基本思想。

vux状态管理图例

image.png

Devtools插件安装:去浏览器->找到三个点->更多工具->扩展程序->打开Chrome网上应用店->搜索Devtools->添加至Chrome

这个插件可以用于调试整个项目程序 image.png

image.png

image.png

image.png

image.png

image.png

Vuex基本使用

  1. 官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用 集中式存储管理 应用的所有组件的状态。

  2. 简单来说,就是把需要多个组件共享的变量全部存储在一个对象里面

  3. 比如:用户的登录状态、用户名称、头像、地理位置信息等等;商品的收藏、购物车中的物品;这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的。

    image.png

Vuex的安装

在命令行安装npm install vuex --save

count案例实现

  1. 创建vuex对象 image.png

  2. 挂载到vue实例中 image.png

  3. 使用vuex中的count image.png

Vuex核心概念

State

用来存放状态信息,在具体组件里可直接通过$store.state.aaa使用state中的值

  • Vuex提出使用单一状态树, 什么是单一状态树呢?
  • 但是,它是什么呢?我们来看一个生活中的例子。
    • 我们的个人档案,社保记录,公积金记录,结婚后的婚姻信息,医疗、文凭、房产记录等等。
    • 这些信息被分散在很多地方进行管理,如果需要办理个东西可能需要跑很多地方。
    • 这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作。
  • 这个和我们在应用开发中比较类似:
    • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。
    • 所以Vuex也使用了单一状态树来管理应用层级的全部状态
    • 将所有的数据所有的状态都放入一个store里去管理,永远用一个store,不要去建多个。
    • 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。

image.png

image.png

Getters

类似组件里的计算属性,计算属性一般在当某个数据经过一些变化再去展示的时候使用。

image.png

image.png

Mutation

  • Mutation状态更新
  1. Vuex的store状态的更新唯一方式:提交Mutation

  2. Mutation主要包括两部分:

    • 字符串的事件类型(type)
    • 一个回调函数(handler),该回调函数的第一个参数就是state。
  3. mutation的定义方式: image.png

  4. 通过mutation更新 image.png

  • Mutation传递参数
    参数被称为是mutation的载荷(Payload),传递一个参数值或一个对象 image.png

  • Mutation中定义的方法类型最好抽为常量,在其他的组件页面通过commit去提交的时候,也是通过常量去提交。

    1. 我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
    2. 定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称. image.png

Mutation的响应规则 Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新. 遵守一些Vuex对应的规则:

  • 提前在store中初始化好所需的属性.

  • 当给state中的对象添加新属性时, 使用下面的方式:

    • 方式一: 使用Vue.set(obj, 'newProp', 123)
    • 方式二: 用新对象给旧对象重新赋值

    image.png

    image.png

Mutation同步函数

  • 通常情况下, Vuex要求我们Mutation中的方法必须是同步方法.

    • 主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照.
    • 但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.
  • 比如我们之前的代码, 当执行更新时, devtools中会有如下信息: 图1

  • 但是, 如果Vuex中的代码, 我们使用了异步函数,会发现state中的info数据一直没有被改变, 因为他无法追踪到。因此不要在mutation中进行异步的操作: 图2

    image.png

    image.png

Action

有异步操作的时候,使用Action去操作,做完异步操作,再通过Mutation去修改State状态

  • context是什么?
    • context是和store对象具有相同方法和属性的对象.
    • 也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等.
    • 但是注意, 这里它们并不是同一个对象, 为什么呢? 我们后面学习Modules的时候, 再具体说.
    • 在具体组件里调用action的时候,通过dispatch,支持传参payload
    image.png Action返回的Promise
  • Promise经常用于异步操作.
    • 在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.

      image.png

Module

很多状态都会交给Vux管理,当应用复杂时,store对象就会变得臃肿,所以我们将store划分模块,每个模块又拥有自己的state、mutations、actions、getters Modules定义的模块名会被放入到同层级的state中

image.png

image.png

image.png

image.png

image.png

网络封装

网络模块选择

Ajax

编码比较繁琐

// 1. 通过内置对象创建了一个用于发送ajax请求的对象 xhr
    var xhr = new XMLHttpRequest();
// 2. 使用xhr的open方法建立连接
    xhr.open('提交方式','url地址');
    // 如果为post提交,需要在send()方法之前设置请求头的content-type属性
    // - 将请求内容转换为urlenoded形式进行发
    xhr.setRequestHeader('content-type','application/x-www-form-urlencoded')
// 3. 使用xhr的send方法发送请求
    xhr.send();
   // - get提交,传入null或不传参
   // - post提交,传入请求内容
// 4. 由于ajax通常为异步任务形式,所以请求发送后不能直接进行响应体内容的接收:
    // 使用onreadystatechange事件进行响应体内容的接收	
    xhr.onreadystatechange = function () {
	if(xhr.readyState === 4 && xhr.status === 200){
            var datas = jSON.parse(xhr.responseText);
	}
    }
jQuery-Ajax
- 首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
- 那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
- jQuery的代码1w+行.
- Vue的代码才1w+行.
- 完全没有必要为了用网络请求就引用这个重量级的框架.
Vue-resource
- Vue-resource是官方推出的.
- 为什么不选择它呢?
- 在Vue2.0退出后, Vue作者就在GitHub的Issues中说明了去掉vue-resource, 并且以后也不会再更新.
- 那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新和维护.
- 对以后的项目开发和维护都存在很大的隐患.
axios
- axios有非常多的优点, 并且用起来也非常方便.

JSON的封装

JSON原理

jsonp是一种跨域方式,原理是通过动态创建的script标签的src属性,进行请求发送。我们利用<script>标签的src帮助我们去服务器请求到数据, 将数据当做一个javascript的函数来执行, 并且执行的过程中传入我们需要的json.

JSON代码封装

image.png

image.png

axios的使用

认识axios
  • 功能特点:

    1. 在浏览器中发送 XMLHttpRequests 请求
    2. 在 node.js 中发送 http请求
    3. 支持 Promise API
    4. 拦截请求和响应
    5. 转换请求和响应数据
    6. 等等
  • axiox请求方式

    1. axios(config)
    2. axios.request(config)
    3. axios.get(url[, config])
    4. axios.delete(url[, config])
    5. axios.head(url[, config])
    6. axios.post(url[, data[, config]])
    7. axios.put(url[, data[, config]])
    8. axios.patch(url[, data[, config]])
  • 使用

    1. 先安装 npm install axios --save
    2. 在main.js引入 import axios from 'axios'
      httpbin.org/ 这个网站可以模拟服务请求
发送请求

axios本身可以返回一个promise,在内部拿到数据,会调用内部的resolve,就可以调用then方法,拿到数据。 默认是get请求 image.png

image.png

  • 发送get请求

image.png

  • 发送并发请求
  1. 使用axios.all, 可以放入多个请求的数组.

  2. axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2

    image.png

  • 常见的配置选项

    image.png

axios实例

当接口服务分布在不同的服务器上时,如果设置全局的baseURL,会导致每个接口访问的都是同一个服务 会单独创建一个实例

只要引用了第三方的东西,不要在每个页面组件都对这个第三方的东西进行依赖,统一封装到一个文件里

image.png

image.png

image.png

import axios from 'axios'

export function request(config) {
  // 1.创建axios的实例
  const instance = axios.create({
    baseURL: 'http://123.207.32.32:8000',
    timeout: 5000
  })

  // 2.发送真正的网络请求 axios本身返回的就是一个promis,这里可以直接return,不需要再去套一个promise
  return instance(config)

  // 如果第三方的axios不再维护了,那么我只需要引入新的 import **** from '****',
  // 将export function request(config) {}中的代码换掉
  // return new Promise(), return一个promise就可以了,其余的页面组件不需要任何的修改
}
import {request} from "./network/request";

// 调用request,返回是一个promise,所以后面可以直接.then/.catch
request({
  url: '/home/multidata'
}).then(res => {
  console.log(res);
}).catch(err => {
  // console.log(err);
})
axios拦截器
请求拦截器

请求成功拦截 请求失败拦截

在请求的过程中增加一些东西

响应拦截器

响应成功拦截 响应失败拦截

import axios from 'axios'

export function request(config) {
  // 1.创建axios的实例
  const instance = axios.create({
    baseURL: 'http://123.207.32.32:8000',
    timeout: 5000
  })

  // 2.axios的拦截器
  // 2.1.请求拦截的作用
  // 请求成功进入config,请求失败进入err
  instance.interceptors.request.use(config => {
    // console.log(config);
    // 1.比如config中的一些信息不符合服务器的要求

    // 2.比如每次发送网络请求时, 都希望在界面中显示一个请求的图标

    // 3.某些网络请求(比如登录(token)), 必须携带一些特殊的信息
    // 使用拦截器,必须把config返回出去,否则后面执行拿不到
    return config
  }, err => {
    // console.log(err);
  })

  // 2.2.响应拦截
  instance.interceptors.response.use(res => {
    // console.log(res);
    // 返回出去,外部才可以拿到值 否则是undefined
    return res.data
  }, err => {
    console.log(err);
  })

  // 3.发送真正的网络请求 axios本身返回的就是一个promis,这里可以直接return,不需要再去套一个promise
  return instance(config)

}