Vue学习笔记

一、邂逅Vue.js

内容概述

认识Vuejs
为什么学习Vuejs
简单认识一下Vuejs
Vuejs安装方式
CDN引入
下载和引入
NPM安装管理
Vuejs初体验
Hello Vuejs
Vue列表展示
案例:计数器
Vuejs的MVVM
Vue中的MVVM

1.遇见Vue.js

为什么学习Vue.js

我相信每个人学习Vue的目的是各部相同的。

  • 可能你的公司正要将原有的项目使用Vue进行重构。
  • 也可能是你的公司新项目决定使用Vue的技术栈。
  • 当然,如果你现在正在换工作,你会发现招聘前端的需求中,10个有8个都对Vue有或多或少的要求。
  • 当然,作为学习者我们知道Vuejs目前非常火,可以说是前端必备的一个技能。

前端开发的复杂性

Vuejs的特点

Vue (读音 /vjuː/,类似于 view),不要读错。

Vue是一个渐进式的框架,什么是渐进式的呢?

  • 渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,带来更丰富的交互体验。
  • 或者如果你希望将更多的业务逻辑使用Vue实现,那么Vue的核心库以及其生态系统。
  • 比如Core+Vue-router+Vuex,也可以满足你各种各样的需求。

Vue有很多特点和Web开发中常见的高级功能

  • 解耦视图和数据
  • 可复用的组件
  • 前端路由技术
  • 状态管理
  • 虚拟DOM

这些特点,你不需要一个个去记住,我们在后面的学习和开发中都会慢慢体会到的,一些技术点我也会在后面进行讲解。

学习Vuejs的前提?

  • 从零学习Vue开发,并不需要你具备其他类似于Angular、React,甚至是jQuery的经验。
  • 但是你需要具备一定的HTML、CSS、JavaScript基础。

2. 安装Vue.js

使用一个框架,我们第一步要做什么呢?安装下载它
安装Vue的方式有很多:

方式一:CDN引入

你可以选择引入开发环境版本还是生产环境版本

<!-- 开发环境版本,包含了有帮助的命令行警告 --> 
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue"></script>

方式二:下载引入

开发环境 https://vuejs.org/js/vue.js

生产环境 https://vuejs.org/js/vue.min.js

方式三:npm安装

后续通过webpack和CLI的使用,我们使用该方式。

3. 体验Vue.js

  • Hello Vuejs
    • mustache -> 体验vue响应式
  • Vue列表展示
    • v-for
    • 后面给数组追加元素的时候, 新的元素也可以在界面中渲染出来
  • Vue计数器小案例
    • 事件监听: click -> methods

Hello Vuejs

我们来做我们的第一个Vue程序,体验一下Vue的响应式

代码做了什么事情?

我们来阅读JavaScript代码,会发现创建了一个Vue对象。

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

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

浏览器执行代码的流程:

  • 执行到10~13行代码显然出对应的HTML
  • 执行第16行代码创建Vue实例,并且对原HTML进行解析和修改。

并且,目前我们的代码是可以做到响应式的。

代码见,01-HelloVuejs.html

  <body>
    <div id="app">
      <h2>{{message}}</h2>
      <h1>{{name}}</h1>
    </div>

    <div>{{message}}</div>

    <script src="../js/vue.js"></script>
    <script>
      // 源码里面有类似于这样的东西 有一个Vue的类 可以往里面传一些参数 Vue的参数是对象类型
      function Vue() {}
    </script>
    <script>
      // let(变量)/const(常量)

      // 编程范式: 声明式编程 实例管理div时,只需声明显示什么东西
      const app = new Vue({
        el: "#app", // 用于挂载要管理的元素
        data: {
          // 定义数据
          message: "你好啊,李银河!",
          name: "coderwhy",
        },
      });

      // 原始js的做法(编程范式: 命令式编程) 明确告诉一步步该如何做
      // 1.创建div元素,设置id属性

      // 2.定义一个变量叫message

      // 3.将message变量放在前面的div元素中显示

      // 4.修改message的数据: 今天天气不错!

      // 5.将修改后的数据再次替换到div元素

      // Vue的响应式 --> 可以在打印台修改 app.name='yyy'
    </script>
  </body>

Vue显示列表

  • 现在,我们来展示一个更加复杂的数据:数据列表。

    • 比如我们现在从服务器请求过来一个列表

    • 希望展示到HTML中。

  • HTML代码中,使用v-for指令

    • 该指令我们后面会详细讲解,这里先学会使用。
  • 是不是变得So Easy,我们再也不需要在JavaScript代码中完成DOM的拼接相关操作了

  • 而且,更重要的是,它还是响应式的。

    • 也就是说,当我们数组中的数据发生改变时,界面会自动改变。
    • 依然让我们打开开发者模式的console,来试一下
 <div id="app">
    <ul>
        <li v-for="item in movies">{{item}}</li>
    </ul>
</div>
 
<script src="../js/vue.js"></script>
<script>
    const app = new Vue({
        el: '#app',
        data: {
            message: '你好啊',
            movies: ['星际穿越', '大话西游', '少年派', '盗梦空间']
        }
    })
</script>

实现计数器

代码见,03-vue案例-计数器.html

4. Vue中的MVVM

什么是MVVM呢?

  • 通常我们学习一个概念,最好的方式是去看维基百科(对,千万别看成了百度百科)
  • https://zh.wikipedia.org/wiki/MVVM
  • 维基百科的官方解释,我们这里不再赘述。

我们直接来看Vue的MVVM

image-20210714103523753

看至这

View层:

视图层

在我们前端开发中,通常就是DOM层。

主要的作用是给用户展示各种信息。

Model层:

数据层

数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据。

在我们计数器的案例中,就是后面抽取出来的obj,当然,里面的数据可能没有这么简单。

VueModel层:

视图模型层

视图模型层是View和Model沟通的桥梁。

一方面它实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中

另一方面它实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情况下改变对应的Data。

计数器的MVVM

  • 我们的计数器中就有严格的MVVM思想

    • View依然是我们的DOM

    • Model就是我们我们抽离出来的obj

    • ViewModel就是我们创建的Vue对象实例

  • 它们之间如何工作呢?

    • 首先ViewModel通过Data Binding让obj中的数据实时的在DOM中显示。

    • 其次ViewModel通过DOM Listener来监听DOM事件,并且通过methods中的操作,来改变obj中的数据。

有了Vue帮助我们完成VueModel层的任务,在后续的开发,我们就可以专注于数据的处理,以及DOM的编写工作了。

5.创建Vue实例传入的options

你会发现,我们在创建Vue实例的时候,传入了一个对象options。

这个options中可以包含哪些选项呢?

  • 详细解析: https://cn.vuejs.org/v2/api/#%E9%80%89%E9%A1%B9-%E6%95%B0%E6%8D%AE

目前掌握这些选项:

  • el:

    • 类型:string | HTMLElement

    • 作用:决定之后Vue实例会管理哪一个DOM。

  • data:

    • 类型:Object | Function (组件当中data必须是一个函数)

    • 作用:Vue实例对应的数据对象。

  • methods:

    • 类型:{ [key: string]: Function }
    • 作用:定义属于Vue的一些方法,可以在其他地方调用,也可以在指令中使用。

6.vue的生命周期

生命周期coderwhy理解:事物从诞生到消亡的整个过程。

Vue生命周期:

二、Vue基础语法

主要内容:

插值操作
绑定属性
计算属性
事件监听
条件判断
循环遍历
阶段案例
v-model

1.插值语法

Mustache

如何将data中的文本数据,插入到HTML中呢?

  • 我们已经学习过了,可以通过Mustache语法(也就是双大括号)。
  • Mustache: 胡子/胡须.

Mustache语法中,不仅仅可以直接写变量,也可以写简单的表达式。

<div id="app">
        <h2>{{firstName + ' ' + lastName}}</h2>
        <h2>{{firstName}} {{lastName}}</h2>\
        <h2>{{counter * 2}}</h2>
    </div>

    <script src="../js/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                firstName: 'kobe',
                lastName: 'bryant',
                counter: 100
            }
        })
    </script>

我们可以像下面这样来使用,并且数据是响应式的

image-20210714154454768

v-once

但是,在某些情况下,我们可能不希望界面随意的跟随改变。这个时候,我们就可以使用一个Vue的指令

v-once:

  • 该指令后面不需要跟任何表达式(比如之前的v-for后面是由跟表达式的)

  • 该指令表示元素和组件(组件后面才会学习)只渲染一次,不会随着数据的改变而改变。

代码如下:

image-20210714161024315

v-html

某些情况下,我们从服务器请求到的数据本身就是一个HTML代码

  • 如果我们直接通过{{}}来输出,会将HTML代码也一起输出。
  • 但是我们可能希望的是按照HTML格式进行解析,并且显示对应的内容。

如果我们希望解析出HTML展示

  • 可以使用v-html指令
    • 该指令后面往往会跟上一个string类型
    • 会将string的html解析出来并且进行渲染

image-20210714161758043

v-text

v-text作用和Mustache比较相似:都是用于将数据显示在界面中

v-text通常情况下,接受一个string类型

image-20210714162355942

v-pre

v-pre用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。

比如下面的代码:

  • 第一个h2元素中的内容会被编译解析出来对应的内容
  • 第二个h2元素中会直接显示{{message}}

image-20210714162700804

v-cloak

  • 在某些情况下,我们浏览器可能会直接显然出未编译的Mustache标签。
  • cloak: 斗篷

当加上该指令后,

image-20210714162901594

代码见,06-v-cloak指令的使用.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
<body>

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

<script src="../js/vue.js"></script>
<script>
  // 在vue解析之前, div中有一个属性v-cloak
  // 在vue解析之后, div中没有一个属性v-cloak
  setTimeout(function () {
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  }, 1000)
</script>

</body>
</html>

2.绑定属性

v-bind介绍

前面我们学习的指令主要作用是将值插入到我们模板的内容当中。

但是,除了内容需要动态来决定外,某些属性我们也希望动态来绑定。

  • 比如动态绑定a元素的href属性
  • 比如动态绑定img元素的src属性

这个时候,我们可以使用v-bind指令:

  • 作用:动态绑定属性
  • 缩写::
  • 预期:any (with argument) | Object (without argument)
  • 参数:attrOrProp (optional)

下面,我们就具体来学习v-bind的使用

v-bind基础

v-bind用于绑定一个或多个属性值,或者向另一个组件传递props值(这个学到组件时再介绍)

在开发中,有哪些属性需要动态进行绑定呢?

  • 还是有很多的,比如图片的链接src、网站的链接href、动态绑定一些类、样式等等

比如通过Vue实例中的data绑定元素的src和href,代码如下:

image-20210714171759530

v-bind语法糖

v-bind有一个对应的语法糖,也就是简写方式

  • 在开发中,我们通常会使用语法糖的形式,因为这样更加简洁。

简写方式如下:

image-20210714172029499

绑定class

绑定class(一)

很多时候,我们希望动态的来切换class,比如:

  • 当数据为某个状态时,字体显示红色。
  • 当数据另一个状态时,字体显示黑色。

绑定class有两种方式:

  • 对象语法
  • 数组语法
绑定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是一个计算属性

绑定class(三)-数组语法

绑定方式:数组语法

  • 数组语法的含义是: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>

绑定样式

  • 我们可以利用v-bind:style来绑定一些CSS内联样式。

  • 在写CSS属性名的时候,比如font-size

    • 我们可以使用驼峰式 (camelCase) fontSize
    • 或短横线分隔 (kebab-case,记得用单引号括起来) ‘font-size’
  • 绑定style有两种方式:

    • 对象语法
    • 数组语法
v-bind绑定style–对象语法
:style="{color: currentColor, fontSize: fontSize + 'px'}"
  • style后面跟的是一个对象类型
    • 对象的key是CSS属性名称
    • 对象的value是具体赋的值,值可以来自于data中的属性

代码见,

 
<div id="app">
  <!--<h2 :style="{key(属性名): value(属性值)}">{{message}}</h2>-->
 
  <!--'50px'必须加上单引号, 否则是当做一个变量去解析-->
  <h2 :style="{fontSize: '50px'}">{{message}}</h2>
 
  <!--finalSize当成一个变量使用-->
  <!--<h2 :style="{fontSize: finalSize}">{{message}}</h2>-->
  <h2 :style="{fontSize: finalSize + 'px', backgroundColor: finalColor}">{{message}}</h2>
  <h2 :style="getStyles()">{{message}}</h2>
</div>
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      finalSize: 100,
      finalColor: 'red',
    },
    methods: {
      getStyles: function () {
        return {fontSize: this.finalSize + 'px', backgroundColor: this.finalColor}
      }
    }
  })
</script>
v-bind绑定style–数组语法
<div v-bind:style="[baseStyles, overridingStyles]"></div>
  • style后面跟的是一个数组类型
  • 多个值以,分割即可
 
<div id="app">
  <h2 :style="[baseStyle, baseStyle1]">{{message}}</h2>
</div>
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      baseStyle: {backgroundColor: 'red'},
      baseStyle1: {fontSize: '100px'},
    }
  })
</script>

3.计算属性

1.(掌握)什么是计算属性?计算属性的基本使用

  • 我们知道,在模板中可以直接通过插值语法显示一些data中的数据。

  • 但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示

    • 比如我们有firstName和lastName两个变量,我们需要显示完整的名称。
    • 但是如果多个地方都需要显示完整的名称,我们就需要写多个{{firstName}} {{lastName}}
  • 我们可以将上面的代码换成计算属性:

    • OK,我们发现计算属性是写在实例的computed选项中的。

代码见,

 
  <div id="app">
    <h2>{{firstName + ' ' + lastName}}</h2>
    <h2>{{firstName}} {{lastName}}</h2>
    <!-- {{}} 里面也可以调用方法,需要加() -->
    <h2>{{getFullName()}}</h2>
 
    <!-- {{}} 里面一般用的是变量名,方法有个() 可能有点别扭,这时候就能使用计算属性 -->
    <!-- !!计算属性不用加括号 -->
    <h2>{{fullName}}</h2>
  </div>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        firstName: 'Lebron',
        lastName: 'James'
      },
      // computed: 计算属性()
      computed: {
         // computed里面定义的也是函数,但是函数名一般不加类似于get的动词 
        //  但是它叫计算属性,所以一般起类似于属性的名字
 
        fullName: function () { // 这样写其实也是一个语法糖 里面有setter和getter 这个是getter
          return this.firstName + ' ' + this.lastName
        }
      },
      methods: {
        getFullName() {
          return this.firstName + ' ' + this.lastName
        }
      }
    })
  </script>

2.(掌握)计算属性的复杂操作

  • 计算属性中也可以进行一些更加复杂的操作,比如下面的例子:

附:高阶函数reduce的补充

代码见,

 
<div id="app">
  <h2>总价格: {{totalPrice}}</h2>
</div>
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      books: [
        {id: 110, name: 'Unix编程艺术', price: 119},
        {id: 111, name: '代码大全', price: 105},
        {id: 112, name: '深入理解计算机原理', price: 98},
        {id: 113, name: '现代操作系统', price: 87},
      ]
    },
    computed: {
      totalPrice: function () {
        let result = 0;
        for (let i=0; i < this.books.length; i++) {
          result += this.books[i].price
        }
        return result;
 
        // 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.第三种方式
        // 高阶函数 filter/reduce/map
        return this.books.reduce((preValue, book) => {
          return preValue + book.price * book.count
        }, 0)
 
        // reduce的补充
        // https://www.cnblogs.com/amujoe/p/11376940.html
      }
    }
  })
</script>

3.(理解)计算属性的setter和getter

每个计算属性都包含一个getter和一个setter

  • 在上面的例子中,我们只是使用getter来读取。
  • 在某些情况下,你也可以提供一个setter方法(不常用)。
  • 在需要写setter的时候,代码如下:

代码见,03-计算属性的setter和getter

4.(掌握)计算属性的缓存(计算属性和methods的对比)

  • 我们可能会考虑这样的一个问题:
    • methods和computed看起来都可以实现我们的功能,
    • 那么为什么还要多一个计算属性这个东西呢?
    • 原因:计算属性会进行缓存,如果多次使用时,计算属性只会调用一次。
  • 我们来看下面的代码:

image-20210715101300024

在浏览器控制台查看缓存:

(fullName没有变化,就调用了一次)

(fullName有变化,重新调用)

代码见,

 
<div id="app">
  <!--1.直接拼接: 语法过于繁琐-->
  <h2>{{firstName}} {{lastName}}</h2>
 
  <!--2.通过定义methods-->
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
 
  <!--3.通过computed-->
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
</div>
 
<script src="../js/vue.js"></script>
<script>
  // angular -> google
  // TypeScript(microsoft) -> ts(类型检测)
  // flow(facebook) ->
  const app = new Vue({
    el: '#app',
    data: {
      firstName: 'Kobe',
      lastName: 'Bryant'
    },
    methods: {
      getFullName: function () {
        console.log('getFullName');
        return this.firstName + ' ' + this.lastName
      }
    },
    computed: {
      fullName: function () {
        console.log('fullName');
        // 会观察有没有变化 没有变化就直接返回原来结果 而不是重新计算 有变化就重新调用一次
        // 浏览器控制台 app.fullName='why' 会再调用一次
        return this.firstName + ' ' + this.lastName
      }
    }
  })
 
</script>

4.ES6补充

1.块级作用域–let/var

  • 事实上var的设计可以看成JavaScript语言设计上的错误,但是这种错误多半不能修复和移除,以为需要向后兼容

    • 大概十年前BrendanEich(js的作者)就决定修复这个问题,于是他添加了一个新的关键字:let
    • 我们可以将let看成更完美的var
  • 块级作用域

    • JS中使用var来声明一个变量时,变量的作用域主要是和函数的定义有关
    • 针对于其他块定义来说是没有作用域的,比如if/for等,这在我们开发中往往会引起一些问题

    1. **变量作用域 **: 变量在什么范围内是可用
      // 下面是一个代码块。{} 就是一个代码块,有没有{}对于定义变量来说没任何意义 内外都可以使用name
        {
          var name = 'why';
          console.log(name);
        }
        console.log(name); // 可以使用name
    

    ​ 补充:JS:代码块{},及代码块的作用是什么?

  • 2.没有块级作用域引起的问题: if的块级

 if (true) {
      var name = 'why';
    }
 console.log(name);// 可以打印
if (true) {
      var name = 'why';
    }
    console.log(name);// 可以打印
 
    var func;
    if (true) {
      var name = 'why';
      func = function () { // 这个变量是为了打印这里的name
        console.log(name);
      }
      // func();// 可以打印
    }
    name = 'kobe'; // 这里把变量改掉了,不应该能改
 
    func(); // 依然可以打印
    // console.log(name);
  • 3.没有块级作用域引起的问题: for的块级

    • 监听按钮的点击
     
      <button>按钮1</button>
      <button>按钮2</button>
      <button>按钮3</button>
      <button>按钮4</button>
      <button>按钮5</button>
     
    <script>
       var btns = document.getElementsByTagName('button');
            for (var i = 0; i < btns.length; i++) {
                btns[i].addEventListener('click', function () {
                    console.log('第' + i + '个按钮被点击');
                    // 点击了第1个,打印却是第5个,点击其他按钮也是打印第5个
                    // 与上面的那个有点像,打印的i是在一个函数里面,在点击之前,这个i已经被改掉了,进行了5次循环
                })
            }
    </script>
    

综上,*if和for里面没有块级*

  • 为了解决这个问题,我们一般用一个闭包
    • **闭包:**内部函数总是可以访问其所在的外部函数中声明的参数和变量,即使在其外部函数被返回(寿命终结)了之后。
 var btns = document.getElementsByTagName('button');
    for (var i = 0; i < btns.length; i++) {
      (function (num) { // 0
        btns[i].addEventListener('click', function () {
          console.log('第' + num + '个按钮被点击');
        })
      })(i)
    }

​ 补充:立即执行函数

 // 立即执行函数
    (function () {
 
    }())
    (function () {
 
    })()
  • 为什么闭包可以解决问题: 函数是一个作用域.
 
        var name = "yyyy";
 
        function abc(name) {
            console.log(name);
        }
        name="why";
        abc('aaaaaa');// 无论外部怎么改变name,打印的还是aaaa
   var name = 'why'
 
    function abc(bbb) { // bbb = 'why'
      console.log(bbb);
    }
    abc(name);// 也不会有任何冲突 name还是why
    name = 'kobe'

补充:关于一些闭包的文章

闭包,看这一篇就够了——带你看透闭包的本质,百发百中

【JS】函数闭包(重点)

前端面试题:闭包

  • 总结
    • ES5之前因为if和for都没有块级作用域的概念, 所以在很多时候, 我们都必须借助于function的作用域来解决应用外面变量的问题.
    • ES6中,加入了let, let它是有if和for的块级作用
  • 总之,记住
    • ES5中的var是没有块级作用域的(if/for)
    • ES6中的let是由块级作用的(if/for)
 // es6写法
    const btns = document.getElementsByTagName('button')
    for (let i = 0; i < btns.length; i++) {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
  • ES6 为什么let可以呢,有它自己的作用域了 i只属于 for (let i = 0; i < btns.length; i++) {} 里面的作用域
    { i = 0
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    
    { i = 1
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    { i = 2
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    { i = 3
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    { i = 4
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
  • 而ES5
    // ES5
    var i = 5
    {
      btns[i].addEventListener('click', function () {
      console.log('第' + i + '个按钮被点击');
    })
    }
    
    {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    
    
    {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    
    
    {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    
    {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }

2.三种方案对比(ES5没有闭包-有闭包-ES6的let)

 
 
  <button>按钮1</button>
  <button>按钮2</button>
  <button>按钮3</button>
 
  <script>
    // 1.没有块级作用域引起的问题: for的块级
    // 为什么闭包可以解决问题: 函数是一个作用域.
    var btns = document.getElementsByTagName('button');
    for (var i = 0; i < btns.length; i++) {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    // 伪代码:
    // 1.情况一: ES5中没有使用闭包(错误的方式)
    i = 2 
    { // 在里面定义在外面定义都是一样的
      // i = 0
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    {
      i = 1
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    {
      // i = 2
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    // 2.情况二: ES5中使用闭包
    var btns = document.getElementsByTagName('button');
    for (var i = 0; i < btns.length; i++) {
      (function (i) { // 0
        btns[i].addEventListener('click', function () {
          console.log('第' + i + '个按钮被点击');
        })
      })(i)
    }
 
    i = 100000000
 
    function (i) { // i = 0
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }(0)
 
    function (i) { // i = 1
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }(1)
 
    function (i) { // i = 2
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }(2)
 
    // ES6中的let
    const btns = document.getElementsByTagName('button')
    for (let i = 0; i < btns.length; i++) {
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
    i = 10000000 {
      i = 0
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    {
      i = 1
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
 
    {
      i = 2
      btns[i].addEventListener('click', function () {
        console.log('第' + i + '个按钮被点击');
      })
    }
  </script>

3.const的使用和注意点

  • const关键字

    • 在很多语言中已经存在比如C/C++中主要的作用是将某个变量修饰为常量
    • 在JavaScript中也是如此使用const修饰的标识符为常量,不可以再次赋值
  • 什么时候使用const呢?

    • 当我们修饰的标识符不会被再次赋值时,就可以使用const来保证数据的安全性
  • 建议:在ES6开发中优先使用const只有需要改变某一个标识符的时候才使用let

  • const的注意

    • const注意一

      •  const a=20;
         a=30;// 错误:不可以修改
        
    • const注意二

      • const name; // 错误:const修饰的标识符必须赋值
        
 
  // 1.注意一: 一旦给const修饰的标识符被赋值之后, 不能修改
  // const name = 'why';
  // name = 'abc';
 
  // 2.注意二: 在使用const定义标识符,必须进行赋值
  // const name;
 
  // 3.注意三: 常量的含义是指向的对象不能修改, 但是可以改变对象内部的属性.
  const obj = {
    name: 'why',
    age: 18,
    height: 1.88
  }
  // obj = {}
  console.log(obj);
 
  obj.name = 'kobe';
  obj.age = 40;
  obj.height = 1.87;
 
  console.log(obj);

4.ES6对象字面量的增强写法

const obj = new Object()
 
  const obj = {  // 这个{} 就是对象的字面量
    name: 'why',
    age: 18,
    run: function () {
      console.log('在奔跑');
    },
    eat: function () {
      console.log('在次东西');
    }
  }
  • 1.属性的增强写法

    •   const name = 'why';
        const age = 18;
        const height = 1.88
      
    • ES5的写法:

    • // ES5的写法
        const obj = {
          name: name,
          age: age,
          height: height
        }
       console.log(obj);
      
    • ES6的写法:

    • // ES6的写法
        const obj = {
          name,
          age,
          height,
        }
        
        console.log(obj);
      
  • 2.函数的增强写法

    • ES5的写法

      •  // ES5的写法
          const obj = {
            run: function () {
          
            },
            eat: function () {
          
            }
          }
        
    • ES6的写法

    • // ES6
        const obj = {
          run() {
       
          },
          eat() {
       
          }
        }
      

5.事件监听v-on

1.事件监听

  • 在前端开发中,我们需要经常和用于交互。

    • 这个时候,我们就必须监听用户发生的事件,比如点击、拖拽、键盘事件等等
    • 在Vue中如何监听事件呢?使用v-on指令
  • v-on介绍

    • 作用:绑定事件监听器
    • 缩写:@
    • 预期:Function | Inline Statement | Object
    • 参数:event
  • 下面,我们就具体来学习v-on的使用

2.v-on基础

  • 这里,我们用一个监听按钮的点击事件,来简单看看v-on的使用

    • 下面的代码中,我们使用了v-on:click="counter++”
    • 另外,我们可以将事件指向一个在methods中定义的函数

  • 注:v-on也有对应的语法糖:

    • v-on:click可以写成@click

代码见,

 
<!-- 
  v-on介绍
作用:绑定事件监听器
缩写:@
 -->
<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&#45;&#45;">-</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>

3.v-on参数

当通过methods中定义方法,以供@click调用时,需要注意参数问题:

情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。

  • 但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去

情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。

image-20210715171311924

image-20210715171324825

代码见,

 
<div id="app">
  <!--1.事件调用的方法没有参数 ()可以不添加-->
  <button @click="btn1Click()">按钮1</button>
 
  <button @click="btn1Click">按钮1</button>
 
  <!--2.在事件定义时, 写方法时省略了小括号, 
    但是方法本身是需要一个参数的, 这个时候, Vue会默认将浏览器生产的event事件对象作为参数传入到方法-->
  <!--<button @click="btn2Click(123)">按钮2</button>-->   
 
  <!--调用时不传入参数,那么参数为undefined-->
  <!--<button @click="btn2Click()">按钮2</button>-->
  <button @click="btn2Click">按钮2</button>
 
  <!--3.方法定义时, 我们需要event对象, 同时又需要其他参数-->
  <!-- 在调用方式, 如何手动的获取到浏览器参数的event对象: $event-->
  <button @click="btn3Click(abc, $event)">按钮3</button>
</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()

4.v-on修饰符

在某些情况下,我们拿到event的目的可能是进行一些事件处理。

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

  • .stop - 调用 event.stopPropagation()。
  • .prevent - 调用 event.preventDefault()。
  • .{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。
  • .native - 监听组件根元素的原生事件。
  • .once - 只触发一次回调。

代码见,03-v-on的修饰符.html

 
<div id="app">
  <!--1. .stop修饰符的使用  停止冒泡-->
  <div @click="divClick">
    aaaaaaa
    <button @click.stop="btnClick">按钮</button>
  </div>
 
  <!--2. .prevent修饰符的使用 阻止默认行为-->
  <br>
  <form action="baidu">
    <input type="submit" value="提交" @click.prevent="submitClick">
  </form>
 
  <!--3. .监听某个键盘的键帽-->
  <input type="text" @keyup.enter="keyUp">
 
  <!--4. .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');
      },
      btn2Click() {
        console.log('btn2Click');
      }
    }
  })
</script>

6.条件判断

1.v-if、v-else-if、v-else

  • v-if、v-else-if、v-else

    • 这三个指令与JavaScript的条件语句if、else、else if类似。
    • Vue的条件指令可以根据表达式的值在DOM中渲染或销毁元素或组件
  • 简单的案例演示:

    image-20210730063621181

  • v-if的原理:

    • v-if后面的条件为false时,对应的元素以及其子元素不会渲染。
    • 也就是根本没有不会有对应的标签出现在DOM中。
  • 代码见,

    • v-if的使用
     
    <div id="app">
      <h2 v-if="isShow">
        <div>abc</div>
        <div>abc</div>
        <div>abc</div>
        <div>abc</div>
        <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>
        <div>abc</div>
        <div>abc</div>
        <div>abc</div>
        <div>abc</div>
        {{message}}
      </h2>
      <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>
    

2.条件渲染案例–登录切换

  • 我们来做一个简单的小案例:
    • 用户再登录时,可以切换使用用户账号登录还是邮箱地址登录。
    • 类似如下情景:

image-20210730085727054

3.(理解)案例小问题–登录切换input的复用

小问题:
  • 如果我们在有输入内容的情况下,切换了类型,我们会发现文字依然显示之前的输入的内容。
  • 但是按道理讲,我们应该切换到另外一个input元素中了。
  • 在另一个input元素中,我们并没有输入内容。
  • 为什么会出现这个问题呢?
问题解答:
  • 这是因为Vue在进行DOM渲染时,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。
  • 在上面的案例中,Vue内部会发现原来的input元素不再使用,直接作为else中的input来使用了。
解决方案:
  • 如果我们不希望Vue出现类似重复利用的问题,可以给对应的input添加key
  • 并且我们需要保证key的不同

代码见,

05-用户登录切换的案例(小问题).html

 
<div id="app">
  <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>

4.(掌握)v-show的使用以及和v-if的区别

  • 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>
<!-- 
  当需要在显示与隐藏之间切片很频繁时,使用v-show
当只有一次切换时,通过使用v-if
 -->
 
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({   
    el: '#app',
    data: {
      message: '你好啊',
      isShow: true
    }
  })
</script>

7.遍历循环v-for

1.v-for遍历数组

  • 当我们有一组数据需要进行渲染时,我们就可以使用v-for来完成。

    • v-for的语法类似于JavaScript中的for循环。
    • 格式如下:item in items的形式。
  • 我们来看一个简单的案例:

  • 如果在遍历的过程中不需要使用索引值

    • v-for=“movie in movies”
    • 依次从movies中取出movie,并且在元素的内容中,我们可以使用Mustache语法,来使用movie
  • 如果在遍历的过程中,我们需要拿到元素在数组中的索引值呢?

    • 语法格式:v-for=(item, index) in items
    • 其中的index就代表了取出的item在原数组的索引值。

image-20210730090556379

代码见,

 
<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>

2.v-for遍历对象

  • v-for可以用户遍历对象:

    • 比如某个对象中存储着你的个人信息,我们希望以列表的形式显示出来。

代码见,
 
<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>

3.组件的key属性

官方推荐我们在使用v-for时,给对应的元素或组件添加上一个:key属性。

为什么需要这个key属性呢(了解)?

  • 这个其实和Vue的虚拟DOM的Diff算法有关系。
  • 这里我们借用React’s diff algorithm中的一张图来简单说明一下:

当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点

  • 我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的。
  • 即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率?

所以我们需要使用key来给每个节点做一个唯一标识

  • Diff算法就可以正确的识别此节点
  • 找到正确的位置区插入新的节点。

所以一句话,key的作用主要是为了高效的更新虚拟DOM。

image-20210715214510060

4.数组中哪些方法是响应式的?

  • 因为Vue是响应式的,所以当数据发生变化时,Vue会自动检测数据变化,视图会发生对应的更新。
  • Vue中包含了一组观察数组编译的方法,使用它们改变数组也会触发视图的更新。
    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()

代码见,

 
 
  <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();
 
          // 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()
 
          // **注意: 通过索引值修改数组中的元素 
          // Vue内部没有监听这种方式 不是响应式 数组有变化,界面无更新
          // this.letters[0] = 'bbbbbb';
 
          // 用其他方法
          // 法1
          // this.letters.splice(0, 1, 'bbbbbb')
 
          // 法2
          // 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);
    // }
    //
    // sum(20, 30, 40, 50, 601, 111, 122, 33)
  </script>

5.阶段案例–图书购物车

  • 代码实现:HTML、CSS

image-20210730091738094

  • 代码实现:JS代码

image-20210730091823867

  • 重点:
    • 价格保留两位小数 ----Vue过滤器
    • 点击数量按钮,改变对应的数量—用index区分
    • 数量最小只能减到1—动态绑定disabled属性

补充:

vue 过滤器filter(全面)

HTML代码:

 
  <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>
              <!-- 价格 保留两位小数 拼接 ¥ -->
              <!-- 法1  总价格那里也需要这个 所以这样写不太好 -->
              <!-- {{item.price.toFixed(2)}} -->
 
              <!-- 法2 调用方法 -->
              <!-- {{getFinalPrice(item.price)}} -->
 
              <!-- 法3 过滤器 -->
              {{item.price | showPrice}}
 
            </td>
            <td>
              <!-- v-bind动态绑定属性如果为true就表示有这个属性 v-bind:disabled="true" 渲染出来就是disabled 所以这里只需要一个布尔值就行 -->
              <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 src="main.js"></script>

CSS代码:

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;
}

JS代码:

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)
    // }
 
    increment(index) {
      this.books[index].count++
    },
    decrement(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
 
      // 其他计算总价格方法
      // for (let i in/of this.books)
      // reduce
    }
  },
  filters: { // 过滤器
    showPrice(price) { // 参数是你要过滤的东西
      // toFixed 保留两位小数
      return '¥' + price.toFixed(2)
    }
  }
})

计算总价格其他方法 :

 
  computed: {
    totalPrice() {
      // 1.普通的for循环
      // let totalPrice = 0
      // for (let i = 0; i < this.books.length; i++) {
      //   totalPrice += this.books[i].price * this.books[i].count
      // }
      // return totalPrice
 
      // 2.for (let i in this.books)
      // let totalPrice = 0
      // for (let i in this.books) {
      //   const book = this.books[i]
      //   totalPrice += book.price * book.count
      // }
      //
      // return totalPrice
 
      // 3.for (let i of this.books)
      // let totalPrice = 0
      // for (let item of this.books) {
      //   totalPrice += item.price * item.count
      // }
      // return totalPrice
 
      return this.books.reduce(function (preValue, book) {
        return preValue + book.price * book.count
      }, 0)
    }
  },

也可以使用高阶函数reduce,下面介绍高阶函数

6.(掌握) JavaScript高阶函数的使用

  • filter/map/reduce
// 编程范式: 命令式编程/声明式编程
// 编程范式: 面向对象编程(第一公民:对象)/函数式编程(第一公民:函数)
 
// filter/map/reduce
 
// filter中的回调函数有一个要求: 必须返回一个boolean值
// true: 当返回true时, 函数内部会自动将这次回调的n加入到新的数组中
// false: 当返回false时, 函数内部会过滤掉这次的n
const nums = [10, 20, 111, 222, 444, 40, 50]
 
// 高阶函数 本身参数也是一个函数
// let total = nums.filter(n => n < 100).map(n => n * 2).reduce((pre, n) => pre + n);
// console.log(total);
 
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);
 
// 1.filter函数的使用
// 10, 20, 40, 50
let newNums = nums.filter(function (n) {
  return n < 100
})
// console.log(newNums);
 
// 2.map函数的使用
// 20, 40, 80, 100
let new2Nums = newNums.map(function (n) { // 20
  return n * 2
})
console.log(new2Nums);
 
// 3.reduce函数的使用
// reduce 作用对数组中所有的内容进行汇总
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
 
 
// 普通写法
// 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);

8.表单绑定v-model

1.表单绑定v-model

  • 表单控件在实际开发中是非常常见的。特别是对于用户信息的提交,需要大量的表单。

  • Vue中使用v-model指令来实现表单元素和数据的双向绑定。

  • 案例的解析:

    • 当我们在输入框输入内容时
    • 因为input中的v-model绑定了message,所以会实时将输入的内容传递给message,message发生改变。
    • 当message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。
    • 所以,通过v-model实现了双向的绑定。

image-20210730092454438

  • 当然,我们也可以将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>

2.v-model原理

  • v-model其实是一个语法糖,它的背后本质上是包含两个操作:

    • 1.v-bind绑定一个value属性
    • 2.v-on指令给当前元素绑定input事件
  • 也就是说下面的代码:

    <input type="text" v-model="message">
    
  • 等同于下面的代码:

    <input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
    

代码,

 
  <div id="app">
    <!--<input type="text" v-model="message">-->
 
    <!-- 上面等同于 -->
    <!--<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>

3.v-model结合radio类型使用

  • 当存在多个单选框时

  • ****单选按钮radio的value会影响v-model的值****(input得有value属性,value是什么获取到的就是什么)

 
  <div id="app">
    <label for="male">
      <!-- 需要加相同的name 否则可以多选 -->
      <!-- <input type="radio" id="male" value="男" name="sex">男 -->
      <!-- 一旦v-moddel绑定的是同一个变量,name可以不用写 -->
      <input type="radio" id="male" value="" v-model="sex"></label>
    <label for="female">
      <input type="radio" id="female" value="" v-model="sex"></label>
    <label for="other">
      <input type="radio" id="other" value="其他" v-model="sex">其他
    </label>
    <h2>您选择的性别是: {{sex}}</h2>
  </div>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊',
        sex: '女' // 可以给radio默认值
      }
    })
  </script>

4.v-model结合checkbox类型使用

  • 复选框分为两种情况:单个勾选框和多个勾选框

  • 单个勾选框:

    • v-model即为布尔值。
    • 此时input的value并不影响v-model的值。
  • 多个复选框:

    • 当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
    • 当选中某一个时,就会将input的value添加到数组中。

image-20210730093757413

代码:

 
  <div id="app">
    <!--1.checkbox单选框 -->
    <!-- 
      v-model即为布尔值true/false。
      此时input的value并不影响v-model的值
 -->
    <label for="agree">
      <input type="checkbox" id="agree" v-model="isAgree">同意协议
    </label>
    <h2>您选择的是: {{isAgree}}</h2>
    <button :disabled="!isAgree">下一步</button>
 
    <!--2.checkbox多选框-->
    <!-- 
      当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
      当选中某一个时,就会将input的value添加到数组中。
   -->
    <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赋值 -->
    <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: '你好啊',
        isAgree: false, // 单选框
 
        hobbies: [], // 多选框,
        originHobbies: ['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
      }
    })
  </script>

5.v-model结合select类型使用

  • 和checkbox一样,select也分单选和多选两种情况。

  • 单选:只能选中一个值。

    • v-model绑定的是一个值。
    • 当我们选中option中的一个时,会将它对应的value赋值到mySelect中
  • 多选:可以选中多个值。

    • v-model绑定的是一个数组。
    • 当选中多个值时,就会将选中的option对应的value添加到数组mySelects中

代码,

 
  <div id="app">
    <!--1.选择一个-->
    <!-- 
    单选:只能选中一个值。
    v-model绑定的是一个值。
    当我们选中option中的一个时,会将它对应的value赋值到mySelect中
   -->
    <!-- v-model绑定在select标签 -->
    <select name="abc" v-model="fruit">
      <option value="苹果">苹果</option>
      <option value="香蕉">香蕉</option>
      <option value="榴莲">榴莲</option>
      <option value="葡萄">葡萄</option>
    </select>
    <h2>您选择的水果是: {{fruit}}</h2>
 
    <!--2.选择多个-->
    <!-- 
    v-model绑定的是一个数组。
    当选中多个值时,就会将选中的option对应的value添加到数组mySelects中
   -->
    <!-- 加上multiple属性就可以多选 要按ctrl才能多选 -->
    <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>

6.input中的值绑定

  • 初看Vue官方值绑定的时候,我很疑惑:what the hell is that?

  • 但是仔细阅读之后,发现很简单,就是动态的给value赋值而已:

    • 我们前面的value中的值,可以回头去看一下,都是在定义input的时候直接给定的。
    • 但是真实开发中,这些input的值可能是从网络获取或定义在data中的。
    • 所以我们可以通过v-bind:value动态的给value绑定值。
    • 这不就是v-bind吗?
  • 这不就是v-bind在input中的应用吗?搞的我看了很久,搞不清他想将什么。

  • 这里不再给出对应的代码,因为会用v-bind,就会值绑定的应用了。

7.v-model修饰符

lazy修饰符:
  • 默认情况下,v-model默认是在input事件中同步输入框的数据的。
  • 也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。
  • lazy修饰符可以让数据在失去焦点或者回车时才会更新:
number修饰符:
  • 默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。
  • 但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。
  • number修饰符可以让在输入框中输入的内容自动转成数字类型:
trim修饰符:
  • 如果输入的内容首尾有很多空格,通常我们希望将其去除
  • trim修饰符可以过滤内容左右两边的空格

代码见,06-v-model修饰符的使用.html

 
<div id="app">
  <!--1.修饰符: lazy 让数据在失去焦点或者回车时才会更新-->
  <input type="text" v-model.lazy="message">
  <h2>{{message}}</h2>
 
 
  <!--2.修饰符: number 让在输入框中输入的内容自动转成数字类型-->
  <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: ''
    }
  })
 
  var age = 0
  age = '1111'
  age = '222'
</script>

三、组件化开发

内容概述

认识组件化

注册组件

组件其他补充

组件数据存放

父子组件通信

父级向子级传递

子级向父级传递

插槽slot

01-认识组件化

1.什么是组件化

  • 人面对复杂问题的处理方式:
    • 任何一个人处理信息的逻辑能力都是有限的
    • 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
    • 但是,我们人有一种天生的能力,就是将问题进行拆解。
    • 如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
  • 组件化也是类似的思想:
    • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
    • 但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了

image-20210730111556106

2.Vue组件化思想

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

image-20210730111857686

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

02-组件化基础

1.注册组件

1.1 注册组件的基本步骤
组件的使用分成三个步骤:
  • 创建组件构造器
  • 注册组件
  • 使用组件。

我们来看看通过代码如何注册组件
查看运行结果:
  • 和直接使用一个div看起来并没有什么区别。
  • 但是我们可以设想,如果很多地方都要显示这样的信息,我们是不是就可以直接使用来完成呢?

组件的基本使用(全局组件)
 
  <!-- 
  组件的使用分成三个步骤:
    1.创建组件构造器
    2.注册组件
    3.使用组件。
 -->
  <div id="app">
    <!--3.在Vue实例范围内使用组件-->
    <my-cpn></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.创建组件构造器对象  extend() 没有s
    const cpnC = Vue.extend({
      // 自定义组件的模板 使用到组件的地方,要显示的HTML代码
      // *最外需要一个div包裹
      template: `
      <div>
        <h2>我是标题</h2>
        <p>我是内容, 哈哈哈哈</p>
        <p>我是内容, 呵呵呵呵</p>
      </div>`
    })
 
    // 2.注册组件(全局注册) 
    // 需要传递两个参数:
    // 1、注册组件的标签名 (必须加引号)
    // 2、组件构造器
 
    Vue.component('my-cpn', cpnC)
 
 
 
    // 以上两步需要在Vue实例创建之前
 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  </script>
1.2 注册组件的步骤解析

这里的步骤都代表什么含义呢?

1.Vue.extend():

  • 调用Vue.extend()创建的是一个组件构造器。

  • 通常在创建组件构造器时,传入template代表我们自定义组件的模板。

  • 该模板就是在使用到组件的地方,要显示的HTML代码。

  • 事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。

2.Vue.component():

  • 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
  • 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器

3.组件必须挂载在某个Vue实例下,否则它不会生效。(见下页)

  • 我们来看下面我使用了三次
  • 而第三次其实并没有生效:

image-20210716161145222

1.3 全局和局部组件
  • 当我们通过调用Vue.component()注册组件时,组件的注册是全局的

    • 这意味着该组件可以在任意Vue示例下使用。

  • 如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件

代码见,
 
<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.注册组件(component注册的是全局组件, 意味着可以在多个Vue的实例下面使用,比如app,和app2都可以使用cpn)
 
  // Vue.component('cpn', cpnC); // '标签名一定要加引号'
 
  // 疑问: 怎么注册的组件才是局部组件了? 挂载在某个实例中
 
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      // 使用组件时的标签名:组件构造器
      // 'cpn': cpnC  局部组件的标签名有无引号都可以
      cpn: cpnC
    }
  })
 
  const app2 = new Vue({
    el: '#app2'
  })
</script>
补充:组件名命名规范
  • 1.全部小写,使用短横线-连接

    • 必须在引用这个自定义元素时使用 短横线分隔命名,
    • 例如
  • 2.或驼峰命名

    • 引用这个自定义元素时两种命名法都可以使用。
    • 也就是说 和 都是可接受的
1.4 父组件和子组件
  • 在前面我们看到了组件树:
    • 组件和组件之间存在层级关系
    • 而其中一种非常重要的关系就是父子组件的关系
  • 我们来看通过代码如何组成的这种层级关系

  • 父子组件错误用法:以子标签的形式在Vue实例中使用

    • 因为当子组件注册到父组件的components时,Vue会编译好父组件的模块
    • 该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了
    • 是只能在父组件中被识别的。
    • 类似这种用法,是会被浏览器忽略的。
  • 父子组件正确用法:在 父组件的components注册,在template 中使用子组件标签

代码见,

 
  <div id="app">
    <cpn2></cpn2>
    <!-- 父子组件错误用法:以子标签的形式在Vue实例中使用 -->
    <!--<cpn1></cpn1>-->
    <!-- 正确写法 在 template 中使用 <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({
      // 2.2 在 template 中使用 <cpn1></cpn1>
      template: `
      <div>
        <h2>我是标题2</h2>
        <p>我是内容, 呵呵呵呵</p>
        <cpn1></cpn1>
      </div>
    `,
      // 2.1 在components注册子组件cpn1
      components: {
        cpn1: cpnC1
      }
    })
 
    // root组件
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn2: cpnC2
      }
    })
  </script>
1.5 注册组件的语法糖

在上面注册组件的方式,可能会有些繁琐。

  • Vue为了简化这个过程,提供了注册的语法糖。

  • 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。

语法糖注册全局组件和局部组件:

image-20210716170011046

image-20210716170047229

代码见,

 
  <div id="app">
    <cpn1></cpn1>
    <cpn2></cpn2>
  </div>
 
  <script src="../js/vue.js"></script>
  <script>
    // 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替
 
    // 1.全局组件注册的语法糖
    // * Vue.component标签名必须加引号
    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>
1.6 模板的分离写法

刚才,我们通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。

如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。

Vue提供了两种方案来定义HTML模块内容:

  • 使用<script>标签

  • 使用<template>标签

代码见,

 
  <div id="app">
    <cpn></cpn>
    <cpn></cpn>
    <cpn></cpn>
  </div>
 
  <!--1.script标签, 注意:类型必须是text/x-template 然后给它设置一个id -->
  <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>

补充

1.7 组件的其他属性
1.组件可以访问Vue实例数据吗?
  • 组件是一个单独功能模块的封装:
    • 这个模块有属于自己的HTML模板,也应该有属性自己的数据data。
  • 组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
    • 我们先来测试一下,组件中能不能直接访问Vue实例中的data

image-20210730153837519

  • 我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿。

  • 结论:Vue组件应该有自己保存数据的地方。

2.组件数据的存放

组件自己的数据存放在哪里呢?

  • 组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
  • 只是这个data属性必须是一个函数
  • 而且这个函数返回一个对象,对象内部保存着数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AHc7QGmo-1631696029338)(C:\Users\atiff\AppData\Roaming\Typora\typora-user-images\image-20210730154101436.png)]

代码见,

 
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>
 
<template id="cpn">
  <div>
    <h2>{{title}}</h2>
    <p>我是内容,呵呵呵</p>
  </div>
</template>
 
<script src="../js/vue.js"></script>
<script>
 
  // 1.注册一个全局组件
  Vue.component('cpn', {
    template: '#cpn',
    data() {
      return {
        title: 'abc'
      }
    }
  })
// 组件是不能直接访问Vue实例中的data数据
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      // title: '我是标题'
    }
  })
 
</script>
3.为什么是一个函数呢?

为什么data在组件中必须是一个函数呢?

  • 首先,如果不是一个函数,Vue直接就会报错。
  • 其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。

image-20210730154251749

代码见,

 <!--组件实例对象-->
  <div id="app">
    <cpn></cpn>
    <cpn></cpn>
    <cpn></cpn>
  </div>
 
  <template id="cpn">
    <div>
      <h2>当前计数: {{counter}}</h2>
      <button @click="increment">+</button>
      <button @click="decrement">-</button>
    </div>
  </template>
  <script src="../js/vue.js"></script>
  <script>
    // 1.注册组件
    const obj = {
      counter: 0
    }
    Vue.component('cpn', {
      template: '#cpn',
      // data() {
      //   return {
      //     counter: 0
      //   }
      // },
      data() {
        // 会一起改变
        return obj
      },
      methods: {
        increment() {
          this.counter++
        },
        decrement() {
          this.counter--
        }
      }
    })
 
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      }
    })
  </script>
  <script>
    const object = {
      name: 'why',
      age: 18
    }
 
 
    // function abc() {
    // 共用一个对象
    //   return object;
    // }
 
    function abc() {
      // 每调用一次函数都返回一个新的对象
      return {
        name: 'why',
        age: 18
      }
    }
 
    let obj1 = abc();
    let obj2 = abc();
    let obj3 = abc();
 
    obj1.name = 'kobe'
    console.log(obj2);
    console.log(obj3);
  </script>
  • 补充(结合网络资料)
    • Object是引用数据类型,如果不用function返回,每个组件的data都是内存的同一个地址,一个数据改变了其他也改变了;
    • JavaScript只有函数构成作用域(注意理解作用域,只有函数{}构成作用域,对象的{}以及if(){}都不构成作用域),data是一个函数时,每个组件实例都有自己的作用域,每个实例相互独立,不会相互影响。
    • 组件中的data写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。
    • 而单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。
    • 所以说vue组件的data必须是函数。这都是因为js的特性带来的,跟vue本身设计无关

2.数据传递

在上一个小节中,我们提到了子组件是不能引用父组件或者Vue实例的数据的。

但是,在开发中,往往一些数据确实需要从上层传递到下层:

  • 比如在一个页面中,我们从服务器请求到了很多的数据。
  • 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
  • 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。

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

  • 通过props向子组件传递数据(父传子)
  • 通过事件向父组件发送消息(子传父)

image-20210730154959677

在下面的代码中,我直接将Vue实例当做父组件,并且其中包含子组件来简化代码。

真实的开发中,Vue实例和子组件的通信父组件和子组件的通信过程是一样的

2.1 父级向子级传递-props
1.props基本用法
  • 在组件中,使用选项props来声明需要从父级接收到的数据
  • props的值有两种方式:
    • 方式一:字符串数组,数组中的字符串就是传递时的名称。
    • 方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
  • 我们先来看一个最简单的props传递

代码,

<!DOCTYPE html>
<html lang="en">
 
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
 
<body>
 
  <div id="app">
    <!--<cpn v-bind:cmovies="movies"></cpn>-->
    
    <!-- 没有v-bind直接这样写是给prop传递一个静态的值,也就是说movies不是一个变量而是一个字符串 -->
    <!--<cpn cmovies="movies" cmessage="message"></cpn>-->
 
    <!-- 步骤2 通过:cmessage="message" 将data中的数据传给子组件props -->
    <cpn :cmessage="message" :cmovies="movies"></cpn>
  </div>
 
 
  <!-- 子组件 -->
  <template id="cpn">
    <div>
      <!-- 步骤3 将props中的值显示在子组件中 -->
      <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',
 
      // 子组件通过prop接收  我们能够在组件实例中访问这个值,就像访问 data 中的值一样
 
      /* ***步骤1*** 在 子组件 定义props */
      // ****方式1:字符串数组,数组中的字符串就是传递时的名称(之后要引用的变量名)
      // props: ['cmovies', 'cmessage'], // 不要把元素当成字符串,把它当成数组
 
 
      
 
      data() {
        return {}
      }
    }
 
 
    // -----父组件-----
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊',
        movies: ['海王', '海贼王', '海尔兄弟']
      },
      components: {
        cpn
      }
    })
  </script>
 
<!-- 
  步骤:
  1.在子组件里写props
  2.在子组件的标签加上v-bind  <cpn v-bind:props里定义的名称="父组件data数据名称"></cpn>
  3.将props中的值显示在子组件中
 -->
 
  <!-- 
  工厂函数
    1,它是一个函数。
    2,它用来创建对象。
    3 ,它像工厂一样,“生产”出来的函数都是“标准件”(拥有同样的属性)
 -->
 
</body>
 
</html>
2.props数据验证—对象写法
  • 在前面,我们的props选项是使用一个数组。

  • 我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。

  • 验证都支持哪些数据类型呢?

    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol
  • 当我们有自定义构造函数时,验证也支持自定义的类型

代码见,

// ***方式2:对象,对象可以设置传递时的类型,也可以设置默认值等->当需要对props进行类型等验证时
      props: {
        // 1.类型限制
        // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
        // cmovies: Array,
        // cmessage: String,
 
        // 多个可能的类型
        // propB: [String, Number],
 
        // 2.提供一些默认值, 以及必传值
        cmessage: {
          type: String,
          default: 'aaaaaaaa',
          required: true // 必填的字符串
        },
        // 类型是对象或者数组时, 默认值必须是一个工厂函数
        cmovies: {
          type: Array,
          default () {
            return {
              message: 'hello'
            }
          }
        },
        // 自定义验证函数
        propF: {
          validator: function (value) {
            // 这个值必须匹配下列字符串中的一个
            return ['success', 'warning', 'danger'].indexOf(value) !== -1
          }
        }
 
      },
3.props中的驼峰标识
  • HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:

代码见,

 
  <div id="app">
    <!-- v-bind 不支持驼峰 需要换成 -  -->
    <cpn :c-info="info" :child-my-message="message" v-bind:class></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: {
        // 在这里使用驼峰 :c-info="info" 那里要用 -
        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>
2.2 子级向父级传递—自定义事件
1.子级向父级传递
  • props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。

  • 我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。

  • 什么时候需要自定义事件呢?

    • 当子组件需要向父组件传递数据时,就要用到自定义事件了。
    • 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
  • 自定义事件的流程:

    • 1.在子组件中,通过$emit()来触发事件。
    • 2.在父组件中,通过v-on来监听子组件事件。
  • 我们来看一个简单的例子:

    • 我们之前做过一个两个按钮+1和-1,点击后修改counter。
    • 我们整个操作的过程还是在子组件中完成,但是之后的展示交给父组件。
    • 这样,我们就需要将子组件中的counter,传给父组件的某个属性,比如total。
  • 图片

代码见,

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

<!--父组件模板-->
<div id="app">
  <!--v-on监听子组件的自定义事件-->
  <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) {
        // 发射事件: 自定义事件
        this.$emit('item-click', item)
      }
    }
  }

  // 2.父组件
  const app = new Vue({
    el: '#app',
    data: {

    },
    components: {
      cpn
    },
    methods: {
      cpnClick(item) {
        console.log('cpnClick', item);
      }
    }
  })
</script>

</body>
</html>
2.父传子–结合双向绑定案例
  • 需求

    • 子组件input绑定v-model,input改变,props里的number1、number2跟着改变,vue实例data里的num1,num2也跟着变
    • 子组件data的dnumber1一改变,dnumber2就 *100,dnumber2一改变,dumber/100
  • 分析

    • 之前的v-model是绑定vue实例data里面的数据
    • 如果v-model绑定了props里的值,会报错(props里的值最好是通过父组件修改)
    • v-model不要绑定props里的值\rightarrow用data或computed代替\rightarrow把number1,number2分别赋值给data的dnumber1,dnumber2

代码,

<!-- 父组件 -->
  <div id="app">
    <h3>父组件</h3>
    <h3>-----num1----</h3>
    <h3>{{num1}}</h3>
    <h3> -----num2----</h3>
 
    <h3>{{num2}}</h3>
    <hr>
    <cpn :number1="num1" :number2="num2" @num1change="num1change" @num2change="num2change" />
  </div>
 
  <!-- 子组件 -->
  <template id="cpn">
    <div>
      <!-- 这样写会报错 应该是由父组件修改它,避免直接修改props的值 -->
      <!-- 
        <input v-model="number1" type="text" />
      <input v-model="number2" type="text" />
     -->
 
      <h3>子组件</h3>
 
      <h3> -----number1----</h3>
      <!-- 为什么props也会跟着一起变? -> number1绑定的是父组件num1 -->
      <h2>props:{{number1}}</h2>
      <h2>data:{{dnumber1}}</h2>
 
      <!--<input type="text" v-model="dnumber1">-->
      <!-- v-model的本质 用@input来传值 -->
      <input type="text" :value="dnumber1" @input="num1Input">
 
      <h3>-----number2----</h3>
 
      <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 cpn = {
      template: '#cpn',
      props: {
        number1: Number,
        number2: Number
      },
      data() {
        return {
          dnumber1: this.number1,
          dnumber2: this.number2
        }
      },
      methods: {
        num1Input(event) {
          // 1.将input中的value赋值到dnumber中
          this.dnumber1 = event.target.value;
 
          // 2.为了让父组件可以修改值, 发出一个事件
          this.$emit('num1change', this.dnumber1)
 
          // 3.同时修饰dnumber2的值
          this.dnumber2 = this.dnumber1 * 100;
          this.$emit('num2change', this.dnumber2);
        },
        num2Input(event) {
          this.dnumber2 = event.target.value;
          this.$emit('num2change', this.dnumber2)
 
          // 同时修饰dnumber1的值
          this.dnumber1 = this.dnumber2 / 100;
          this.$emit('num1change', this.dnumber1);
        }
      }
 
 
    }
 
 
    // 父组件
    const app = new Vue({
      el: '#app',
      data: {
        num1: 1,
        num2: 0
      },
      methods: {
        num1change(value) {
          // value传过来的是string类型,需要转换成数字
          this.num1 = parseFloat(value)
        },
        num2change(value) {
          this.num2 = parseFloat(value)
        }
      },
      components: {
        cpn
      }
    })
  </script>
3.(了解)父传子–结合双向绑定案例(watch实现)

代码见,

 
<div id="app">
  <cpn :number1="num1"
       :number2="num2"
       @num1change="num1change"
       @num2change="num2change"/>
</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: 0
    },
    methods: {
      num1change(value) {
        this.num1 = parseFloat(value)
      },
      num2change(value) {
        this.num2 = parseFloat(value)
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        props: {
          number1: Number,
          number2: Number,
          name: ''
        },
        data() {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        watch: {
          dnumber1(newValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue);
          },
          dnumber2(newValue) {
            this.number1 = newValue / 100;
            this.$emit('num2change', newValue);
          }
        }
      }
    }
  })
</script>
2.3 个人补充—侦听器watch

参考文章:Vue——watch选项详解_

1.什么是watch选项?(官方解释)

类型:{ [key: string]: string | Function | Object | Array }
详细:一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例i化时调用 $watch(),遍历 watch 对象的每一个属性。

2.通俗解释

watch选项能够监听值的变化。

3.基本使用

代码见,

 <!-- watch选项能够监听值的变化 -->
    <div id="app">
        <input type="text" v-model="number">
    </div>
    <script src='../js/vue.js'></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                number: 1,
            },
            watch: {
                // 监听number变量,当它有变化执行
                /* number(newValue, oldValue) { // newValue新值,oldValue旧值 参数只写newValue也可以
                    console.log('newVal', newValue);
                    console.log('oldVal', oldValue);
                } */
                // 也可以这样写
                number: {
                    handler(newValue, oldValue) { //handler方法就是你watch中需要具体执行的方法
                        console.log('newVal', newValue);
                        console.log('oldVal', oldValue);
                    },
                    immediate: true
                    //immediate为true时则立即触发回调函数;如果为false,则和上面的例子一样,不会立即执行回调。
                }
            }
        })
    </script>
2.4 其他补充
2.5 父子组件的访问
1.父子组件的访问方式: $children(父访问子)
  • 有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问根组件。
    • 父组件访问子组件: c h i l d r e n 或 children或 childrenrefs(引用)
    • 子组件访问父组件:使用$parent
  • 我们先来看下**$children**的访问
    • this.$children是一个数组类型,它包含所有子组件对象。
    • 我们这里通过一个遍历,取出所有子组件的message状态。

图片

2.父子组件的访问方式: $refs(父访问子)

this.$children是一个数组类型,它包含所有子组件对象。

这里通过一个遍历,取出所有子组件的message状态。

  • $children的缺陷:

    • 通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值
    • 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
    • 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
  • $refs的使用:

    • $refsref指令通常是一起使用的。
    • 首先,我们通过ref给某一个子组件绑定一个特定的ID
    • 其次,通过this.$refs.ID就可以访问到该组件了。

image-20210730201642126

代码见,

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>

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

  <my-cpn></my-cpn>
  <y-cpn></y-cpn>

  <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() {
        // 1.$children
        // console.log(this.$children);
        // for (let c of this.$children) {
        //   console.log(c.name);
        //   c.showMessage();
        // }
        // console.log(this.$children[3].name);

        // 2.$refs => 对象类型, 默认是一个空的对象 ref='bbb'
        console.log(this.$refs.aaa.name);
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是子组件的name'
          }
        },
        methods: {
          showMessage() {
            console.log('showMessage');
          }
        }
      },
    }
  })
</script>

</body>
</html>

图片

3.父子组件的访问方式: p a r e n t ( 子 访 问 父 ) 、 parent(子访问父)、 parent(访)root(根组件)
  • 如果我们想在子组件中直接访问父组件,可以通过$parent

  • 注意事项:

    • 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。
    • 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
    • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
    • 另外,更不好做的是通过$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() {
                // 1.访问父组件$parent
                // console.log(this.$parent);
                // console.log(this.$parent.name);
 
                // 2.访问根组件$root
                console.log(this.$root);
                console.log(this.$root.message);
              }
            }
          }
        }
      }
    }
  })
</script>
2.6 非父子组件通信
  • 刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?

    • 非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。
  • 在Vue1.x的时候,可以通过 d i s p a t c h 和 dispatch和 dispatchbroadcast完成

    • $dispatch用于向上级派发事件
    • $broadcast用于向下级广播事件
    • 但是在Vue2.x都被取消了
  • 在Vue2.x中,有一种方案是通过中央事件总线,也就是一个中介来完成。

    • 但是这种方案和直接使用Vuex的状态管理方案还是逊色很多。
    • 并且Vuex提供了更多好用的功能,所以这里我们暂且不讨论这种方案,后续我们专门学习Vuex的状态管理

03-组件化高级

1.插槽slot

1.1为什么使用slot
  • slot翻译为插槽:

    • 在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
    • 插槽的目的是让我们原来的设备具备更多的扩展性。
    • 比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
  • 组件的插槽:

    • 组件的插槽也是为了让我们封装的组件更加具有扩展性。
    • 让使用者可以决定组件内部的一些内容到底展示什么。
  • 栗子:移动网站中的导航栏。

    • 移动开发中,几乎每个页面都有导航栏。
    • 导航栏我们必然会封装成一个插件,比如nav-bar组件。
    • 一旦有了这个组件,我们就可以在多个页面中复用了。
  • 但是,每个页面的导航是一样的吗?No,我以京东M站为例

image-20210730202824913

1.2 如何封装这类组件呢?slot
  • 如何去封装这类的组件呢?

    • 它们也很多区别,但是也有很多共性。
    • 如果,我们每一个单独去封装一个组件,显然不合适:比如每个页面都返回,这部分内容我们就要重复去封装。
    • 但是,如果我们封装成一个,好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等。
  • 如何封装合适呢?抽取共性,保留不同。

    • 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
    • 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
    • 是搜索框,还是文字,还是菜单。由调用者自己来决定。
  • 这就是为什么我们要学习组件中的插槽slot的原因

1.3 slot的基本使用
  • 了解了为什么用slot,我们再来谈谈如何使用slot?

    • 在子组件中,使用特殊的元素就可以为子组件开启一个插槽。
    • 该插槽插入什么内容取决于父组件如何使用。
  • 我们通过一个简单的例子,来给子组件定义一个插槽:

    • 中的内容表示,如果没有在该组件中插入任何其他内容,就默认显示该内容
    • 有了这个插槽后,父组件如何使用呢

image-20210730203542571

  • 简而言之,插槽就是预留的空间(位置),即占位符

代码见,

 
 
  <!--
    插槽
      插槽,也就是slot,是组件的一块HTML模板,这块模板 显示不显示、以及 怎样显示 由父组件来决定
    
    单个插槽 | 默认插槽 | 匿名插槽
      可以放置在组件的任意位置 一个组件中只能有一个该类插槽
      
      1.插槽的基本使用 <slot></slot>
    
      2.插槽的默认值 <slot>button</slot>
    
      3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
-->
 
  <div id="app">
    <cpn></cpn>
 
    <cpn>
      <span>哈哈哈</span>
    </cpn>
 
    <cpn>
      <i>呵呵呵</i>
    </cpn>
 
    <cpn>
      <i>呵呵呵</i>
      <div>我是div元素</div>
      <p>我是p元素</p>
    </cpn>
 
    <cpn></cpn>
    <cpn></cpn>
 
    <!-- 如果不使用插槽,往组件标签里写东西是没有效果的 -->
    <cpn2>略略略</cpn2>
  </div>
 
 
  <template id="cpn">
    <div>
      <h2>我是组件</h2>
      <p>我是组件, 哈哈哈</p>
      <slot><button>按钮</button></slot>
      <!--<button>按钮</button>-->
    </div>
  </template>
  <template id="cpn2">
    <div>
      <h2>我是组件222</h2>
      <p>我是组件22222, 哈哈哈</p>
    </div>
  </template>
 
  <script src="../js/vue.js"></script>
  <script>
    const app = new Vue({
      el: '#app',
      data: {
        message: '你好啊'
      },
      components: {
        cpn: {
          template: '#cpn'
        },
        cpn2: {
          template: '#cpn2'
        }
 
      }
    })
  </script>
 
1.4 slot的具名插槽

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC

推荐文章:vue_插槽的理解和使用 - 圆圆方方 - 博客园 (cnblogs.com)

  • 当子组件的功能复杂时,子组件的插槽可能并非是一个。

    • 比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
    • 那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
    • 这个时候,我们就需要给插槽起一个名字
  • 如何使用具名插槽呢?

    • 非常简单,只要给slot元素一个name属性即可
  • 我们来给出一个案例:

    • 这里我们先不对导航组件做非常复杂的封装,先了解具名插槽的用法。

image-20210730213140501

代码(vue2.5版本):

<!-- 
  具名插槽其实就是给插槽取个名字。一个子组件可以放多个插槽,而且可以放在不同的地方,
  而父组件填充内容时,可以根据这个名字把内容填充到对应插槽中。
  具名插槽就可以有很多个,只要名字(name属性)不同就可以了
 -->
  <div id="app">
 
    <!-- 这个只能替换没有名字的插槽-->
    <cpn><span>没有名字的替换</span></cpn>
    <!-- vue2.5的写法  -->
    <!-- 下面这两个还是会显示没有名字的插槽的默认内容 -->
    <cpn><span slot="center">标题</span></cpn>
    <cpn><button slot="left">返回</button></cpn>
  </div>
 
 
  <template id="cpn">
    <div>
      <!-- 没有名字的插槽 -->
      <slot>没有名字的插槽默认内容</slot>
 
 
      <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>
1.5(个人补充)vue2.6之后具名插槽的写法
   <!-- 
  注意 v-slot 只能添加在 < template > 上,只有当被提供的内容只有默认插槽时,
  组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件
 -->
    <div id="app">
        <!-- 默认 -->
        <cpn></cpn>
        <hr>
        <!-- 使用v-slot替换 -->
        <cpn>
            <!-- vscode 快捷语法 vslot-named -->
            <template v-slot:left>
                <span>返回</span>
            </template>
        </cpn>
        <cpn>
            <!--任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。或者可以给他起名default-->
            <!--<template v-slot:default>
                    我是内容
                </template>-->
 
            <template v-slot:center>
                <span>标题</span>
            </template>
            <span>替换没有名字的插槽</span>
        </cpn>
        <cpn>
        <!-- 
        跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。
        例如 v-slot:header 可以被重写为 #header:,前提是必须要有插槽名!!!
        -->
            <template #right>
                <span>替换后的右边</span>
            </template>
        </cpn>
    </div>
    <template id="cpn">
        <div>
            <slot name="left"><span>左边</span></slot>
            <slot name="center"><span>中间</span></slot>
            <slot name="right"><span>右边</span></slot>
            <slot>默认插槽内容</slot>
        </div>
    </template>
    <!-- 需要重新引入一个vue2.6之后的版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        const cpn = {
            template: '#cpn'
        }
        const app = new Vue({
            el: '#app',
            data: {},
            components: {
                cpn
            }
        })
    </script>
1.6 编译作用域
  • 在真正学习插槽之前,我们需要先理解一个概念:编译作用域。

  • 官方对于编译的作用域解析比较简单,我们自己来通过一个例子来理解这个概念:

  • 我们来考虑下面的代码是否最终是可以渲染出来的:

    • 中,我们使用了isShow属性。
    • isShow属性包含在组件中,也包含在Vue实例中。
  • 答案:最终可以渲染出来,也就是使用的是Vue实例的属性。

  • 为什么呢?

    • 官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
    • 而我们在使用的时候,整个组件的使用过程是相当于在父组件中出现的。
    • 那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
    • 因此,isShow使用的是Vue实例中的属性,而不是子组件的属性

代码见,

 <div id="app">
    <cpn v-show="isShow"></cpn>
    <cpn v-for="item in names"></cpn>
  </div>
 
  <template id="cpn">
    <div>
      <h2>我是子组件</h2>
      <p>我是内容, 哈哈哈</p>
      <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>
1.7.作用域插槽:准备
  • 作用域插槽是slot一个比较难理解的点,而且官方文档说的又有点不清晰。

  • 这里,我们用一句话对其做一个总结,然后我们在后续的案例中来体会:

    • 父组件替换插槽的标签,但是内容由子组件来提供。
    • (样式父组件说了算,但内容可以显示子组件插槽绑定的data)
  • 我们先提一个需求:

    • 子组件中包括一组数据,比如:pLanguages: [‘JavaScript’, ‘Python’, ‘Swift’, ‘Go’, ‘C++’]
    • 需要在多个界面进行展示:
      • 某些界面是以水平方向一一展示的,
      • 某些界面是以列表形式展示的,
      • 某些界面直接展示一个数组
    • 内容在子组件,希望父组件告诉我们如何展示,怎么办呢?
      • 利用slot作用域插槽就可以了
  • 我们来看看子组件的定义:

1.8 作用域插槽:使用
  • 在父组件使用我们的子组件时,从子组件中拿到数据:
    • 我们通过获取到slotProps属性
    • 在通过slotProps.data就可以获取到刚才我们传入的data了

  • 目的:让插槽内容能够访问引用的组件中才有的数据
  • 常用场景(以下为常用的情况之一)
    • 如果子组件中的某一部分的数据,每个父组件都会有自己的一套对该数据的不同的呈现方式,这时就需要用到作用域插槽。

代码(vue2.6之前):

<!-- vue2.6之前写法 -->
  <div id="app">
    <!-- 以下三个组件 内容一样,样式不同 -->
    <cpn></cpn>
 
    <cpn>
      <!--slot-scope声明属性名 接收子组件传递的数据pLanguages-->
      <template slot-scope="slot">
        <!-- 在dom标签使用该数据,通常用插值表达式接收具体数据 -->
        <!--<span v-for="item in slot.data"> - {{item}}</span>-->
        <h3>{{slot}}</h3>
        <span>{{slot.data.join(' - ')}}</span>
      </template>
    </cpn>
 
 
    <cpn>
      <template slot-scope="slot">
        <!--<span v-for="item in slot.data">{{item}} * </span>-->
        <span>{{slot.data.join(' * ')}}</span>
      </template>
    </cpn>
 
    <!--<cpn></cpn>-->
  </div>
 
  <!-- 子组件 template -->
  <template id="cpn">
    <div>
      <!-- 作用域插槽要求,在slot上面绑定数据 -->
      <slot :data="pLanguages">
        <ul>
          <li v-for="item in pLanguages">{{item}}</li>
        </ul>
      </slot>
      <hr>
    </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>
1.9(个人补充)vue2.6后作用域插槽的写法

代码:

 
    <div id="app">
        <!-- 默认 -->
        <h4>默认</h4>
        <cpn></cpn>
        <hr>
        <h4>替换样式</h4>
        <cpn>
            <!-- 具名插槽和作用域插槽混用 -->
            <template v-slot:slot1='props1'>
                <!-- 
                <span>
                    {{props1}}
                </span> -->
 
                <span>{{props1.data1.join('-')}}</span>
                <h3>
                    {{props1.msg}}
                </h3>
            </template>
            <template v-slot:slot2="props2">
                <h2 style="color: red;">
                    {{props2.data2}}
                </h2>
            </template>
        </cpn>
        <!--
            当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。
            这样我们就可以把 v-slot 直接用在组件上,但是不能和具名插槽混用
            -->
        <cpn v-slot="props3">
            <template>
                <h3 style="color: blue;">
                    {{props3.data3}}
                </h3>
            </template>
        </cpn>
    </div>
    <template id="cpn">
        <div>
            <!-- 可以传多个值 所有的值会包含在一个对象中 在父组件中v-slot=""中定义名字接收 -->
            <slot :data1='movies' :msg='message' name='slot1'>
                <ul>
                    <li v-for="(item, index) in movies" :key="index">
                        {{item}}
                    </li>
                </ul>
            </slot>
            <slot :data2='name' name='slot2'>
                {{name}}
            </slot>
            <slot :data3='defult'>默认插槽</slot>
        </div>
    </template>
    <!-- 需要重新引入一个vue2.6之后的版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <script>
        const cpn = {
            template: '#cpn',
            data() {
                return {
                    movies: ['战狼', '鬼吹灯', '盗墓笔记'],
                    message: '你好呀',
                    name: 'yangyanyan',
                    defult: '我是默认的数据'
                }
            },
        }
        const app = new Vue({
            el: '#app',
            data: {},
            components: {
                cpn
            }
        })
    </script>

动态组件

PPT无。ximnd中有

异步组件

PPT无。ximnd中有

04-组件声明周期

PPT无。ximnd中有

四、前端模块化

常见的模块化规范:

CommonJS、AMD、CMD,也有ES6的Modules

01-为什么需要模块化?

1.JavaScript原始功能

  • 在网页开发的早期,js制作作为一种脚本语言,做一些简单的表单验证或动画实现等,那个时候代码还是很少的。
    • 那个时候的代码是怎么写的呢?直接将代码写在

image-20210730215328324

  • 小明后来发现代码不能正常运行,去检查自己的变量,发现确实true
  • 最后杯具发生了,小明加班到2点还是没有找到问题出在哪里(所以,某些加班真的是无意义的)

2.匿名函数的解决方案

  • 我们可以使用匿名函数来解决方面的重名问题

    • 在aaa.js文件中,我们使用匿名函数

  • 但是如果我们希望在main.js文件中,用到flag,应该如何处理呢?

    • 显然,另外一个文件中不容易使用,因为flag是一个局部变量。

3.使用模块作为出口

  • 我们可以使用将需要暴露到外面的变量,使用一个模块作为出口,什么意思呢?
  • 来看下对应的代码:

五、Webpack详解

内容概述

认识webpack

webpack的安装

webpack的起步

webpack的配置

loader的使用

webpack中配置Vue

plugin的使用

搭建本地服务器

01-webpack

1.什么是webpack

  • 什么是webpack?

    • 这个webpack还真不是一两句话可以说清楚的。
  • 我们先看看官方的解释:

    • At its core, webpack is a static module bundler for modern JavaScript applications.
    • 从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具。
  • 但是它是什么呢?用概念解释概念,还是不清晰。

    • 我们从两个点来解释上面这句话:模块 和 打包

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X2DEHZ20-1631696029359)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\1.png)]

2.前端模块化

  • 前端模块化:
    • 在前面学习中,我已经用了大量的篇幅解释了为什么前端需要模块化。
    • 而且我也提到了目前使用前端模块化的一些方案:AMD、CMD、CommonJS、ES6。
    • 在ES6之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发。
    • 并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,并且将其进行整合打包。
    • 而webpack其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。
    • 而且不仅仅是JavaScript文件,我们的CSS、图片、json文件等等在webpack中都可以被当做模块来使用(在后续我们会看到)。
    • 这就是webpack中模块化的概念。
  • 打包如何理解呢?
    • 理解了webpack可以帮助我们进行模块化,并且处理模块间的各种复杂关系后,打包的概念就非常好理解了。
    • 就是将webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。
    • 并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作。
    • 但是打包的操作似乎grunt/gulp也可以帮助我们完成,它们有什么不同呢?

3.webpack和grunt/gulp对比

  • grunt/gulp的核心是Task

    • 我们可以配置一系列的task,并且定义task要处理的事务(例如ES6、ts转化,图片压缩,scss转成css)
    • 之后让grunt/gulp来依次执行这些task,而且让整个流程自动化。
    • 所以grunt/gulp也被称为前端自动化任务管理工具。
  • 我们来看一个gulp的task

    • 下面的task就是将src下面的所有js文件转成ES5的语法。

    • 并且最终输出到dist文件夹中。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1pZaEReI-1631696029360)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\2.png)]

  • 什么时候用grunt/gulp呢?

    • 如果你的工程模块依赖非常简单,甚至是没有用到模块化的概念。
    • 只需要进行简单的合并、压缩,就使用grunt/gulp即可。
    • 但是如果整个项目使用了模块化管理,而且相互依赖非常强,我们就可以使用更加强大的webpack了。
  • 所以,grunt/gulp和webpack有什么不同呢?

    • grunt/gulp更加强调的是前端流程的自动化,模块化不是它的核心。
    • webpack更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能。

02-webpack的安装

webpack安装

  • 安装webpack首先需要安装Node.js,Node.js自带了软件包管理工具npm

  • 查看自己的node版本:

    node -v
    
  • 全局安装webpack(这里我先指定版本号3.6.0,因为vue cli2依赖该版本)

    npm install webpack@3.6.0 -g
    
  • 局部安装webpack(后续才需要)

    • –save-dev`是开发时依赖,项目打包后不需要继续使用的。
    cd 对应目录
    npm install webpack@3.6.0 --save-dev
    
  • 为什么全局安装后,还需要局部安装呢?

    • 在终端直接执行webpack命令,使用的全局安装的webpack
    • 当在package.json中定义了scripts时,其中包含了webpack命令,那么使用的是局部webpack

03-webpack起步

1.准备工作

  • 我们创建如下文件和文件夹:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mVaCA13O-1631696029361)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\3.png)]

  • 文件和文件夹解析:

    • dist文件夹:用于存放之后打包的文件
    • src文件夹:用于存放我们写的源文件
      • main.js:项目的入口文件。具体内容查看下面详情。
      • mathUtils.js:定义了一些数学工具函数,可以在其他地方引用,并且使用。具体内容查看下面的详情。
    • index.html:浏览器打开展示的首页html
    • package.json:通过npm init生成的,npm包管理的文件(暂时没有用上,后面才会用上)
  • mathUtils.js文件中的代码:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TSZ38iru-1631696029363)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\4.png)]

  • main.js文件中的代码:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dvifJngz-1631696029363)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\5.png)]

2.js文件的打包

  • 现在的js文件中使用了模块化的方式进行开发,他们可以直接使用吗?不可以。

    • 因为如果直接在index.html引入这两个js文件,浏览器并不识别其中的模块化代码。
    • 另外,在真实项目中当有许多这样的js文件时,我们一个个引用非常麻烦,并且后期非常不方便对它们进行管理。
  • 我们应该怎么做呢?使用webpack工具,对多个js文件进行打包。

    • 我们知道,webpack就是一个模块化的打包工具,所以它支持我们代码中写模块化,可以对模块化的代码进行处理。(如何处理的,待会儿在原理中,我会讲解)
    • 另外,如果在处理完所有模块之间的关系后,将多个js打包到一个js文件中,引入时就变得非常方便了。
  • OK,如何打包呢?使用webpack的指令即可

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQVYb2sQ-1631696029364)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\6.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qY192Fxn-1631696029365)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\7.png)]

3.使用打包后的文件

  • 打包后会在dist文件下,生成一个bundle.js文件

    • 文件内容有些复杂,这里暂时先不看,后续再进行分析。

    • bundle.js文件,是webpack处理了项目直接文件依赖后生成的一个js文件,我们只需要将这个js文件在index.html中引入即可

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wtDbYBzj-1631696029366)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\8.png)]

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e6ivdLg3-1631696029367)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\9.png)]

04-webpack配置

1.入口和出口

  • 我们考虑一下,如果每次使用webpack的命令都需要写上入口和出口作为参数,就非常麻烦,有没有一种方法可以将这两个参数写到配置中,在运行时,直接读取呢?

  • 当然可以,就是创建一个webpack.config.js文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6zxRywby-1631696029368)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\10.png)]

  • 因为output的路径得是绝对路径,需要安装path包

  • 依赖到node的包,建议先npm init初始化一下,会出现下面信息,填package name后一路回车就行,然后会生成package.json文件

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hpfw2rR8-1631696029369)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\11.png)]

  • 在终端输入 webpack 就能直接打包了 ,但是在开发中一般不会用webpack,一般映射为npm run build

2.局部安装webpack

  • 目前,我们使用的webpack是全局的webpack,如果我们想使用局部来打包呢?

    • 因为一个项目往往依赖特定的webpack版本,全局的版本可能很这个项目的webpack版本不一致,导出打包出现问题。
    • 所以通常一个项目,都有自己局部的webpack。
  • 第一步,项目中需要安装自己局部的webpack

    • 这里我们让局部安装webpack3.6.0
    • Vue CLI3中已经升级到webpack4,但是它将配置文件隐藏了起来,所以查看起来不是很方便
    npm install webpack@3.6.0 --save-dev
    
  • 第二步,通过node_modules/.bin/webpack

    node_modules/.bin/webpack 
    

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OQgbaWAc-1631696029370)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\12.png)]

3.package.json中定义启动

  • 但是,每次执行都敲这么一长串有没有觉得不方便呢?

    • OK,我们可以在package.json的scripts中定义自己的执行脚本。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7ffYFeU2-1631696029371)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\13.png)]

  • package.json中的scripts的脚本在执行时,会按照一定的顺序寻找命令对应的位置。

    • 首先,会寻找本地的node_modules/.bin路径中对应的命令。

    • 如果没有找到,会去全局的环境变量中寻找。

    • 如何执行我们的build指令呢?

      npm run build
      

05-css-loader的使用

1.什么是loader?

  • loader是webpack中一个非常核心的概念。
  • webpack用来做什么呢?
    • 在我们之前的实例中,我们主要是用webpack来处理我们写的js代码,并且webpack会自动处理js之间相关的依赖。
    • 但是,在开发中我们不仅仅有基本的js代码处理,我们也需要加载css、图片,也包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等。
    • 对于webpack本身的能力来说,对于这些转化是不支持的。
    • 那怎么办呢?给webpack扩展对应的loader就可以啦。
  • loader使用过程:
    • 步骤一:通过npm安装需要使用的loader
    • 步骤二:在webpack.config.js中的modules关键字下进行配置
  • 大部分loader我们都可以在webpack的官网中找到,并且学习对应的用法

2.css文件处理 - 准备工作

  • 项目开发过程中,我们必然需要添加很多的样式,而样式我们往往写到一个单独的文件中。

    • 在src目录中,创建一个css文件,其中创建一个normal.css文件。

    • 我们也可以重新组织文件的目录结构,将零散的js文件放在一个js文件夹中。

      [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNxJ283U-1631696029372)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\14.png)]

  • normal.css中的代码非常简单,就是将body设置为red

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rrsFCMS1-1631696029373)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\15.png)]

  • 但是,这个时候normal.css中的样式会生效吗?

    • 当然不会,因为我们压根就没有引用它。
    • webpack也不可能找到它,因为我们只有一个入口,webpack会从入口开始查找其他依赖的文件
  • 在入口文件中引用:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gd0swOaR-1631696029374)(D:\ProjectsQin\otherQin\cloudimages\vue\coderwhy\5\16.png)]

3.css文件处理-打包错误信息

六、Vue CLI详解

01-Vue Cli

1 Vue CLI是什么

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

2 Vue CLI依赖环境-Node

  • 安装NodeJS
    • 可以直接在官方网站中下载安装.
    • 网址: http://nodejs.cn/download/
  • 检测安装的版本
    • 默认情况下自动安装Node和NPM
    • Node环境要求8.9以上或者更高版本

  • 什么是NPM呢?

    • shijiNPM的全称是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 [name]
    

3.Vue CLI使用前提 - Webpack

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

4.Vue CLI的使用

  • 安装Vue脚手架

    • npm install -g @vue/cli
      
  • 注意:上面安装的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目时不可以的。

  • Vue CLI2初始化项目

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

02-Vue CLI2

1.Vue CLI2详解–Vue CLI初始化项目过程

2.Vue CLI2目录结构详解

3.Runtime-Compiler和Runtime-only的区别

  • 简单总结
    • 如果在之后的开发中,你依然使用template,就需要选择Runtime-Compiler
    • 如果你之后的开发中,使用的是.vue文件夹开发,那么可以选择Runtime-only

4.render和template

  • Runtime-CompilerRuntime-only

image-20210730222447141

  • 为什么存在这样的差异呢?
    • 我们需要先理解Vue应用程序是如何运行起来的。
    • Vue中的模板如何最终渲染成真实DOM。
    • 我们来看下面的一幅图。

5.Vue程序运行过程

  • 总结
  • Runtime-Compiler:
    • template -> ast -> render -> vdom -> 真实DOM
  • Runtime-only:(1.性能更高 2.下面的代码量更少)
  • render -> vdom -> UI

6.render函数的使用

image-20210730222810300

main.js代码:

import Vue from 'vue'
import App from './App'
 
Vue.config.productionTip = false // 消息提示的环境配置,设置为开发环境或者生产环境
 
/* eslint-disable no-new */
/* 
// runtime-compiler
new Vue({
  el: '#app',
  components: { App },
  template: '<App/>',
})
*/
 
 
// const cpn = { // 组件
//   template: '<div>{{message}}</div>',
//   data() {
//     return {
//       message: '我是组件message'
//     }
//   }
// }
 
 
 
// 也可以用下面这个方案 runtime-only
new Vue({
  el: '#app',
  render: function (createElement) { // createElement是一个函数
    // 1.使用方式一: createElement('标签', {标签的属性}, ['内容'])
    // 1.1 基本使用
 
    // return createElement('h2', {
    //   class: 'box'
    // },
    // ['Hello World'])
 
    // 1.2 嵌套render函数
    // return createElement('h2', {
    //     class: 'box'
    //   },
    //   ['Hello World', createElement('button', ['按钮'])])
 
    // 2.传入组件对象:
    // return createElement(cpn)
    return createElement(App)
  }
})
 
 
// runtime-compiler(v1)
// template -> ast -> render -> vdom -> UI
 
// runtime-only(v2)(1.性能更高 2.下面的代码量更少)
// render -> vdom -> UI
  • 那么.vue文件中的template是由谁处理的了?
    • 是由vue-template-compiler

7.npm run build

8.npm run dev

9.修改配置:webpack.base.conf.js起别名

03-Vue CLI3的使用

1.认识Vue CLI3

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

2.Vue CLI3

3.目录结构详解

4.配置去哪里了?

  • UI方面的配置
    • 启动配置服务器:vue ui

  • 一大堆配置文件去哪里了?

5.自定义配置:起别名

04-箭头函数

1.箭头函数的基本使用

  • 箭头函数: 也是一种定义函数的方式
  • 定义函数的方式
function
  const aaa = function () {
 
  }
 
  // 2.对象字面量中定义函数
  const obj = {
    bbb() {
 
    }
  }
 
  // 3.ES6中的箭头函数
  // const ccc = (参数列表) => {
  //
  // }
  const ccc = () => {
 
  }

2.箭头函数参数和返回值

 
  // 1.参数问题:
  // 1.1.放入两个参数
  const sum = (num1, num2) => {
    return num1 + num2
  }
 
  // 1.2.放入一个参数
  const power = num => {
    return num * num
  }
 
  // 2.函数中
  // 2.1.函数代码块中有多行代码时
  const test = () => {
    // 1.打印Hello World
    console.log('Hello World');
 
    // 2.打印Hello Vuejs
    console.log('Hello Vuejs');
  }
 
  // 2.2.函数代码块中只有一行代码
  // const mul = (num1, num2) => {
  //   return num1 + num2
  // }
  const mul = (num1, num2) => num1 * num2
  console.log(mul(20, 30));
 
  // const demo = () => {
  //   console.log('Hello Demo');
  // }
  const demo = () => console.log('Hello Demo')
  console.log(demo());
 

3.箭头函数中this的使用

  • 什么时候使用箭头函数?
    • 当把一个函数作为参数传到另外一个函数
setTimeout(function () {
    console.log(this);
  }, 1000)
  
  setTimeout(() => {
    console.log(this);
  }, 1000)
  • 问题: 箭头函数中的this是如何查找的了?
  • 答案: 向外层作用域中, 一层层查找this, 直到有this的定义.
  const obj = {
      aaa() {
        setTimeout(function () {
          console.log(this); // window
        })
 
        setTimeout(() => {
          console.log(this); // obj对象
        })
      }
    }
 
    obj.aaa()
 
    const obj = {
      aaa() {
        setTimeout(function () {
          setTimeout(function () {
            console.log(this); // window
          })
 
          setTimeout(() => {
            console.log(this); // window
          })
        })
 
        setTimeout(() => {
          setTimeout(function () {
            console.log(this); // window
          })
 
          setTimeout(() => {
            console.log(this); // obj
          })
        })
      }
    }
 
    obj.aaa()

七、Vue-router

内容概述

  • 认识路由
  • vue-router基本使用
  • vue-router嵌套路由
  • vue-router参数传递
  • vue-router导航守卫
  • keep-alive

01-认识路由

1.什么是路由?

  • 说起路由你想起了什么?

    • 路由是一个网络工程里面的术语。
    • 路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动. — 维基百科
  • 额, 啥玩意? 没听懂

    • 在生活中, 我们有没有听说过路由的概念呢? 当然了, 路由器嘛.
    • 路由器是做什么的? 你有想过吗?
    • 路由器提供了两种机制: 路由和转送.
      • 路由是决定数据包从来源到目的地的路径.
      • 转送将输入端的数据转移到合适的输出端.
    • 路由中有一个非常重要的概念叫路由表.
      • 路由表本质上就是一个映射表, 决定了数据包的指向.

2.后端路由阶段

  • 早期的网站开发整个HTML页面是由服务器来渲染的.

    • 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
  • 但是, 一个网站, 这么多页面服务器如何处理呢?

    • 一个页面有自己对应的网址, 也就是URL.
    • URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理.
    • Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
    • 这就完成了一个IO操作.
  • 上面的这种操作, 就是后端路由.

    • 当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户顿.
    • 这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
  • 后端路由的缺点:

    • 一种情况是整个页面的模块由后端人员来编写和维护的.
    • 另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码.
    • 而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情

3.前端路由阶段

  • 前后端分离阶段:

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

    • 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
    • 也就是前端来维护一套路由规则.
  • 前端路由的核心是什么呢?

    • 改变URL,但是页面不进行整体的刷新。
    • 如何实现呢?

02-前端路由的规则

1.URL的hash

  • URL的hash
    • URL的hash也就是锚点(#), 本质上是改变window.location的href属性.
    • 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新

2.HTML5的history模式:pushState

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

3.HTML5的history模式:replaceState

  • history.replaceState()

4.HTML5的history模式:go

  • history.go()

5.补充说明

  • 上面只演示了三个方法
  • 因为 history.back() 等价于 history.go(-1)
  • history.forward() 则等价于 history.go(1)
  • 这三个接口等同于浏览器界面的前进后退。

03-vue-router基础

1.认识vue-router

  • 目前前端流行的三大框架, 都有自己的路由实现:

    • Angular的ngRouter
    • React的ReactRouter
    • Vue的vue-router
  • 当然, 我们的重点是vue-router

    • vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。
    • 我们可以访问其官方网站对其进行学习: https://router.vuejs.org/zh/
  • vue-router是基于路由和组件的

    • 路由用于设定访问路径, 将路径和组件映射起来.
    • 在vue-router的单页面应用中, 页面的路径的改变就是组件的切换

2.安装和使用vue-router

  • 因为我们已经学习了webpack, 后续开发中我们主要是通过工程化的方式进行开发的.

    • 所以在后续, 我们直接使用npm来安装路由即可.
    • 步骤一: 安装vue-router
    npm install vue-router --save
    
    • 步骤二: 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)

      • 第一步:导入路由对象,并且调用 Vue.use(VueRouter)
      import Vue from 'vue' 
      import VueRouter from 'vue-router' 
       
      Vue.use(VueRouter)
      
      • 第二步:创建路由实例,并且传入路由映射配置

      • 第三步:在Vue实例挂载创建的路由实例

  • 使用vue-router的步骤:

  • 第一步: 创建路由组件

  • 第二步: 配置路由映射: 组件和路径映射关系

  • 第三步: 使用路由: 通过和

  • : 该标签是一个vue-router中已经内置的组件, 它会被渲染成一个标签.

  • : 该标签会根据当前的路径, 动态渲染出不同的组件.

    • 网页的其他内容, 比如顶部的标题/导航, 或者底部的一些版权信息等会和处于同一个等级.
    • 在路由切换时, 切换的是挂载的组件, 其他内容不会发生改变.
  • 最终效果如下

04-细节处理

1.路由的默认路径

  • 我们这里还有一个不太好的实现:

    • 默认情况下, 进入网站的首页, 我们希望渲染首页的内容.
    • 但是我们的实现中, 默认没有显示首页组件, 必须让用户点击才可以.
  • 如何可以让路径默认跳到到首页, 并且渲染首页组件呢?

    • 非常简单, 我们只需要配置多配置一个映射就可以了.

  • 配置解析:

    • 我们在routes中又配置了一个映射.
    • path配置的是根路径: /
    • redirect是重定向, 也就是我们将根路径重定向到/home的路径下, 这样就可以得到我们想要的结果了

2.HTML5的History模式

  • 我们前面说过改变路径的方式有两种:
    • URL的hash
    • HTML5的history
    • 默认情况下, 路径的改变使用的URL的hash.
  • 如果希望使用HTML5的history模式, 非常简单, 进行如下配置即可:

  • 补充:history模式的url不会有#符号

3.router-link补充

  • 在前面的中, 我们只是使用了一个属性: to, 用于指定跳转的路径.

  • 还有一些其他属性:

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

4.修改linkActiveClass

  • 该class具体的名称也可以通过router实例的属性进行修改

  • exact-active-class 类似于active-class,
    • 只是在精准匹配下才会出现的class.
    • 后面看到嵌套路由时, 我们再看下这个属性.

5.路由代码跳转

  • 有时候, 页面的跳转可能需要执行对应的JavaScript代码,
  • 这个时候, 就可以使用第二种跳转方式了 比如, 我们将代码修改如下:

阶段代码
  • router里的index.js
// 配置路由相关的信息
// 导入路由对象
import VueRouter from 'vue-router'
import Vue from 'vue'
 
import Home from '../components/Home'
import About from '../components/About'
 
// 1.通过Vue.use(插件), 安装插件
Vue.use(VueRouter)
 
// 2.创建VueRouter对象
const routes = [
  {
    path: '',
    // redirect重定向
    redirect: '/home'
  },
  {
    path: '/home',
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
// 创建路由实例,并且传入路由映射配置
const router = new VueRouter({
  // 配置路由和组件之间的应用关系
  routes,
  mode: 'history',
  linkActiveClass: 'active'
})
// 3.将router对象传入到Vue实例
export default router

App.vue

<template>
  <div id="app">
    <h2>我是APP组件</h2>
    <!-- 使用路由 -->
    <!--<router-link to="/home" tag="button" replace active-class="active">首页</router-link>-->
    <!--<router-link to="/about" tag="button" replace active-class="active">关于</router-link>-->
    
    <router-link to="/home" tag="button" replace>首页</router-link>
    <router-link to="/about" tag="button" replace>关于</router-link>
    <!-- // 通过代码的方式修改路由 vue-router -->
    <!-- <button @click="homeClick">首页</button>
    <button @click="aboutClick">关于</button> -->
    <router-view></router-view>
  </div>
</template>
 
<script>
export default {
  name: 'App',
  methods: {
    homeClick() {
      // 通过代码的方式修改路由 vue-router
      // push => pushState
      // this.$router.push('/home')
      this.$router.replace('/home')
      console.log('homeClick');
    },
    aboutClick() {
      // this.$router.push('/about')
      this.$router.replace('/about')
      console.log('aboutClick');
    }
  }
}
</script>
 
<style>
  /*.router-link-active {*/
    /*color: #f00;*/
  /*}*/
 
  .active {
    color: #f00;
  }
</style>

main.js

import Vue from 'vue'
import App from './App'
import router from './router'
 
Vue.config.productionTip = false
 
new Vue({
  el: '#app',
  // 在Vue实例中挂载创建的路由实例
  router,
  render: h => h(App)
})

6.动态路由

  • 在某些情况下,一个页面的path路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径:
    • /user/aaaa或/user/bbbb
    • 除了有前面的/user之外,后面还跟上了用户的ID
    • 这种path和Component的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)。

image-20210731151650135

代码:

  • 创建一个User.vue组件
<template>
  <div>
    <h2>我是用户界面</h2>
    <p>我是用户的相关信息, 嘿嘿嘿</p>
    <h2>计算属性userId:{{userId}}</h2>
    <h2>$route.params.id:{{$route.params.id}}</h2>
    <button @click="btnClick">按钮</button>
  </div>
</template>
 
<script>
  export default {
    name: "User",
    computed: {
      userId() {
        return this.$route.params.id
      }
    },
    created() {
      console.log('User created');
    },
    destroyed() {
      console.log('User destroyed');
    },
    methods: {
      btnClick() {
        // 所有的组件都继承自Vue类的原型
        console.log(this.$router); // 是我们index.js创建的那个大的路由对象router
        console.log(this.$route); // 当前哪个路由处于活跃状态,获取到的就是哪个路由
 
        console.log(this.name);
      }
    }
  }
</script>
 
<style scoped>
 
</style>
  • router文件夹下index.js
...
const routes = [
    ...
    {
        path:'/user/:id',
        component:User
    }
]
const router = new VueRouter({
    routes,
    mode:'history',
    linkActiveClass:'active'
})
export default router
  • App.vue
<template>
  <div id="app">
   
    <h2>我是APP组件</h2>
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
 
    <!-- 动态路由 -->
    <!--<router-link :to="/user/yyy">用户</router-link>-->
    <!-- 动态拼接 使用v-bind -->
     <router-link :to="'/user/'+userId">用户</router-link>
  </div>
</template>
 
<script>
export default {
  name: "App",
  components: {},
  data() {
    return {
      userId: "zhangsan"
    };
  },
};
</script>

05-路由懒加载

1.认识路由的懒加载

  • 官方给出了解释:

    • 当打包构建应用时,Javascript 包会变得非常大,影响页面加载。
    • 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
  • 官方在说什么呢?

    • 首先, 我们知道路由中通常会定义很多不同的页面.
    • 这个页面最后被打包在哪里呢? 一般情况下, 是放在一个js文件中.
    • 但是, 页面这么多放在一个js文件中, 必然会造成这个页面非常的大.
    • 如果我们一次性从服务器请求下来这个页面, 可能需要花费一定的时间, 甚至用户的电脑上还出现了短暂空白的情况.
    • 如何避免这种情况呢? 使用路由懒加载就可以了

2.路由懒加载的效果

image-20210731152605409

懒加载打包

image-20210731152713703

3.懒加载的方式

  • 方式一: 结合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')

06-路由嵌套

1.认识嵌套路由

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

  • 实现嵌套路由有两个步骤:
    • 创建对应的子组件, 并且在路由映射中配置对应的子路由.
    • 在组件内部使用标签.

2.嵌套路由实现

  • 定义两个组件:

3.嵌套默认路径

  • 嵌套路由也可以配置默认的路径, 配置方式如下:

07-传递参数

1.准备工作

  • 为了演示传递参数, 我们这里再创建一个组件, 并且将其配置好

    • 第一步: 创建新的组件Profile.vue

    • 第二步: 配置路由映射

    • 第三步: 添加跳转的

2.传递参数的方式

URL:协议://主机:端口/路径?查询

  • 传递参数主要有两种类型:

  • params和query

  • params的类型:

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

    • 配置路由格式: /router, 也就是普通配置
    • 传递的方式: 对象中使用query的key作为传递方式
    • 传递后形成的路径: /router?id=123, /router?id=abc
  • 如何使用它们呢? 也有两种方式: 的方式和JavaScript代码方式

3.传递参数方式一: <router-link>

4.传递参数方式二: JavaScript代码

5.获取参数

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

6. r o u t e 和 route和 routerouter是有区别的

  • r o u t e 和 route和 routerouter是有区别的
    • r o u t e r 为 V u e R o u t e r 实 例 , 想 要 导 航 到 不 同 U R L , 则 使 用 router为VueRouter实例,想要导航到不同URL,则使用 routerVueRouterURL使router.push方法
    • $route为当前router跳转对象里面可以获取name、path、query、params等

7.代码

  • profile.vue
<template>
  <div>
    <h2>我是Profile组件</h2>
    <h2>{{$route.query.name}}</h2>
    <h2>{{$route.query.age}}</h2>
    <h2>{{$route.query.height}}</h2>
  </div>
</template>
 
<script>
  export default {
    name: "Profile",
    created() {
      console.log('Profile created');
    },
    destroyed() {
      console.log('Profile destroyed');
    }
  }
</script>
 
<style scoped>
 
</style>
  • App.vue
<template>
  <div id="app">
    <!-- 导航守卫 keep-alive -->
    <h2>我是APP组件</h2>
 
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
 
    <!-- 动态路由 -->
    <!--<router-link :to="/user/yyy">用户</router-link>-->
    <!-- 动态拼接 使用v-bind -->
    <!--<router-link :to="'/user/'+userId">用户</router-link>-->
    <!--&lt;!&ndash;<router-link to="/profile">档案</router-link>&ndash;&gt;-->
 
    <!-- 传递参数类型:1.params(通过path) 2.query -->
    <!-- 参数传递方式:1.router-link的to -->
    <router-link :to="{path: '/profile', query: {name: 'why', age: 18, height: 1.88}}">
    档案</router-link>
    <!-- 参数传递方式 2.js:this.$router.push -->
<!-- 
    <button @click="userClick">用户</button>
    <button @click="profileClick">档案</button> -->
 
  </div>
</template>
 
<script>
export default {
  name: "App",
  components: { Home, HomeNews },
  data() {
    return {
      userId: "zhangsan"
    };
  },
  methods: {
    homeClick() {
      // 通过代码的方式修改路由 vue-router
      // push => pushState
      // this.$router.push('/home')
      this.$router.replace("/home");
      console.log("homeClick");
    },
    aboutClick() {
      // this.$router.push('/about')
      this.$router.replace("/about");
      console.log("aboutClick");
    },
    userClick() {
      this.$router.push("/user/" + this.userId);
    },
    profileClick() {
      this.$router.push({
        path: "/profile",
        query: {
          name: "kobe",
          age: 19,
          height: 1.87,
        },
      });
    },
  },
};
</script>
 
<style>
/*.router-link-active {*/
/*color: #f00;*/
/*}*/
 
.active {
  color: #f00;
}
</style>

08-导航守卫

"导航”表示路由正在发生改变。

正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。有多种机会植入路由导航过程中:全局的, 单个路由独享的, 或者组件级的。

1.为什么使用导航守卫?

  • 我们来考虑一个需求: 在一个SPA应用中, 如何改变网页的标题呢?

    • 网页标题是通过来显示的, 但是SPA只有一个固定的HTML, 切换不同的页面时, 标题并不会改变.
    • 但是我们可以通过JavaScript来修改的内容.window.document.title = ‘新的标题’.
    • 那么在Vue项目中, 在哪里修改? 什么时候修改比较合适呢?
  • 普通的修改方式:

    • 我们比较容易想到的修改标题的位置是每一个路由对应的组件.vue文件中.
    • 通过mounted声明周期函数, 执行对应的代码进行修改即可.
    // Home.vue
     mounted() {
          console.log('home mounted');
          document.title='首页'
      },
     
    // About.vue
     mounted() {
          console.log('home mounted');
          document.title='关于'
     },
     
    // ...
     
    // 其他页面也是这样写
    
    • 但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码).
  • 有没有更好的办法呢? 使用导航守卫即可.

  • 什么是导航守卫?

    • vue-router提供的导航守卫主要用来监听监听路由的进入和离开的.
  • vue-router提供了beforeEach和afterEach的钩子函数, 它们会在路由即将改变前和改变后触发.

2.导航守卫使用

  • 我们可以利用beforeEach来完成标题的修改.
    • 首先, 我们可以在钩子当中定义一些标题, 可以利用meta来定义
    • 其次, 利用导航守卫,修改我们的标题.

  • 导航钩子的三个参数解析:
    • to: 即将要进入的目标的路由对象.
    • from: 当前导航即将要离开的路由对象.
    • next: 调用该方法后, 才能进入下一个钩子.
  • router下的index.js
// ...
const routes = [
  {
    path: '',
    // redirect重定向
    redirect: '/home'
  },
  {
    path: '/home',
    component: Home,
    // meta元数据(描述数据的数据)
    meta: {
      title: '首页'
    },
    // 嵌套路由
    children: [
      // {
      //   path: '',
      //   redirect: 'news'
      // },
      {
        path: 'news',// 没有斜杠 / 
        component: HomeNews
      },
      {
        path: 'message',
        component: HomeMessage
      }
    ]
  },
  {
    path: '/about',
    component: About,
    meta: {
      title: '关于'
    },
  },
  {
    path: '/user/:id',
    component: User,
    meta: {
      title: '用户'
    },
  },
  {
    path: '/profile',
    component: Profile,
    meta: {
      title: '档案'
    },
  }
]
const router = new VueRouter({
       // ...
})
 
// 1. 全局导航守卫
// 1.1 前置守卫(guard) 路由跳转之前
// beforeEach()注册一个全局前置守卫,本身是一个函数,又传入一个函数guard,有三个参数
router.beforeEach((to, from, next) => {
  // 从from跳转到to
  // // to 和 from都是route对象
  // document.title = to.meta.title 
  // 上面这样写的话如果有嵌套路由的话是undefined,要使用matched(匹配)获取
  document.title = to.matched[0].meta.title
  console.log(to);// 到哪个页面去?
  console.log(from);// 从哪个页面来?
 
  // 调用该方法后,才能进入下一个钩子
  // 如果是后置钩子,也就是afterEach,不需要主动调用next()函数
  // 这里其实可以判断用户登陆权限之类的,拦截访问 ,权限不符调用next(false)拦截
  next()
})
 
 
// 1.2 后置钩子(hook) 不需要主动调用next()函数
router.afterEach((to, from) => {
  console.log('----');
})
// 钩子->回调
export default router
 

3.导航守卫补充

  • 补充一:如果是后置钩子, 也就是afterEach, 不需要主动调用next()函数.
  • 补充二: 上面我们使用的导航守卫, 被称之为全局守卫(beforeEach、afterEach).
    • 路由独享的守卫(beforeEnter).
    • 组件内的守卫(beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave).
  • 更多内容, 可以查看官网进行学习:
  • 路由独享的守卫(beforeEnter)
const routes = [
    // ...
      {
    path: '/about',
    component: About,
    meta: {
      title: '关于'
    },
    // 2.路由独享的守卫
    beforeEnter: (to, from, next) => {
      console.log('about beforeEnter');
      next()
    }
  },
    // ...
]
 
const router = new VueRouter({
//...
})
  • 组件内的守卫(beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave)
// Home.vue
 
<template>
  <div>
    <h2>我是首页</h2>
    <p>我是首页内容, 哈哈哈</p>
 
    <router-link to="/home/news">新闻</router-link>
    <router-link to="/home/message">消息</router-link>
 
    <router-view></router-view>
 
    <h2>{{message}}</h2>
  </div>
</template>
 
<script>
export default {
  name: "Home",
  data() {
    return {
      name: "yyy",
    };
  },
  beforeRouteEnter(to, from, next) {
    // 在渲染该组件的对应路由被 confirm 前调用
    // 不!能!获取组件实例 `this`
    // 因为当守卫执行前,组件实例还没被创建
    // console.log(to);
    // console.log(from);
    console.log(this); // undefined
    next((vm) => { // 通过传一个回调给 next来访问组件实例
      console.log(vm.name);
    });
  },
  beforeRouteUpdate(to, from, next) {
    // 在当前路由改变,但是该组件被复用时调用
    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
    // 可以访问组件实例 `this`
    console.log("beforeRouteUpdate");
    next();
  },
  beforeRouteLeave(to, from, next) {
    // 导航离开该组件的对应路由时调用
    // 可以访问组件实例 `this`
    console.log(this.$route.path);
    next();
  },
};
</script>
 
<style scoped>
 
</style>

09-keep-alive

keep-alive遇见vue-router

  • keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
    • 它们有两个非常重要的属性:
    • include - 字符串或正则表达,只有匹配的组件会被缓存
    • exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
  • router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存

  • 通过create声明周期函数来验证

App.vue

<template>
  <div id="app">
    <!-- 导航守卫 keep-alive -->
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
 
    <!-- 在vue中我们可以使用keepalive来进行组件缓存 -->
    <keep-alive exclude="Profile,User">
      <router-view />
    </keep-alive>
  </div>
</template>

Home.vue

<template>
  <div>
    <h2>我是首页</h2>
    <p>我是首页内容, 哈哈哈</p>
 
    <router-link to="/home/news">新闻</router-link>
    <router-link to="/home/message">消息</router-link>
 
    <router-view></router-view>
 
    <h2>{{ message }}</h2>
  </div>
</template>
 
<script>
export default {
  name: "Home",
  data() {
    return {
      message: "你好啊",
      path: "/home/news",
    };
  },
  created() {
    console.log("home created");
  },
  mounted() {
    console.log("home mounted");
    // document.title='首页'
  },
  destroyed() {
    console.log("home destroyed");
  },
  // 这两个函数, 只有该组件被保持了状态使用了keep-alive时, 才是有效的
  activated() {
    // 活跃状态
    this.$router.push(this.path);
    console.log("activated");
  },
  deactivated() {
    console.log("deactivated");
  },
  // 离开当前路由页面时调用
  beforeRouteLeave(to, from, next) {
    // 首页中使用path属性记录离开时的路径,在beforeRouteLeave中记录
    console.log(this.$route.path);
    this.path = this.$route.path;
    next();
  },
};
</script>
 
<style scoped>
</style>

Profile.vue

 
<script>
  export default {
    name: "Profile",
    created() {
      console.log('Profile created');
    },
    destroyed() {
      console.log('Profile destroyed');
    }
  }
</script>

User.vue

<script>
  export default {
    name: "User",
    created() {
      console.log('User created');
    },
    destroyed() {
      console.log('User destroyed');
    },
  }
</script>

10-TabBar练习

1.TabBar实现思路

  • 1.如果在下方有一个单独的TabBar组件,你如何封装
    • 自定义TabBar组件,在APP中使用
    • 让TabBar出于底部,并且设置相关的样式
  • 2.TabBar中显示的内容由外界决定
    • 定义插槽
    • flex布局平分TabBar
  • 3.自定义TabBarItem,可以传入 图片和文字
    • 定义TabBarItem,并且定义两个插槽:图片、文字。
    • 给两个插槽外层包装div,用于设置样式。
    • 填充插槽,实现底部TabBar的效果
  • 4.传入 高亮图片
    • 定义另外一个插槽,插入active-icon的数据
    • 定义一个变量isActive,通过v-show来决定是否显示对应的icon
  • 5.TabBarItem绑定路由数据
    • 安装路由:npm install vue-router —save
    • 完成router/index.js的内容,以及创建对应的组件
    • main.js中注册router
    • APP中加入组件
  • 6.点击item跳转到对应路由,并且动态决定isActive
    • 监听item的点击,通过this.$router.replace()替换路由路径
    • 通过this.$route.path.indexOf(this.link) !== -1来判断是否是active
  • 7.动态计算active样式 封装新的计算属性:this.isActive ? {‘color’: ‘red’} : {}

2.代码实现

分析:

image-20210731163856393

3.步骤代码详解

(1)基本结构的搭建(无封装组件)
  • assets / css / base.css
body{
    margin: 0;
    padding: 0;
}
  • App.vue
<template>
  <div id="app">
    <div id="tab-bar">
      <div class="tab-bar-item">首页</div>
      <div class="tab-bar-item">分类</div>
      <div class="tab-bar-item">购物车</div>
      <div class="tab-bar-item">我的</div>
    </div>
  </div>
</template>
 
<script>
export default {
  name: "App",
  components: {},
};
</script>
 
<style>
@import "../src/assets/css/base.css";
#tab-bar {
  /* 本身的样式 */
  background-color: #f6f6f6;
  box-shadow: 0 -1px 1px rgba(100, 100, 100, 0.2);
 
  /* 利用flex进行布局 */
  display: flex;
  
  /* 定位相关 */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}
.tab-bar-item {
  flex: 1;
  text-align: center;
  height: 49px;
}
</style>
  • 效果图,基本结构搭建完成

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HW4bWefG-1631696029425)(C:\Users\atiff\Desktop\59.png)]

(2) TabBar和TabBarItem组件封装

复习知识点:(十一)插槽

  • App.vue
<template>
  <div id="app">
    <tab-bar>
      <!--TabBar的 slot 有几个就填几个item -->
      <tab-bar-item>
        <!-- TabBarItem的slot 传入不同图片 -->
        <img slot="item-icon" src="./assets/img/tabbar/home.svg" alt="">
        <div slot="item-text">首页</div>
      </tab-bar-item>
      <tab-bar-item>
        <img slot="item-icon" src="./assets/img/tabbar/category.svg" alt="">
        <div slot="item-text">分类</div>
      </tab-bar-item>
      <tab-bar-item>
        <img slot="item-icon" src="./assets/img/tabbar/shopcart.svg" alt="">
        <div slot="item-text">购物车</div>
      </tab-bar-item>
      <tab-bar-item>
        <img slot="item-icon" src="./assets/img/tabbar/profile.svg" alt="">
        <div slot="item-text">我的</div>
      </tab-bar-item>
    </tab-bar>
  </div>
</template>
 
<script>
  import TabBar from './components/tabbar/TabBar'
  import TabBarItem from './components/tabbar/TabBarItem'
 
  export default {
    name: 'App',
    components: {
      TabBar,
      TabBarItem
    }
  }
</script>
 
<style>
/* 在css里面引用需要用@import; */
  @import "./assets/css/base.css";
</style>
  • TabBar.vue
<template>
  <div id="tab-bar">
    <!-- 使用插槽,以便能往里面动态传入TabBarItem -->
    <slot></slot>
  </div>
</template>
 
<script>
export default {
  name: "TabBar",
};
</script>
 
<style scoped>
#tab-bar {
  /* 利用flex进行布局 */
  display: flex;
  /* 本身的样式 */
  background-color: #f6f6f6;
  box-shadow: 0 -1px 1px rgba(100, 100, 100, 0.2);
  /* 定位相关 */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
}
</style>
  • TabBarItem.vue
<template>
  <div class="tab-bar-item">
    <!--所有的item都展示同一个图片, 同一个文字,-->
    <!--<img src="../../assets/img/tabbar/home.svg" alt="">-->
    <!--<div>首页</div>-->
    
    <!-- 使用插槽动态传入图片文字 -->
    <slot name="item-icon"></slot>
    <slot name="item-text"></slot>
  </div>
</template>
 
<script>
export default {
  name: "TabBarItem",
};
</script>
 
<style scoped>
.tab-bar-item {
  flex: 1;
  text-align: center;
  height: 49px;
  font-size: 14px;
}
 
.tab-bar-item img {
  width: 24px;
  height: 24px;
  margin-top: 3px;
 
  /* 去掉图片与文字默认的间隔3px */
  vertical-align: middle;
 
  margin-bottom: 2px;
}
</style>
  • 效果图,目前基本样式已完成
(3)给TabBarItem传入active图片
  • App.vue
<template>
  <div id="app">
    <tab-bar>
      <tab-bar-item>
        <img slot="item-icon" src="../src/assets/img/tabbar/home.svg" alt="">
        <img slot="item-icon-active" src="../src/assets/img/tabbar/home_active.svg" alt="">
        <div slot="item-text">首页</div>
      </tab-bar-item>
      <tab-bar-item>
         <img slot="item-icon" src="../src/assets/img/tabbar/category.svg" alt="">
         <img slot="item-icon-active" src="../src/assets/img/tabbar/category_active.svg" alt="">
 
        <div  slot="item-text">分类</div>
      </tab-bar-item>
      <tab-bar-item>
         <img slot="item-icon" src="../src/assets/img/tabbar/shopcart.svg" alt="">
         <img slot="item-icon-active" src="../src/assets/img/tabbar/shopcart_active.svg" alt="">
        <div  slot="item-text">购物车</div>
      </tab-bar-item>
      <tab-bar-item>
         <img slot="item-icon" src="../src/assets/img/tabbar/profile.svg" alt="">
         <img slot="item-icon-active" src="../src/assets/img/tabbar/profile_active.svg" alt="">
        <div slot="item-text">我的</div>
      </tab-bar-item>
    </tab-bar>
  </div>
</template>
  • TabBarItem.vue
<template>
  <div class="tab-bar-item">
    <slot v-if="!isActive" name="item-icon"></slot>
    <!-- 图片active插槽 -->
    <slot v-else name="item-icon-active"></slot>
    <slot :class="{ active: isActive }" name="item-text"></slot>
</template>
 
<script>
export default {
  name: "TabBarItem",
  data() {
    return {
      isActive: true,
    };
  },
};
</script>
 
<style>
/* ... */
.active {
  color: red;
}
</style>
  • 但是上面的TabBarItem.vue这样写会有问题,文字样式不起效果

  • 把TabBarItem.vue的HTML改成下面
<template>
  <div class="tab-bar-item">
    <div v-if="!isActive">
      <slot name="item-icon"></slot>
    </div>
    <!-- 图片active插槽 -->
    <div v-else>
      <slot name="item-icon-active"></slot>
    </div>
    <!-- 这样写样式不起效果  -->
    <!-- <slot :class="{ active: isActive }" name="item-text"></slot> -->
    <!-- 渲染的时候会直接把它这个替换成  <div slot="item-text">首页</div> -->
    <!-- 一般不会直接在插槽上绑定动态属性,因为渲染时会把它替换掉
     外面套一个div这样不会把原来的替换掉,上面两个最好也这样写  -->
    <div :class="{ active: isActive }">
      <slot name="item-text"></slot>
    </div>
  </div>
</template>
  • 效果图

七、Promise的使用

01-认识Promise

1.什么是Promise呢?

  • ES6中一个非常重要和好用的特性就是Promise
    • 但是初次接触Promise会一脸懵逼,这是什么东西?
    • 看看官方或者一些文章对它的介绍和用法,也是一头雾水。
  • Promise到底是做什么的呢?
    • Promise是异步编程的一种解决方案
  • 那什么时候我们会来处理异步事件呢?
    • 一种很常见的场景应该就是网络请求了。
    • 我们封装一个网络请求的函数,因为不能立即拿到结果,所以不能像简单的3+4=7一样将结果返回。
    • 所以往往我们会传入另外一个函数,在数据请求成功时,将数据通过传入的函数回调出去。
    • 如果只是一个简单的网络请求,那么这种方案不会给我们带来很大的麻烦。
  • 但是,当网络请求非常复杂时,就会出现回调地狱。
    • OK,我以一个非常夸张的案例来说明。

2.网络请求的回调地狱

  • 我们来考虑下面的场景(有夸张的成分):
    • 我们需要通过一个url1从服务器加载一个数据data1,data1中包含了下一个请求的url2
    • 我们需要通过data1取出url2,从服务器加载数据data2,data2中包含了下一个请求的url3
    • 我们需要通过data2取出url3,从服务器加载数据data3,data3中包含了下一个请求的url4
    • 发送网络请求url4,获取最终的数据data4
  • 上面的代码有什么问题吗?
    • 正常情况下,不会有什么问题,可以正常运行并且获取我们想要的结果。
    • 但是,这样额代码难看而且不容易维护。
    • 我们更加期望的是一种更加优雅的方式来进行这种异步操作。
  • 如何做呢?就是使用Promise。
    • Promise可以以一种非常优雅的方式来解决这个问题。

02-Promise基本使用

1.定时器的异步事件

  • 我们先来看看Promise最基本的语法。

  • 这里,我们用一个定时器来模拟异步事件:

    • 假设下面的data是从网络上1秒后请求的数据
    • console.log就是我们的处理方式。
  • 这是我们过去的处理方式,我们将它换成Promise代码

  • 这个例子会让我们感觉脱裤放屁,多此一举
    • 首先,下面的Promise代码明显比上面的代码看起来还要复杂。
    • 其次,下面的Promise代码中包含的resolve、reject、then、catch都是些什么东西?
  • 我们先不管第一个复杂度的问题,因为这样的一个屁大点的程序根本看不出来Promise真正的作用。

2.定时器异步事件解析

  • 我们先来认认真真的读一读这个程序到底做了什么?
    • new Promise很明显是创建一个Promise对象
    • 小括号中((resolve, reject) => {})也很明显就是一个函数,而且我们这里用的是之前刚刚学习过的箭头函数。
      • 但是resolve, reject它们是什么呢?
      • 我们先知道一个事实:在创建Promise时,传入的这个箭头函数是固定的(一般我们都会这样写)
      • resolve和reject它们两个也是函数,通常情况下,我们会根据请求数据的成功和失败来决定调用哪一个。
    • 成功还是失败?
      • 如果是成功的,那么通常我们会调用resolve(messsage),这个时候,我们后续的then会被回调。
      • 如果是失败的,那么通常我们会调用reject(error),这个时候,我们后续的catch会被回调。
  • OK,这就是Promise最基本的使用了。

3.Promise三种状态

  • 首先, 当我们开发中有异步操作时, 就可以给异步操作包装一个Promise
    • 异步操作之后会有三种状态
  • 我们一起来看一下这三种状态:
    • pending:等待状态,比如正在进行网络请求,或者定时器没有到时间。
    • fulfill:满足状态,当我们主动回调了resolve时,就处于该状态,并且会回调.then()
    • reject:拒绝状态,当我们主动回调了reject时,就处于该状态,并且会回调.catch()

03-Promise链式调用

1.Promise链式调用

  • 我们在看Promise的流程图时,发现无论是then还是catch都可以返回一个Promise对象。
  • 所以,我们的代码其实是可以进行链式调用的:
  • 这里我们直接通过Promise包装了一下新的数据,将Promise对象返回了
    • Promise.resovle():将数据包装成Promise对象,并且在内部回调resolve()函数
    • Promise.reject():将数据包装成Promise对象,并且在内部回调reject()函数

2.链式调用简写

  • 简化版代码:
    • 如果我们希望数据直接包装成Promise.resolve,那么在then中可以直接返回数据
    • 注意下面的代码中,我讲return Promise.resovle(data)改成了return data
    • 结果依然是一样的

04-Promise的all方法

代码见,05-Promise的all方法使用.html

八、Vuex详解

01-认识Vuex

1.Vuex是做什么的

  • 官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
    • 它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
    • Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
  • 状态管理到底是什么?
    • 状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。
    • 其实,你可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。
    • 然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。
    • 那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?
  • 等等,如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?
    • 当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?没错,就是响应式。
    • 如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。
    • 不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。
管理什么状态呢?
  • 但是,有什么状态时需要我们在多个组件间共享的呢?
    • 如果你做过大型开放,你一定遇到过多个状态,在多个界面间的共享问题。
    • 比如用户的登录状态、用户名称、头像、地理位置信息等等。
    • 比如商品的收藏、购物车中的物品等等。
    • 这些状态信息,我们都可以放在统一的地方,对它进行保存和管理,而且它们还是响应式的(待会儿我们就可以看到代码了,莫着急)。
  • OK,从理论上理解了状态管理之后,让我们从实际的代码再来看看状态管理。
    毕竟,Talk is cheap, Show me the code.(来自Linus)
  • 我们先来看看但界面的状态管理吧.

2.单页面状态管理

  • 我们知道,要在单个组件中进行状态管理是一件非常简单的事情

    • 什么意思呢?我们来看下面的图片。

  • 这图片中的三种东西,怎么理解呢?

    • State:不用多说,就是我们的状态。(你姑且可以当做就是data中的属性)
    • View:视图层,可以针对State的变化,显示不同的信息。(这个好理解吧?)
    • Actions:这里的Actions主要是用户的各种操作:点击、输入等等,会导致状态的改变。
  • 写点代码,加深理解:

    • 看下右边的代码效果, 肯定会实现吧?

单界面状态管理的实现

  • 在这个案例中,我们有木有状态需要管理呢?没错,就是个数counter。
  • counter需要某种方式被记录下来,也就是我们的State。
  • counter目前的值需要被显示在界面中,也就是我们的View部分。
  • 界面发生某些操作时(我们这里是用户的点击,也可以是用户的input),需要去更新状态,也就是我们的Actions
  • 这不就是上面的流程图了吗?

3.多页面状态管理

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

4.Vuex状态管理图例

  • 一起在来看一副官方给出的图片

02-Vuex基本使用

1.Vuex的安装

2.Vuex的代码组织

3.count案例的实现

1.案例分析
  • 我们还是实现一下之前简单的案例
  • 首先,我们需要在某个地方存放我们的Vuex代码:
    • 这里,我们先创建一个文件夹store,并且在其中创建一个index.js文件
    • 在index.js文件中写入如下代码:
2.挂载到Vue实例中
  • 其次,我们让所有的Vue组件都可以使用这个store对象
    • 来到main.js文件,导入store对象,并且放在new Vue中
    • 这样,在其他Vue组件中,我们就可以通过this.$store的方式,获取到这个store对象了

3.使用Vuex的count
  • 好的,这就是使用Vuex最简单的方式了。
  • 我们来对使用步骤,做一个简单的小节:
    • 1.提取出一个公共的store对象,用于保存在多个组件中共享的状态
    • 2.将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
    • 3.在其他组件中使用store对象中保存的状态即可
      • 通过this.$store.state.属性的方式来访问状态
      • 通过this.$store.commit(‘mutation中方法’)来修改状态
  • 注意事项:
    • 我们通过提交mutation的方式,而非直接改变store.state.count。
    • 这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变store.state.count的值。

03-Vuex核心概念

  • Vuex有几个比较核心的概念:

    • State

    • Getters

    • Mutation

    • Action

    • Module

  • 我们对它进行一一介绍.

1.state

  • Vuex提出使用单一状态树, 什么是单一状态树呢?
    • 英文名称是Single Source of Truth,也可以翻译成单一数据源。
  • 但是,它是什么呢?我们来看一个生活中的例子。
    • OK,我用一个生活中的例子做一个简单的类比。
    • 我们知道,在国内我们有很多的信息需要被记录,比如上学时的个人档案,工作后的社保记录,公积金记录,结婚后的婚姻信息,以及其他相关的户口、医疗、文凭、房产记录等等(还有很多信息)。
    • 这些信息被分散在很多地方进行管理,有一天你需要办某个业务时(比如入户某个城市),你会发现你需要到各个对应的工作地点去打印、盖章各种资料信息,最后到一个地方提交证明你的信息无误。
    • 这种保存信息的方案,不仅仅低效,而且不方便管理,以及日后的维护也是一个庞大的工作(需要大量的各个部门的人力来维护,当然国家目前已经在完善我们的这个系统了)。
  • 这个和我们在应用开发中比较类似:
    • 如果你的状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难。
    • 所以Vuex也使用了单一状态树来管理应用层级的全部状态。
    • 单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。

2.getter

Getters基本使用
  • 有时候,我们需要从store中获取一些state变异后的状态,比如下面的Store中:

    • 获取学生年龄大于20的个数。

  • 我们可以在Store中定义getters

Getters作为参数和传递参数
  • 如果我们已经有了一个获取所有年龄大于20岁学生列表的getters, 那么代码可以这样来写

  • getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数.
    • 比如上面的案例中,我们希望根据ID获取用户的信息

3.Mutation

Mutation状态更新
  • Vuex的store状态的更新唯一方式:提交Mutation
  • Mutation主要包括两部分:
    • 字符串的事件类型(type)
    • 一个回调函数(handler),该回调函数的第一个参数就是state。
  • mutation的定义方式:

  • 通过mutation更新

Mutation传递参数
  • 在通过mutation更新数据的时候, 有可能我们希望携带一些额外的参数
    • 参数被称为是mutation的载荷(Payload)
  • Mutation中的代码:

image-20210801211828993

  • 但是如果参数不是一个呢?
    • 比如我们有很多参数需要传递.
    • 这个时候, 我们通常会以对象的形式传递, 也就是payload是一个对象.
    • 这个时候可以再从对象中取出相关的信息.

image-20210801211939021

Mutation提交风格
  • 上面的通过commit进行提交是一种普通的方式
  • Vue还提供了另外一种风格, 它是一个包含type属性的对象

  • Mutation中的处理方式是将整个commit的对象作为payload使用, 所以代码没有改变, 依然如下:

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

  • 这就要求我们必须遵守一些Vuex对应的规则:

    • 提前在store中初始化好所需的属性.
    • 当给state中的对象添加新属性时, 使用下面的方式:
      • 方式一: 使用Vue.set(obj, ‘newProp’, 123),删除属性使用Vue.delete(obj,‘oldProp’)
      • 方式二: 用心对象给旧对象重新赋值
  • 我们来看一个例子:

    • 当我们点击更新信息时, 界面并没有发生对应改变.

  • 如何才能让它改变呢?

    • 查看下面代码的方式一和方式二

    • 都可以让state中的属性是响应式的.
Mutation常量类型 – 概念
  • 我们来考虑下面的问题:
    • 在mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
    • 当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
    • 方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.
  • 如何避免上述的问题呢?
    • 在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
    • 我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.
  • 具体怎么做呢?
    • 我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
    • 定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.
Mutation常量类型 – 代码

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

  • 但是, 如果Vuex中的代码, 我们使用了异步函数: 图2

4.Action

Action的基本定义
  • 我们强调, 不要再Mutation中进行异步操作.

    • 但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
    • Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.
  • Action的基本使用代码如下:

    • context是什么?
    • context是和store对象具有相同方法和属性的对象.
    • 也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等.
      但是注意, 这里它们并不是同一个对象, 为什么呢? 我们后面学习Modules的时候, 再具体说.
  • 这样的代码是否多此一举呢?

    • 我们定义了actions, 然后又在actions中去进行commit, 这不是脱裤放屁吗?
    • 事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了.
Action的分发
  • 在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch

  • 同样的, 也是支持传递payload

Action返回的Promise
  • 前面我们学习ES6语法的时候说过, Promise经常用于异步操作.
    • 在Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolve或reject.
  • OK, 我们来看下面的代码:

5.Module

认识Module
  • Module是模块的意思, 为什么在Vuex中我们要使用模块呢?
    • Vue使用单一状态树,那么也意味着很多状态都会交给Vuex来管理.
    • 当应用变得非常复杂时,store对象就有可能变得相当臃肿.
    • 为了解决这个问题, Vuex允许我们将store分割成模块(Module), 而每个模块拥有自己的state、mutations、actions、getters等
  • 我们按照什么样的方式来组织模块呢?
    • 我们来看左边的代码

Module局部状态
  • 上面的代码中, 我们已经有了整体的组织结构, 下面我们来看看具体的局部模块中的代码如何书写.
    • 我们在moduleA中添加state、mutations、getters
    • mutation和getters接收的第一个参数是局部状态对象

  • 注意:
    • 虽然, 我们的doubleCount和increment都是定义在对象内部的.
    • 但是在调用的时候, 依然是通过this.$store来直接调用的.
模块中Actions的写法
  • actions的写法呢? 接收一个context参数对象
    • 局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

  • 如果getters中也需要使用全局的状态, 可以接受更多的参数

04-项目组织结构

  • 当我们的Vuex帮助我们管理过多的内容时, 好的项目结构可以让我们的代码更加清晰

05-TODOLIST练习

九、网络封装

主要内容:

常见的网络请求模块,以及优缺点对比。

JSONP的原理和封装
JSONP原理回顾
JSONP请求封装

axios的内容详解
认识axios网络模块
发送基本请求
axios创建实例
axios拦截器的使用

01-网络模块选择

Vue中发送网络请求有非常多的方式, 那么, 在开发中, 如何选择呢?

选择一: 传统的Ajax

  • 传统的Ajax是基于XMLHttpRequest(XHR)

  • 为什么不用它呢?

    • 非常好解释, 配置和调用方式等非常混乱.
    • 编码起来看起来就非常蛋疼.
    • 所以真实开发中很少直接使用, 而是使用jQuery-Ajax

选择二: jQuery-Ajax

  • 在前面的学习中, 我们经常会使用jQuery-Ajax
    • 相对于传统的Ajax非常好用.
  • 为什么不选择它呢?
    • 首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
    • 那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
    • jQuery的代码1w+行.
    • Vue的代码才1w+行.
    • 完全没有必要为了用网络请求就引用这个重量级的框架.

选择三: Vue-resource

  • 官方在Vue1.x的时候, 推出了Vue-resource.
    • Vue-resource的体积相对于jQuery小很多.
    • 另外Vue-resource是官方推出的.
  • 为什么不选择它呢?
    • 在Vue2.0退出后, Vue作者就在GitHub的Issues中说明了去掉vue-resource, 并且以后也不会再更新.
    • 那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新和维护.
    • 对以后的项目开发和维护都存在很大的隐患.

选择四: Axios

  • 在说明不再继续更新和维护vue-resource的同时, 作者还推荐了一个框架: axios为什么不用它呢?
    • axios有非常多的优点, 并且用起来也非常方便.
    • 稍后, 我们对他详细学习.

02-JSONP

JSONP原理

  • 在前端开发中, 我们一种常见的网络请求方式就是JSONP
    • 使用JSONP最主要的原因往往是为了解决跨域访问的问题.
  • JSONP的原理是什么呢?
  • JSONP的核心在于通过

image-20210713191311888

JSONP封装

import axios from "axios";

export default {
  name: "app",
  created() {
    // 提问:为什么我这里没有跨域的问题?
    // 1.没有请求参数
    axios
      .get("http://123.207.32:8000/category")
      .then((res) => {
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      });

    // 2.有请求参数
    axios
      .get("http://123.207.32:8000/home/data", {
        params: { type: "sell", page: 1 },
      })
      .then((res) => {
        console.log(res);
      })
      .catch((err) => {
        console.log(err);
      });
  },
};
function handleParam(data) {
  let url = "";
  for (let key in data) {
    let value = data[key] !== undefined ? data[key] : "";
    url += `&${key}=${encodeURIComponent(value)}`;
  }
  return url;
}

03-Axios的使用

1.认识axios

为什么选择axios?

认识axios网络模块

功能特点:

  • 在浏览器中发送 XMLHttpRequests 请求
  • 在 node.js 中发送 http请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 等等

补充: axios名称的由来? 个人理解

  • 没有具体的翻译.
  • axios: ajax i/o system.
axios请求方式

支持多种请求方式:

  • axios(config)
  • axios.request(config)
  • axios.get(url[, config])
  • axios.delete(url[, config])
  • axios.head(url[, config])
  • axios.post(url[, data[, config]])
  • axios.put(url[, data[, config]])
  • axios.patch(url[, data[, config]])

config是一个对象

如何发送请求呢?

  • 我们看一下下面的案例

2.发送请求

发送get请求

代码,

import axios from 'axios'

axios({
  url: 'http://123.207.32.32:8000/home/multidata',
  method: 'get'
}).then(res => {
  console.log(res)
})

或
发送并发请求
  • 有时候, 我们可能需求同时发送两个请求
    • 使用axios.all, 可以放入多个请求的数组.
    • axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2

axios全局配置
  • 在上面的示例中, 我们的BaseURL是固定的
    • 事实上, 在开发中可能很多参数都是固定的.
    • 这个时候我们可以进行一些抽取, 也可以利用axiox的全局配置
axios.defaults.baseURL = ‘123.207.32.32:8000’
axios.defaults.headers.post[‘Content-Type’] = ‘application/x-www-form-urlencoded’;

常见配置选项
  • 请求地址

    • url: ‘/user’,
  • 请求类型

    • method: ‘get’,
  • 请根路径
    • baseURL: ‘http://www.mt.com/api’,
  • 请求前的数据处理
    • transformRequest:[function(data){}],
  • 请求后的数据处理
    • transformResponse: [function(data){}],
  • 自定义的请求头
    • headers:{‘x-Requested-With’:‘XMLHttpRequest’},
  • URL查询对象
    • params:{ id: 12 },
  • 查询对象序列化函数
    • paramsSerializer: function(params){ }
  • request body
    • data: { key: ‘aa’},
  • 超时设置s
    • timeout: 1000,
  • 跨域是否带Token
    • withCredentials: false,
  • 自定义请求处理
    • adapter: function(resolve, reject, config){},
  • 身份验证信息

    • auth: { uname: ‘’, pwd: ‘12’},
  • 响应的数据格式 json / blob /document /arraybuffer / text / stream

    • responseType: ‘json’,

3.axios实例

创建axios实例
  • 为什么要创建axios的实例呢?
    • 当我们从axios模块中导入对象时, 使用的实例是默认的实例.
    • 当给该实例设置一些默认配置时, 这些配置就被固定下来了.
    • 但是后续开发中, 某些配置可能会不太一样.
    • 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等.
    • 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.

代码见,

import axios from 'axios'
// 创建新的实例
const axiosInstance = axios.create({
  baseURL: 'http://152.136.185.210:7878/api/m5',
  timeout: 5000,
  headers: {
    'Content-type': 'application/x-www-form-urlencoded'
  }
})

// 发送网络请求
axiosInstance({
  url: '/category',
  method: 'get'
})
  .then(res => {
    console.log(res)
  })
  .catch(err => {
    console.log(err)
  })
axios的封装

代码见LearnVuejs09/…request.js,视频见05-(掌握)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.请求拦截的作用
  instance.interceptors.request.use(config => {
    // console.log(config);
    // 1.比如config中的一些信息不符合服务器的要求

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

    // 3.某些网络请求(比如登录(token)), 必须携带一些特殊的信息
    return config
  }, err => {
    // console.log(err);
  })

  // 2.2.响应拦截
  instance.interceptors.response.use(res => {
    // console.log(res);
    return res.data
  }, err => {
    console.log(err);
  })

  // 3.发送真正的网络请求
  return instance(config)
}

// export function request(config) {
//   return new Promise((resolve, reject) => {
//     // 1.创建axios的实例
//     const instance = axios.create({
//       baseURL: 'http://123.207.32.32:8000',
//       timeout: 5000
//     })
//
//     // 发送真正的网络请求
//     instance(config)
//       .then(res => {
//         resolve(res)
//       })
//       .catch(err => {
//         reject(err)
//       })
//   })
// }

// export function request(config) {
//   // 1.创建axios的实例
//   const instance = axios.create({
//     baseURL: 'http://123.207.32.32:8000',
//     timeout: 5000
//   })
//
//   // 发送真正的网络请求
//   instance(config.baseConfig)
//     .then(res => {
//       // console.log(res);
//       config.success(res);
//     })
//     .catch(err => {
//       // console.log(err);
//       config.failure(err)
//     })
// }

// export function request(config, success, failure) {
//   // 1.创建axios的实例
//   const instance = axios.create({
//     baseURL: 'http://123.207.32.32:8000',
//     timeout: 5000
//   })
//
//   // 发送真正的网络请求
//   instance(config)
//     .then(res => {
//       // console.log(res);
//       success(res);
//     })
//     .catch(err => {
//       // console.log(err);
//       failure(err)
//     })
// }

// function test(aaa, bbb) {
//   // aaa('Hello World')
//   bbb('err message')
// }
//
// test(function (res) {
//   console.log(res);
// }, function (err) {
//   console.log(err);
// })

4.axios的拦截器

使用拦截器
  • axios提供了拦截器,用于我们在发送每次请求或者得到相应后,进行对应的处理。
  • 如何使用拦截器呢?

代码见,

请求拦截器
  • 请求拦截可以做到的事情:

  • 请求拦截中错误拦截较少,通常都是配置相关的拦截
    • 可能的错误比如请求超时,可以将页面跳转到一个错误页面中。
响应拦截器
  • 响应拦截中完成的事情:

    • 响应的成功拦截中,主要是对数据进行过滤。

    • 响应的失败拦截中,可以根据status判断报错的错误码,跳转到不同的错误提示页面。

十、项目实战

项目开发

A.创建项目

vue 脚手夹创建项目

B.划分目录结构

1.删除src下assets文件夹下的图片,并新建两个文件夹,img和css

2.在src下新建文件夹views,用于存放视图组件

3.老师习惯在components文件夹下建两个文件夹common和content。common用于存放不仅仅只是在当前项目中用到的组件,而且在其他项目中也使用的组件;和当前业务相关的公共组件放在content(注:src/components下一般放一些公共的组件)

4.src下新建router文件夹

5.src下新建store文件夹

6.src下新建network文件夹,网络相关,网络相关的所有东西的封装都会放在network文件夹下

7.src下新建问价夹common,一般放一些公共的js文件。(可能会在common文件夹下新建const.js,一些公共的常量放在该js文件中;新建utils.js,工具类;新建mixin.js混入)

C.引用了两个css文件

下载normalize.js放在src/assets/css中(normalize.js下载地址:)

在src/assets/css中,新建base.css代码如下:

@import "./normalize.css";

/* :root -> 获取根元素html */

:root {
  --color-text: #666;
  --color-high-text: #ff5777;
  --color-tint: #ff8198;
  --color-background: #fff;
  --font-size: 14px;
  --line-height: 1.5;
}

*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: "Helvetica Neue",Helvetica,"PingFang SC","Hiragino Sans GB","Microsoft YaHei","微软雅黑",Arial,sans-serif;
  user-select: none; /* 禁止用户鼠标在页面上选中文字/图片等 */
  -webkit-tap-highlight-color: transparent; /* webkit是苹果浏览器引擎,tap点击,highlight背景高亮,color颜色,颜色用数值调节 */
  background: var(--color-background);
  color: var(--color-text);
  /* rem vw/vh */
  width: 100vw;
}

a {
  color: var(--color-text);
  text-decoration: none;
}


.clear-fix::after {
  clear: both;
  content: '';
  display: block;
  width: 0;
  height: 0;
  visibility: hidden;
}

.clear-fix {
  zoom: 1;
}

.left {
  float: left;
}

.right {
  float: right;
}
D.配置路径别名

在根目录下创建 vue.config.js,代码如下

module.exports = {
  configureWebpack: {
    resolve: {
      // 配置别名
      alias: {
        assets: "@/assets",
        common: "@/common",
        components: "@/components",
        network: "@/network",
        views: "@/views"
      }
    }
  }
};

作用:如在访问’./assets’文件时可以直接’assets’访问到,’@'为自动配置的,表示src,即node_modules下自动配置了‘@’:‘src’

根目录下.editorconfig的作用主要是对代码进行统一的风格

4.4. 项目的模块划分: tabbar -> 路由映射关系
4.5. 首页开发
  • navbar 的封装
  • 网络数据的请求
  • 轮播图
  • 推荐信息
git remote add origin https://github.com/coderwhy/testmall.git
git push -u origin master

sync -> 同步

async -> 异步

aysnc operation: 操作

xcode/iphonex/xml

token ->

linus -> linux/git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值