vue笔记(coderwhy)

开始

跟着b站 coderwhy 老师所做的笔记,对比了几个感觉很适合Vue初学
链接:https://www.bilibili.com/video/BV15741177Eh?from=search&seid=5110308374587736588&spm_id_from=333.337.0.0

下载本地Markdown文件:https://gitee.com/xiao-li-tongxue/img/tree/master/
在这里插入图片描述

00、ES6的补充(后面用到)

a、let/var

  • ES5中的 var 是没有块级作用域的(if/for)
  • ES6中的 let 是有块级作用域的(if/for)

ES5之前因为if和for都没有块级作用域的概念, 所以在很多时候, 我们都必须借助于function的作用域来解决应用外面变量的问题.(因为function有作用域)

ES6中,加入了let, let它是有if和for的块级作用域

 //1.变量作用域: 变量在什么范围内是可用的
  {
    var name = 'why';
    console.log(name);
  }
  //在括号外面依然可以用,因为ES5之前var是没有块级作用域的概念的
  console.log(name);
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>

<script>
  // 没有块级作用域引起的问题: for的块级
  var btns = document.getElementsByTagName('button');
  for (var i=0; i<btns.length; i++) {
    console.log('执行成功');
    btns[i].addEventListener('click', function () {
      console.log('第' + i + '个按钮被点击');
    })
</script>

image-20201028132929184

上面也就相当于执行五次循环,每次循环都是下面这段代码

var i = 5;
 var btns = document.getElementsByTagName('button');


    btns[i].addEventListener('click', function () {
      console.log('第' + i + '个按钮被点击');
    })

    btns[i].addEventListener('click', function () {
      console.log('第' + i + '个按钮被点击');
    })
...*3

一、解决作用域问题(ES5)

若要解决此问题,ES5之前可以使用闭包

<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++) {
    (function (num) {
      console.log('执行成功');
      btns[i].addEventListener('click', function () {
        console.log('第' + num + '个按钮被点击');
      })
    })(i)
  }
</script>

上面就相当于执行五次循环,每次循环的num值都不一样。

image-20201028133310041

二、解决作用域问题(ES6)

如果使用ES6语法的话,就简单很多

let关键字是有块级作用域的概念的

<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<button>按钮5</button>

<script>
const btns = document.getElementsByTagName('button')
  for (let i = 0; i < btns.length; i++) {
    btns[i].addEventListener('click', function () {
      console.log('第' + i + '个按钮被点击');
    })
</script>      

image-20201028134053323

b、const的使用

const关键字,在很多语言中已经存在, 比如C/C++中, 主要的作用是将某个变量修饰为常量.

在JavaScript中也是如此, 使用const修饰的标识符为常量, 不可以再次赋值.

建议: 在ES6开发中,优先使用const, 只有需要改变某一个标识符的时候才使用let.

<script>
/*

  //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);
</script>

image-20201031113742623

c、对象增强语法

image-20201031113248395

d、箭头函数

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

//无形参
var f = () => 5;
// 等同于
var f = function () { return 5 };

var f = a = > a
//一个参数,等同于
var f = function(a){
   return a;
}

//多个形参
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

一、关于箭头函数this的指向问题

1,箭头函数:出现的作用除了让函数的书写变得很简洁,可读性很好外;最大的优点是解决了this执行环境所造成的一些问题。比如:解决了匿名函数this指向的问题(匿名函数的执行环境具有全局性),包括setTimeout和setInterval中使用this所造成的问题。

2,我们常见的window属性和方法有alter,document,parseInt,setTimeout,setInterval,localtion等等,这些在默认的情况下是省略了window前缀的。(window.alter = alter)。

箭头函数没有自己的this, 它的this是继承而来;

<script>
 function test() {
   console.log(this);
 }
 test();
</script>

结果是: window

原因: test()是一个全局函数,也就是说是挂在window对象下的,所以test()等价于 window.test() ,所以此时的this是window

<script>
 var obj = {
   say: function () {
     setTimeout(function () {
       console.log(this)
     });
   }
 }
 obj.say();
</script>

结果是: window

匿名函数,定时器中的函数,由于没有默认的宿主对象,所以默认this指向window

<script>
    const obj = {
        aaa() {
            setTimeout(function () { // *
                setTimeout(function () {
                    console.log(this); // window前缀,this指向window
                })

                setTimeout(() => {
                    console.log(this); // window,上一层是 * 标记处,window前缀方法,this指向window
                })
            })

            setTimeout(() => {      //***
                setTimeout(function () {
                    console.log(this); // window前缀
                })

                setTimeout(() => {
                    console.log(this); // Object ,找到上一层,即 *** 标记处,也是箭头函数,再找上一层,即 aaa() 方法处,this指向Object
                })
            })
        }
    }

    obj.aaa()
</script>

e、高阶函数

高阶函数2.js

let total = nums.filter(n => n<100).map(n => n*2).reduce((pre,n) => pre + n)
<div id="app">
</div>

<script src="../js/vue.js"></script>
<script src="高阶函数2.js"></script>
<script>
  console.log(total)
</script>

按照我们之前的方式

高阶函数.js:

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

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

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

console.log(total);

利用函数:

const nums = [10, 20, 111, 222, 444, 40, 50]

//1.filter函数的使用
// 10, 20, 40, 50
let newNums = nums.filter(function (n) {
  return n < 100
})
//filter函数返回一个数组,将 n<100 的值都存入数组中
// console.log(newNums);

// 2.map函数的使用
// 20, 40, 80, 100
let new2Nums = newNums.map(function (n) { // 20
  return n * 2
})

// 3.reduce函数的使用
/*reduce作用对数组中所有的内容进行汇总,第一个参数中的第一个值preValue的默认值是第二个参数的值0,第一个参数的第二个值n是
数组中的值*/
let total = new2Nums.reduce(function (preValue, n) {
  return preValue + n
}, 0)
console.log(total);

//第一次: preValue 0 n 20
//第二次: preValue 20 n 40
//第三次: preValue 60 n 80
//第四次: preValue 140 n 100
//240

转换为高阶函数:

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

利用箭头函数的高阶函数:

即开头的高阶函数2.js

let total = nums.filter(n => n<100).map(n => n*2).reduce((pre,n) => pre + n)

01、认识和安装Vuejs

a、Vuejs简单介绍

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

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

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

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

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

b、Vuejs安装

安装Vue的方式有很多:

  1. 方式一:直接CDN引入

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

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

    开发环境 https://vuejs.org/js/vue.js 
    生产环境 https://vuejs.org/js/vue.min.js
    
  3. 方式三:NPM安装

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

02、Vuejs初体验

a、HelloVue代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">{{message}}</div>         <!--显示下方数据-->
<div>{{message}}</div>                  <!--只会显示{{message}}-->

<script src="../js/vue.js"></script>
<script>
    const app = new Vue({
        el:'#app',  //用于挂载要管理的元素
        data:{      //定义一些数据
            message:'你好啊,李银河'
        }
    })
</script>
</body>
</html>

image-20201003181257570

1、代码做了什么事?

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

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

{}中包含了el属性:该属性决定了这个Vue对象挂载到哪一个元素上,很明显,我们这里是挂载到了id为app的元素上

{}中包含了data属性:该属性中通常会存储一些数据

这些数据可以是我们直接定义出来的,比如像上面这样,也可能是来自网络,从服务器加载的。

2、浏览器执行代码的流程:

执行到8行代码显然出对应的HTML

执行第13行代码创建Vue实例,并且对原HTML进行解析和修改。

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

b、vue列表展示

<body>

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

<script src="../js/vue.js"></script>
<script>
    const app = new Vue({
        el:'#app',
        data:{
            movies:['星际穿越','少年派','大话西游']
        }
    })
</script>
</body>

这样是显示不出来列表的,网页显示是这样:

image-20201003181558091

可以利用v-for显示(后面会讲)

<body>

<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:{
            movies:['星际穿越','少年派','大话西游']
        }
    })
</script>
</body>

image-20201003185202496

在浏览器控制台添加列表数据

image-20201003185456475

c、小案例:计数器

点击 + 计数器+1

点击 - 计数器 -1

这里,我们又要使用新的指令和属性了

新的属性:methods,该属性用于在Vue对象中定义方法。

新的指令:@click, 该指令用于监听某个元素的点击事件,并且需要指定当发生点击时,执行的方法(方法通常是methods中定义的方法)

@click 是v-on:click 的语法糖,即 v-on:click的简写。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div id="app">
    <h2>当前计数:{{counter}}</h2>
    <!--
    <button v-on:click="add">+</button>
    <button v-on:click="sub">-</button>
    -->
    <button @click="add">+</button>
    <button @click="sub">-</button>
</div>

<script src="../js/vue.js"></script>
<script>
    const app = new Vue({
        el:'#app',
        data:{
            counter:0
        },
        methods:{
            add(){
                console.log('add被执行');
                this.counter++;
            },
            sub(){
                console.log('sub被执行');
                this.counter--;
            }
        }
    })
</script>

image-20201011152633591

03、Vue的MVVM

MVVM与MVC最大的区别就是:它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变。

Vue实例中的data相当于Model层,而ViewModel层的核心是Vue中的双向数据绑定,即Model变化时VIew可以实时更新,View变化也能让Model发生变化。

整体看来,MVVM比MVC精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作DOM元素。因为在MVVM中,View不知道Model的存在,Model和ViewModel也观察不到View,这种低耦合模式提高代码的可重用性。

  1. View层:
    视图层
    在我们前端开发中,通常就是DOM层。
    主要的作用是给用户展示各种信息。

  2. Model层:

    ​ 数据层
    ​ 数据可能是我们固定的死数据,更多的是来自我们服务器,从网络上请求下来的数据。
    ​ 在我们计数器的案例中,就是后面抽取出来的obj,当然,里面的数据可能没有这么简单。

  3. VueModel层:

    ​ 视图模型层
    ​ 视图模型层是View和Model沟通的桥梁。
    ​ 一方面它实现了Data Binding,也就是数据绑定,将Model的改变实时的反应到View中
    ​ 另一方面它实现了DOM Listener,也就是DOM监听,当DOM发生一些事件(点击、滚动、touch等)时,可以监听到,并在需要的情 况下改变对应的Data。

04、Vue的生命周期

a、什么是生命周期?

简而言之:从生到死的过程,从Vue实例创建-运行-销毁的过程

Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、销毁等一系列过程

b、生命周期方法?

Vue从生到死的过程中伴随着各种各样的事件,这些事件会自动触发一些方法.这些方法我们统称为生命周期方法

生命周期钩子 = 生命周期函数 = 生命周期事件

一、创建期间生命周期方法

beforeCreate:

created:

beforeMount

mounted

二、运行期间生命周期方法

beforeUpdate

updated

三、销毁期间的生命周期方法

beforeDestroy

destroyed

四、代码解释

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Vue生命周期方法</title>
    <!--引入vue框架-->
    <script src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <p>{{msg}}</p>
</div>
 
<script>
   
    let vm = new Vue({
        el:'#app',
        data:{
            msg:"IT程序员的日常"
        },
        methods:{
            say:function () {
                console.log("IT程序员的日常");
            }
        },
        beforeCreate:function () {
            /*执行beforeCreate的时候,表示Vue刚刚出生,还没有任何内容,data/methods都没有初始化*/
            //console.log(this.msg);
            //this.say();
            //console.log(this.say);
        },
        created:function () {
            /*执行created的时候,表示Vue实例已经初始化好了部分内容,data/methods
            想在Vue实例中最早访问到data/methods,只有在这个方法才能访问
            */
            //console.log(this.msg);
            //this.say();
            //console.log(this.say);
        },
        beforeMount:function () {
            /*执行beforeMount,表示已经根据数据编译好了模板,但是还没有渲染到界面上*/
            // console.log(document.querySelector("p").innerText);
            // console.log(document.querySelector("p").innerHTML);
        },
        mounted:function () {
            /*执行mounted,表示已经根据数据编译好了模板,已经将模板有渲染到界面上,此时可以对界面进行其他操作了*/
            console.log(document.querySelector("p").innerText);
            console.log(document.querySelector("p").innerHTML);
        },
        beforeUpdate:function(){
            /*主要data中的数据发生了变化就会执行
             执行beforeUpdate时候,data的数据已经是最新的了,但是没有更新界面上的数据*/
            // console.log(this.msg);
            // console.log(document.querySelector("p").innerText);
            // console.log(document.querySelector("p").innerHTML);
        },
        updated:function () {
           /*主要data中的数据发生了变化就会执行
            执行updated时候,data的数据已经是最新的了,界面上的数据也已经更新*/
            
            /*
            console.log(this.msg);
            console.log(document.querySelector("p").innerText);
            console.log(document.querySelector("p").innerHTML);
            */
        },
         beforeDestroy:function(){
            /*执行beforeDestroy的时候,表示Vue实例即将销毁,但是还未销毁,实例中的数据等都可以使用
              最后能使用Vue实例的地址
             */
        },
        destroyed:function () {
            /*
             执行destroyed的时候,表示vue实例完全销毁,实例中的任何内容都不能被使用了
            */
        }
 
    })
</script>
</body>
</html>

05、模板语法

a、插值操作

一、Mustache

Mustache语法:也就是双大括号语法。

Mustache:胡子/胡须

<div id="app">
  <h2>{{message}}</h2>
  <h2>{{message}}, 李银河!</h2>

  <!--mustache语法中,不仅仅可以直接写变量,也可以写简单的表达式-->
  <h2>{{firstName + lastName}}</h2>
  <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: {
      message: '你好啊',
      firstName: 'kobe',
      lastName: 'bryant',
      counter: 100
    },
  })
</script>

二、v-once

当我们不希望界面某些数据跟随改变的时候,可以使用 v-once 指令

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

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

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

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

若利用响应式修改数据:message改变,添加 v-once 的标签所显示的界面没有被重新渲染

三、v-html

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

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

如果我们希望解析出HTML展示,可以使用v-html指令,该指令后面往往会跟上一个string类型,会将字符串中的html解析出来并且进行渲染

<div id="app">
  <h2>{{url}}</h2>
  <h2 v-html="url"></h2>
</div>

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

四、v-test

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

v-text通常情况下,接受一个string类型,且后面跟上其他字符串等,不会追加

<div id="app">
  <h2>{{message}}, 李银河!</h2>
  <h2 v-text="message">, 李银河!</h2>
</div>

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

五、v-pre

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

比如下面的代码:

第一个h2元素中的内容会被编译解析出来对应的内容

第二个h2元素中会直接显示{{message}}

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

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

六、v-cloak

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

v-cloak指令:它会在vue实例结束编译时从绑定的html元素上移除

cloak: 斗篷

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

当网络较慢,网页还在加载 Vue.js ,而导致 Vue 来不及渲染,这时页面就会显示出 Vue 源代码。我们可以使用 v-cloak 指令来解决这一问题。

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

<!--vue解析之前,div中有 v-cloak属性,此标签被隐藏-->
<!--vue解析之后,div中没有 v-cloak属性了,此标签显示-->
<div id="app" v-cloak>
  <h2>{{message}}</h2>
</div>

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

</body>
</html>

b、动态绑定属性

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

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

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

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

  1. 作用:动态绑定属性

  2. 缩写(语法糖)::

一、v-bind的基本使用

<div id="app">
  <!-- 错误的做法: 这里不可以使用mustache语法-->
  <!--<img src="{{imgURL}}" alt="">-->
    
  <!-- 正确的做法: 使用v-bind指令 -->
  <!--alt属性是在图片不能正常加载时候显示的提示语-->
  <img v-bind:src="imgURL" alt="">
  <a v-bind:href="aHref">百度一下</a>
  <!--<h2>{{}}</h2>-->

  <!--语法糖的写法-->
  <img :src="imgURL" alt="">
  <a :href="aHref">百度一下</a>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      imgURL: 'https://img11.360buyimg.com/mobilecms/s350x250_jfs/t1/20559/1/1424/73138/5c125595E3cbaa3c8/74fc2f84e53a9c23.jpg!q90!cc_350x250.webp',
      aHref: 'http://www.baidu.com'
    }
  })
</script>

image-20201024090115478

二、v-bind动态绑定class

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

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

绑定class有两种方式:

  • 对象语法
  • 数组语法
1、对象语法

对象语法的含义是:class后面跟的是一个对象

对象语法有下面这些用法:

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

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

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

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

案例:

<head>
  <meta charset="UTF-8">
  <title>Title</title>

  <style>
    .active {
      color: red;
    }
  </style>
</head>
<body>

<div id="app">
  <h2 class="active">{{message}}</h2>
  <h2 :class="active">{{message}}</h2>

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

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

</body>

image-20201024093542646

点击按钮之后:image-20201024093606082

2、数组语法

数组语法的含义是:class后面跟的是一个数组

数组语法有下面这些用法:

加单引号表示字符串,没有单引号表示变量

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

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

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

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

案例:

<div id="app">
  <!--加上' ' 表示字符串-->
  <h2 class="title" :class="['active', 'line']">{{message}}</h2>
  <!--不加' ' 表示变量-->
  <h2 class="title" :class="[active, line]">{{message}}</h2>
  <h2 class="title" :class="getClasses()">{{message}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      active: 'aaaaaa',
      line: 'bbbbbbb'
    },
    methods: {
      getClasses: function () {
        return [this.active, this.line]
      }
    }
  })
</script>

image-20201024131341495

三、v-bind和v-for的联合使用

作业需求: 点击列表中的哪一项, 那么该项的文字变成红色

v-for中使用下标属性

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!--将active类颜色改为red-->
  <style>
    .active{
      color: red;
    }
  </style>
</head>
<body>

<!--作业需求: 点击列表中的哪一项, 那么该项的文字变成红色-->
<div id="app">
  <ul>
    <li v-for="(m, index) in movies" :class="{active: isActive == index}" @click="changeColor(index)">{{m}}</li>
  </ul>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      movies: ['海王', '海尔兄弟', '火影忍者', '进击的巨人'],
      isActive : -1
    },
    methods:{
      changeColor(index){
        this.isActive = index;
      }
    }
  })
</script>

</body>
</html>

四、v-vind动态绑定style

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

在写CSS属性名的时候,比如font-size,我们可以使用驼峰式 (camelCase) fontSize ,或短横线分隔 (kebab-case,记得用单引号括起来) ‘font-size’

绑定class有两种方式:

  • 对象语法
  • 数组语法
1、对象语法
:style="{color: currentColor, fontSize: fontSize + 'px'}"

style后面跟的是一个对象类型

对象的key是CSS属性名称

对象的value是具体赋的值,值可以来自于data中的属性

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <style>
    .title {
      font-size: 50px;
      color: red;
    }
  </style>
</head>
<body>

<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: final Size + '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>

</body>
</html>

image-20201024172606563

2、数组语法
<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>

image-20201024220227541

c、计算属性

计算属性是写在实例的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: {
      fullName: function () {
        return this.firstName + ' ' + this.lastName
      }
    },
    methods: {
      getFullName() {
        return this.firstName + ' ' + this.lastName
      }
    }
  })
</script>

image-20201024222951205

二、计算属性的复杂操作

<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

        /*其他循环的写法*/
        /*for (let i in this.books) {
        result += this.books[i].price
      }*/

        /* for (let book of this.books) {
          result +=book.price
         }*/
      }
    }
  })
</script>

image-20201024222743771

三、计算属性的getter和setter

上面写的计算属性只是简写,实际上是利用get方法

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

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      firstName: 'Kobe',
      lastName: 'Bryant'
    },
    computed: {
    /*  fullName: function () {
        return this.firstName + ' ' + this.lastName
      }*/
      //上面是计算属性的简写,实际上计算属性是下面这样写的
      // 计算属性一般是没有set方法, 只读属性.
      fullName: {
        //若使用set方法,需要接受参数
        set: function(newValue) {
          // console.log('-----', newValue);
          //利用空格分隔符,将firstName和lastName分出存入names数组中
          //split() 方法用于把一个字符串分割成字符串数组 
          const names = newValue.split(' ');
          this.firstName = names[0];
          this.lastName = names[1];
        },
        get: function () {
          return this.firstName + ' ' + this.lastName
        }
      },

    }
  })
</script>

因为有set属性,所以可以修改fullName计算属性中set方法形参

四、计算属性和methods的对比

计算属性有缓存

<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');
        return this.firstName + ' ' + this.lastName
      }
    }
  })

</script>

image-20201028111433027

d、事件监听

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

这个时候,我们就必须监听用户发生的时间,比如点击、拖拽、键盘事件等等

在Vue中如何监听事件呢?使用v-on指令

一、v-on的基本使用

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

<!--可以直接写成表达式-->
  <button v-on:click="counter++">+</button>
  <button v-on:click="counter--">-</button>

<!--写成函数形式-->
  <!--<button v-on:click="increment">+</button>-->
  <!--<button v-on:click="decrement">-</button>-->

<!--语法糖格式:@ -->
  <button @click="increment">+</button>
  <button @click="decrement">-</button>
</div>

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

二、v-on的参数问题

<div id="app">
  <!--1.事件调用的方法没有参数-->
  <button @click="btn1Click()">按钮1</button>
  <button @click="btn1Click">按钮1</button>


   
  <!--<button @click="btn2Click(123)">按钮2</button>-->
<!--  生成的是 -------- 123-->

<!--如果函数需要参数,但是没有传入, 那么函数的形参为undefined--> 
  <!--  <button @click="btn2Click()">按钮2</button>-->
<!--  生成的是 -------- undefined-->


<!--2.在事件定义时, 写方法时省略了小括号, 但是方法本身是需要一个参数的,
    这个时候, Vue会默认将浏览器生产的event原生事件对象作为参数传入到方法-->
  <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);
      }
    }
  })

</script>

image-20201031175918894

三、v-on的修饰符

image-20201031175005140

事件冒泡:点击子级元素接收到事件后,会把他接收到的事件传播给他的父级,也就是父级也会接收到子级的事件

<div id="app">
  <!--1. .stop修饰符的使用,阻止Html的事件冒泡-->
  <!--若不加 .stop修饰符,会发生事件冒泡,即点击div中的按钮,回调bthClick方法,会将接收到的事件传播给他的父级(div),也就是divClick也会被回调-->
  <div @click="divClick">
    aaaaaaa
    <button @click.stop="btnClick">按钮</button>
  </div>

  <!--2. .prevent修饰符的使用-->
  <!--阻止默认行为,点击提交后,不会直接跳转,而会回调submitClick方法-->
  <br>
  <form action="baidu">
    <input type="submit" value="提交" @click.prevent="submitClick">
  </form>

  <!--3. @keyup. 监听某个键盘的键帽,这是监听回车,当回车敲下并回弹的时候,回调keyup方法-->
  <!--若只写 @keyup="keyUp",则点击任意键盘回弹的时候都会回调keyUp方法-->
    <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>

e、条件判断

一、v-if的使用

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

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

image-20210425225822025

image-20210425230129902

二、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: 62
    },
    /*
    //一般不用 v-if v-else-if...的语法,因为有些麻烦,一般封装为函数或者计算属性,方便调用
    computed: {
      result() {
        let showMessage = '';
        if (this.score >= 90) {
          showMessage = '优秀'
        } else if (this.score >= 80) {
          showMessage = '良好'
        }else if (this.score>=60){
          showMessage = '及格'
        }else {
          showMessage = '不及格'
        }
        return showMessage
      }
    }*/
  })
</script>

四、用户登陆切换的案例

<div id="app">
  <span v-if="isUser">
    <label for="username">用户账号</label>
     <!--placeholder属性提供可描述输入字段预期值的提示信息-->
    <input type="text" id="username" placeholder="用户账号">
  </span>
  <span v-else>
    <label for="email">用户邮箱</label>
    <input type="text" id="email" placeholder="用户邮箱">
  </span>
  <button @click="isUser = !isUser">切换类型</button>
</div>

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

但是这样会有一个问题,如果我们在有输入内容的情况下,切换了类型,我们会发现文字依然显示之前的输入的内容。
但是按道理讲,我们应该切换到另外一个input元素中了。在另一个input元素中,我们并没有输入内容。

这是因为Vue在进行DOM渲染时,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。
在上面的案例中,Vue内部会发现原来的input元素不再使用,直接作为else中的input来使用了。

解决方案:
如果我们不希望Vue出现类似重复利用的问题,可以给对应的input添加key
并且我们需要保证key的不同

更改方案:

添加key,保证key的不同

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

五、v-show的使用

v-show的用法和v-if非常相似,也用于决定一个元素是否渲染:

v-if和v-show都可以决定一个元素是否渲染,那么开发中我们如何选择呢?

  1. v-if当条件为false时,压根不会有对应的元素在DOM中。

  2. v-show当条件为false时,仅仅是将元素的display属性设置为none而已。

开发中如何选择呢?

  1. 当需要在显示与隐藏之间切片很频繁时,使用v-show
  2. 当只有一次切换时,通过使用v-if
<div id="app">
  <!--v-if: 当条件为false时, 包含v-if指令的元素, 根本就不会存在dom中-->
  <h2 v-if="isShow" id="aaa">{{message}}</h2>

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

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

f、循环遍历

一、v-for循环数组

当我们有一组数据需要进行渲染时,我们就可以使用v-for来完成。
v-for的语法类似于JavaScript中的for循环
格式如下:item in items的形式

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

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

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

image-20201112093210915

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

image-20201112093225408

三、v-for使用过程添加key

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

为什么需要这个key属性呢(了解)
这个其实和Vue的虚拟DOM的Diff算法有关系

这里我们借用React’s diff algorithm中的一张图来简单说明一下:

image-20201112191508070

当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点,我们希望可以在B和C之间加一个F,Diff算法默认执行起来是这样的:即把C更新成F,D更新成C,E更新成D,最后再插入E,是不是很没有效率

所以我们需要使用key来给每个节点做一个唯一标识
Diff算法就可以正确的识别此节点
找到正确的位置区插入新的节点。
所以一句话,key的作用主要是为了高效的更新虚拟DOM

div id="app">
  <ul>
    <!--绑定的key必须和后面的item是一一对应的,所以不能用index-->
    <li v-for="item in letters" :key="item">{{item}}</li>
  </ul>
</div>

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

四、检测哪些数组方法响应式

因为Vue是响应式的,所以当数据发生变化时,Vue会自动检测数据变化,不用刷新页面,视图会发生对应的更新

<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, 并且后面跟上要插入的元素
        其实就是删除 第一个参数 后面的 第二个参数 个元素,然后加上新元素
        */
        this.letters.splice(1, 3, 'm', 'n', 'l', 'x')
        // this.letters.splice(1, 0, 'x', 'y', 'z')

        // 6.sort() 排序
        // this.letters.sort()

        // 7.reverse()  反转数据
        // this.letters.reverse()

        // 注意: 通过索引值修改数组中的元素,这个不是响应式的
        // this.letters[0] = 'bbbbbb';

        //若要修改,则可以通过以下代码修改
        //方法1
        // this.letters.splice(0, 1, 'bbbbbb')

        //方法2
        // set(要修改的对象, 索引值, 修改后的值)
        // Vue.set(this.letters, 0, 'bbbbbb')
      }
    }
  })
</script>

通过索引值修改数组中的元素,不是响应式的,可以利用splice或者Vue的set方法修改数组中的元素

g、书籍购物车案例

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
  <!--rel 属性规定当前文档与被链接文档之间的关系。
只有 rel 属性为 "stylesheet" 值得到了所有浏览器的支持。其他值只得到了部分地支持。
  -->
  <link rel="stylesheet" href="style.css">
</head>
<body>

<div id="app">
  <div v-if="books.length">
    <table>
      <thead>
      <tr>
        <th></th>
        <th>书籍名称</th>
        <th>出版日期</th>
        <th>价格</th>
        <th>购买数量</th>
        <th>操作</th>
      </tr>
      </thead>
      <tbody>
      <tr v-for="(item, index) in books">
        <td>{{item.id}}</td>
        <td>{{item.name}}</td>
        <td>{{item.date}}</td>
        <!--用 | 符号后跟过滤器,前面相当于传入过滤器的参数-->
        <td>{{item.price | showPrice}}</td>
        <td>
             <!--当书籍的数量小于或等于1的时候,无效化 - 按钮-->
          <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>
<script>
</script>
</body>
</html>

main.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
    }
  },
  filters: {
    showPrice(price) {
      return '¥' + price.toFixed(2)
    }
  }
})

style.css

table {
  /*border:边框的样式  1px表示线的像素 solid表示实线 #e9e9e9表示白色*/
  border: 1px solid #e9e9e9;
  /*为表格设置合并边框模型
    border-collapse 属性设置表格的边框是否被合并为一个单一的边框,还是象在标准的 HTML 中那样分开显示
    */
  border-collapse: collapse;
/*设置表格的边框间距*/
  border-spacing: 0;
}

th, td {
  /*边距填充*/
  padding: 8px 16px;
  border: 1px solid #e9e9e9;
  text-align: left;
}

th {
  /*背景颜色*/
  background-color: #5c6b77;
  /*字体颜色*/
  color: #f7f7f7;
  /*设置段落字体的粗细*/
  font-weight: 600;
}

h、v-modle的使用

一、v-model的基本使用

双向绑定数据

<div id="app">
  <input type="text" v-model="message">
  {{message}}
</div>

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

二、v-model的原理

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

  1. v-bind绑定一个value属性

  2. v-on指令给当前元素绑定input事件

也就是说下面三行代码等同

<input type="text" v-model="message">
<!--等同于-->
<input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
<!--语法糖-->
<input type="text" :value="message" @input="message = $event.target.value">

三、v-model结合radio类型

单选框

for属性规定label与哪个表单元素绑定

label 和表单控件绑定方式有两种:

方法一:将表单控件作为label的内容,这种就是隐式绑定。

此时不需要for属性,绑定的控件也不需要id属性。

隐式绑定:
<label>Date of Birth: <input type="text" name="DofB" /></label>

方法二:为label标签下的for属性命名一个目标表单的id,这种就是显示绑定,for属性绑定input的id

显式绑定:
<label for="SSN">Social Security Number:</label>
<input type="text" name="SocSecNum" id="SSN" />

image-20201123213539562

为什么要给label上面加上for属性

给 label 加了 for 属性绑定了input控件后,可以提高鼠标用户的用户体检

如果在label 元素内点击单选框的文本,就会触发此控件,也就是说,当用户渲染该标签时,浏览器就会自动将焦点转到和标签相关的表单控件上

如下面点击 单选框后面的文本 男 就会自动选上那个单选框,若不加 lable 元素,则点击 男 没有反应

关于 radio 类型标签:

  1. name要相同,来定义为一组,表示必须是单选框,若name不同则不是单选框
  2. 为了保证传给后台的值是不同的,要定义不同的value
<div id="app">
  <!--隐式-->
  <label>
    <!--当 v-model绑定同一个值的时候,name可以省略-->
    <input type="radio" id="male" value="" v-model="sex"><!--<input type="radio" id="male" name='sex' value="男" v-model="sex">男-->
  </label>
  
  <!--显式-->
  <input type="radio" id="female" value="" v-model="sex">
  <!--<input type="radio" id="female" name='sex' value="女" v-model="sex">女-->
  <label for="female"></label>
  <h2>您选择的性别是: {{sex}}</h2>
</div>

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

image-20201124000416881

四、v-model结合checkbox类型

单个勾选框、复选框

单个勾选框:

  1. v-model 即为布尔值
  2. 此时 input 的 value 并不影响 v-model 的值

多个复选框:

  1. 当是多个复选框时,因为可以选中多个,所以对应的 data 中属性是一个数组
  2. 当选中某一个时,就会将 input 的 value 添加到数组中
<div id="app">
<!-- 1.checkbox单选框,绑定的是一个布尔值 -->
  <label for="agree">
    <input type="checkbox" id="agree" v-model="isAgree">同意协议
  </label>
  <h2>您选择的是: {{isAgree}}</h2>
    <!--当勾选同意了之后,才能进行下一步-->
  <button :disabled="!isAgree">下一步</button>
  <br/>  <br/>  <br/>  <br/>

  <!--2.checkbox多选框,绑定的是一个数组-->
  <input type="checkbox" value="篮球" v-model="hobbies">篮球
  <input type="checkbox" value="足球" v-model="hobbies">足球
  <input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
  <input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
  <h2>您的爱好是: {{hobbies}}</h2>
</div>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      isAgree: false, // 单选框
      hobbies: [], // 多选框,
    }
  })
</script>

image-20201123235943395

五、v-model结合select类型

单选:只能选中一个值

  1. v-model绑定的是一个值
  2. 当我们选中 option 中的一个时,会将它对应的 value 赋值到 fruit 中

多选:可以选中多个值

  1. v-model绑定的是一个数组
  2. 当选中多个值时,就会将选中的option对应的value添加到数组 fruits 中
<div id="app">
  <!--1.仅支持选择一个的选择列表-->
  <select name="abc" v-model="fruit">
    <option value="苹果">苹果</option>
    <option value="香蕉">香蕉</option>
    <option value="榴莲">榴莲</option>
    <option value="葡萄">葡萄</option>
  </select>
  <h2>您选择的水果是: {{fruit}}</h2>

  <!--2.可选择多个的选择列表-->
  <!--multiple表示可接受多个值的文件上传字段,按住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>

image-20201124000310521

六、input中的值绑定

就是动态的给value赋值而已

<div id="app">
    
  <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: {
      hobbies: [], // 多选框,
      originHobbies: ['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
    }
  })
</script>

七、v-model修饰符的使用

a、lazy修饰符:

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

b、number修饰符:

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

c、trim修饰符:

  1. 如果输入的内容首尾有很多空格,通常我们希望将其去除
  2. trim修饰符可以过滤内容左右两边的空格
<div id="app">
  <!--1.修饰符: lazy,懒加载,按下回车或失去焦点后才改变v-model绑定的变量的值-->
  <input type="text" v-model.lazy="message">
  <h2>{{message}}</h2>


  <!--2.修饰符: number,默认情况下,v-model会自动将绑定的变量转换为string类型-->
  <input type="number" v-model.number="age">
  <h2>{{age}}-{{"输入的类型是:"}}{{typeof age}}</h2>

  <!--3.修饰符: trim,去除前后空白行-->
  <input type="text" v-model.trim="name">
  <h2>您输入的名字:{{name}}</h2>
</div>

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

</script>

06、组件化开发

a、什么是组件化

image-20201127165029362

b、组件化的基本使用过程

组件的使用分成三个步骤:

  1. 创建组件构造器
  2. 注册组件
  3. 使用组件

image-20201127165251455

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

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

<my-cpn></my-cpn>

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

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

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

结果是显示五次组件中的内容

Vue.extend():

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

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

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

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

Vue.component():

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

组件必须挂载在某个Vue实例下,否则它不会生效

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

image-20201127170159792

c、全局组件和局部组件

image-20201127170811925

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

const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    //注册局部组件,仅允许当前Vue实例使用
    components: {
      // cpn:使用组件时的标签名
      cpn: cpnC
    }
  })

d、父组件和子组件

image-20201127172435288

<div id="app">
  <cpn2></cpn2>
  <!--此处直接写子组件的标签是错误的,浏览器不会进行编译-->
  <!--<cpn1></cpn1>-->
</div>

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


  // 2.创建第二个组件构造器(父组件)
  const cpnC2 = Vue.extend({
    template: `
      <div>
        <h2>我是标题2</h2>
        <p>我是内容, 呵呵呵呵</p>
        <cpn1></cpn1>
      </div>
    `,
    //将 cpnC1组件注册到cpnC2组件中
    components: {
      cpn1: cpnC1
    }
  })

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

e、注册组件语法糖

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

<script>
  // 1.全局组件注册的语法糖
  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>

f、组件模板的分离写法

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

  1. 使用
<div id="app">
  <cpn></cpn>
  <cpn></cpn>
  <cpn></cpn>
</div>

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

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

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

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

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

g、组件数据存放

一、 组件访问Vue实例数据

组件不能直接访问Vue实例中的data,Vue组件应该有自己保存数据的地方

image-20201128165754797

二、组件数据存放的方式

存放到组件对象的data属性,data属性需要是一个函数

image-20201128174408824

三、data为什么是一个函数

因为对象是一个引用数据类型,如果data是一个对象的情况下会造成所有组件共用一个data。(址传递)

而当data是一个函数的情况下,每次函数执行完毕后都会返回一个新的对象,这样的话每个组件都会维护一份独立的对象(data)

官网的解释为:

data必须是一个函数

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

data: {
  count: 0
}

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

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

若直接提供对象,则是值传递,不能保证每个组件都维护一份独立的对象

如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例

h、父子组件通信

image-20201129130605753

一、父级向子级传递

props的值有两种方式:

  1. 字符串数组,数组中的字符串就是传递时的名称(key)
  2. 对象,对象可以设置传递时的类型,也可以设置默认值等

下面是数组

<div id="app">
  <!--若不使用动态绑定,则只会赋值为字符串-->
  <!--<cpn cmovies="movies" cmessage="message"></cpn>-->

  <!--动态绑定-->
  <cpn :cmessage="message" :cmovies="movies"></cpn>
</div>

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

<script src="../js/vue.js"></script>
<script>
  // 父传子: props
  const cpn = {
    template: '#cpn',
    props: ['cmovies', 'cmessage'],
    data() {
      /*此处必须有返回,不然会报错*/
      return {}
    },
  }

  //root组件
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊',
      movies: ['海王', '海贼王', '海尔兄弟']
    },
    //子组件
    components: {
      /*对象的增强语法,key是上面定义的cpn变量名,value是cpn变量的值*/
      cpn
    }
  })
</script>

对象:

<div id="app">
   <!--cmovies可以不赋值,因为有默认值,而cmessage必须赋值,因为设置了required-->
  <cpn :cmessage="message" :cmovies="movies"></cpn>
</div>

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

<script src="../js/vue.js"></script>
<script>
  const cpn = {
    template: '#cpn',
    props: {
      // 1.只定义类型限制
      // cmovies: Array,
      // cmessage: String,

      // 2.提供一些默认值, 以及必传值
      cmessage: {
        //定义类型是String
        type: String,
        //默认值为 aaaaaaa
        default: 'aaaaaaaa',
        //是否为必须传值
        required: true
      },
      // 类型是对象或者数组时, 默认值必须是一个函数
      cmovies: {
        type: Array,
        default() {
          return []
        }
      }
    },
  }

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

验证也支持自定义的类型

image-20201208235805341

image-20201208235823020

二、父传子(props中的驼峰标识)

若在组件调用时,用普通的驼峰命名法,vue不能识别

<div id="app">
  <!--若用 :CInfo,则不会被渲染出来-->
  <!--驼峰命名加上 - 即可-->
  <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: {
      //用全部小写也可以,驼峰命名较为麻烦
      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>

三、子传父($emit)

<!--父组件模板-->
<div id="app">
  <!--这个地方也不能用驼峰标识-->
  <!--此处若不写参数则默认传入 $event -->
  <cpn @item-click="cpnClick($event)"></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) {
        // 发射事件: 自定义事件
        //子传父,item就是传递的数据
        this.$emit('item-click', item)
      }
    }
  }

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

image-20201211173226991

四、综合案例

输入框中输入数据,将此值修改为父组件的num1,2和子组件的 number1,2,并且 num1*100 = num2

<body>
<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">-->
    <input type="text" :value="dnumber1" @input="num1Input">
    <h2>props:{{number2}}</h2>
    <h2>data:{{dnumber2}}</h2>
<!--    <input type="text" v-model="dnumber2">-->
    <input type="text" :value="dnumber2" @input="num2Input">
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      num1: 1,
      num2: 0
    },
    methods: {
      num1change(value){
        this.num1 = parseFloat(value);
      },
      num2change(value){
        this.num2 = parseFloat(value);
      }
    },
    components:{
      cpn: {
        template: '#cpn',
        props: {
          /*应该用父组件进行修改,不应该直接修改
          错误提示:避免直接改变属性,因为每当父组件重新渲染时,
          该值都将被覆盖。相反,请使用根据此属性值的数据或计算属性修改
           */
          number1: Number,
          number2: Number
        },
        data(){
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        methods:{
          num1Input(event){
            this.dnumber1 = event.target.value;
          // 发射事件,改变父组件中的num1
            this.$emit('num1change',this.dnumber1);
          // 同时修饰dnumber2的值
            this.dnumber2 = this.dnumber1 *100;
            this.$emit('num2change',this.dnumber2);
          },
          num2Input(event){
            this.dnumber2 = event.target.value;
            this.$emit('num2change',this.dnumber2);
            this.dnumber1 = this.dnumber2 / 100;
            this.$emit('num1change',this.dnumber1);
          },
        }
      }
    }
  })
</script>
</body>

利用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,
        },
        data() {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        //watch:当数据发生改变时启动,参数newValue表示新改变的数据
        watch: {
          dnumber1(newValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue);
          },
          //在上面 this.dnumber2 = newValue * 100; 改变了dnumber2 的值,所以会直接调用此处
          dnumber2(newValue) {
            this.number1 = newValue / 100;
            this.$emit('num2change', newValue);
          }
        }
      }
    }
  })
</script>

五、父子组件的访问方式

1、父组件直接访问子组件

使用$children

this.$children是一个数组类型,它包含所有子组件对象。可以通过一个遍历,取出所有子组件的message状态

image-20210109150148265

children的缺陷:

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

关于$refs的使用:

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

image-20210109151009127

<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();
        }*/

        // 2.$refs => 对象类型, 默认是一个空的对象 通过ref='aaa'给子组件绑定特定的ID:aaa
        console.log(this.$refs.aaa.name);
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            name: '我是子组件的name'
          }
        },
        methods: {
          showMessage() {
            console.log('showMessage');
          }
        }
      },
    }
  })
</script>
2、子组件直接访问父组件

$parent

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

image-20210109152754039

3、子组件访问根组件

利用$root,用法和与上面类似

07、插槽slot

组件的插槽也是为了让我们封装的组件更加具有扩展性

抽取共性,保留不同:将共性抽取到组件中,将不同暴露为插槽

a、slot的基本使用

<!--
1.插槽的基本使用 <slot></slot>
2.插槽的默认值 <slot>button</slot>
3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
-->


<div id="app">
<!--使用默认值-->
  <cpn></cpn>

<!--替换元素-->
  <cpn><span>哈哈哈</span></cpn>
<!--替换元素-->
  <cpn><i>呵呵呵</i></cpn>
<!--使用默认值-->
  <cpn></cpn>
</div>


<template id="cpn">
  <div>
    <h2>我是组件</h2>
    <!--插槽-->
    <slot><button>按钮</button></slot>
    <hr/>
  </div>
</template>

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

image-20210109184812401

b、具名插槽的使用

<div id="app">
  <cpn><span slot="center">标题</span></cpn>
  <cpn><button slot="left">返回</button></cpn>
</div>


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

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

image-20210109185228039

c、作用域插槽

学习作用域插槽我们需要先知道什么是编译作用域

根据官方的解释:在该模板的所有东西都会在此模板的作用域内编译

image-20210110124800261

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

  <cpn>
    <!--目的是获取子组件中的pLanguages-->
    <!--从子组件传过来的data值为 pLanguages 的数据将会被slot-scope接收,
    需要注意的是,slot-scope接收的是一个对象,这里命名为slot,
    也就是说你传过来的data将会作为slot的一个属性,所以需要slot.data才能调用到data-->
    <template slot-scope="slot">
      <!--<span v-for="item in slot.data"> - {{item}}</span>-->
      <!--用上行代码,前面会多出一个 - ,解决此问题可以利用join函数,将字符串用 - 分隔开-->
      <span>{{slot.data.join(' - ')}}</span>
    </template>
  </cpn>

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

<template id="cpn">
  <div>
    <!--子组件中定义data数据,并且绑定到slot上-->
  <slot :data="pLanguages">
      <ul>
        <li v-for="item in pLanguages">{{item}}</li>
      </ul>
   </slot>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    },
    components: {
      cpn: {
        template: '#cpn',
        data() {
          return {
            pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift'],
          }
        }
      }
    }
  })
</script>

08、简单模块化开发

a、为什么要有模块化

在早期的网页开发中,为了应对代码量的剧增,通常会将代码组织到多个js文件中进行维护。

但是有很多问题,比如全局变量同名问题:

image-20210114120749907

另外,这种代码的编写方式对js文件的依赖顺序几乎是强制性的

但是当js文件过多,比如有几十个的时候,弄清楚它们的顺序是一件比较费事的事情。

而且即使你弄清楚顺序了,也不能避免上面出现的这种尴尬问题的发生

我们可以使用匿名函数来解决此类问题,但很多代码变得不可复用

image-20210114121315612

我们可以使用模块化雏形来解决此问题,只是简单的封装而已

image-20210114122830390

b、CommonJS规范(了解)

CommonJS的导出和导入,但需要在node环境中才能使用

image-20210114123843716

//aaa.js
function sum(num1, num2) {
  return num1 + num2
}

var flag = true

if (flag) {
  console.log(sum(10, 20));
}
//将flag和sum函数导出
module.exports = {
  flag: flag,
  sum: sum
}


//在mmm.js中导入
var aaa = require('./aaa.js')
var flag = aaa.flag;
var sum = aaa.sum;
//上面三行等用于
var {flag, sum} = require('./aaa.js')

c、ES6模块化

使用export(导出)和import(导入),引用的时候需要类型为 model

<script src="aaa.js" type="module"></script>
<script src="bbb.js" type="module"></script>
<script src="mmm.js" type="module"></script>

两种导出方式

var age = 18
var flag = true

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

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

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

导入:

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

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

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

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

导出类或函数

export function mul(num1, num2) {
  return num1 * num2
}

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

导入

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

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

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

导出 default 。export default在同一个模块中,不允许同时存在多个

//const address = '北京市'

//export default address

export default function (argument) {
  console.log(argument);
}

导入:

// 4.导入 export default中的内容,且导入的时候不用加{} 
import addr from "./aaa.js";

addr('你好啊');

如果我们希望某个模块中所有的信息都导入,一个个导入显然有些麻烦,可以通过通配符 * 来导入模块中的所有 export 变量

通常为了方便使用,我们会用 as xxx 给 * 取一个别名xxx

// 5.统一全部导入
// import {flag, num, num1, height, Person, mul, sum} from "./aaa.js";
import * as aaa from './aaa.js'

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

09、webpack详解

从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具

image-20210119220837414

a、webpack安装

image-20210119221207976

b、webpack起步

dist文件夹:用于存放之后打包的文件(输入命令后自动生成 webpack src/main.js dist/bundle.js 需要在当前目录输入此命令,即在01-webpack的起步目录)

src文件夹:用于存放我们写的源文件

ppackage.json:通过 npm init 生成的,npm包管理的文件

image-20210119224808891

info.js

export const name = 'why';
export const age = 18;
export const height = 1.88;

mathUtis.js

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

function mul(num1, num2) {
  return num1 * num2
}
//导出 add mul
module.exports = {
  add,
  mul
}

main.js

// 1.使用commonjs的模块化规范
const {add, mul} = require('./mathUtils.js')

console.log(add(20, 30));
console.log(mul(20, 30));

// 2.使用ES6的模块化的规范
import {name, age, height} from "./info";

console.log(name);
console.log(age);
console.log(height);

如果直接在index.html引入这两个js文件,浏览器并不识别其中的模块化代码。

另外,在真实项目中当有许多这样的 js 文件时,我们一个个引用非常麻烦,并且后期非常不方便对它们进行管理

使用webpack打包工具生成的 bist目录中的 bundle.js文件,只打包 main.js 即可,webpack会自动将 main.js 中的引用也打包

D:\01_Java\00_Java_project\12_vue\14-webpack使用\01-webpack的起步>webpack src/main.js dist/bundle.js

index.html:在 index.html 中引用这个bundle.js 文件即可

<body>
<script src="./dist/bundle.js"></script>
</body>

image-20210119233657102

c、webpack配置

image-20210120224858282

首先应该使用 npm init 初始化项目 会生成package.json 文件

然后使用npm install 将 packge.json 所依赖的东西安装

一、入口和出口

如果每次使用 webpack 的命令都需要写上入口和出口作为参数,就非常麻烦,可以利用 webpack.config.js 文件

//导入node自带的path模块,用以获取文件前缀目录
const path = require('path')

module.exports = {
  //入口
  entry: './src/main.js',
  //出口
  output: {
    //resolve函数表示拼接,__dirname表示当前文件的所在的路径,即将dist拼接到当前路径中
    path: path.resolve(__dirname, 'dist'),  //此处要写绝对路径
    filename: 'bundle.js'                   //出口文件名
  },
}

二、局部安装webpack

因为一个项目往往依赖特定的webpack版本,全局的版本可能很这个项目的webpack版本不一致,导出打包出现问题

所以一个项目安装一个局部的 webpack 是十分必要的

第一步:局部安装开发版本的webpack

npm install webpack@3.6.0 --save-dev

image-20210120182513052

三、自定义执行脚本

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

  • 首先,会寻找本地的 node_modules/.bin 路径中对应的命令。
  • 如果没有找到,会去全局的环境变量中寻找

image-20210120170126259

和直接终端输入不同,输入npm run xxx 是优先在局部安装的 webpack 中使用

此处添加 build 将 webpack 替换为 npm run build 命令

d、webpack的loader

在我们之前的实例中,我们主要是用webpack来处理我们写的js代码,并且webpack会自动处理js之间相关的依赖。

但是,在开发中我们不仅仅有基本的js代码处理,我们也需要加载css、图片,也包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等。

对于webpack本身的能力来说,对于这些转化是不支持的,需要给webpack扩展对应的loader

nloader使用过程:

  1. 步骤一:通过npm安装需要使用的loader
  2. 步骤二:在 webpack.config.js 中的modules关键字下进行配置

大部分loader我们都可以在webpack的官网中找到,并且学习对应的用法

一、css-loader

image-20210120193002734

normal.css

body {
   background-color: red;
}

我们需要在 main.js 中引用它才可以找到它

// 3.依赖css文件
require('./css/normal.css')

然后我们进行打包发现错误,提示 normal.css 必须有对应的loader

在webpack的官方(https://webpack.docschina.org/loaders/)中,我们可以找到如下关于样式的loader使用方法:

按照官方配置 webpack.config.js 文件,首先安装 css-loaderstyle-loader

css-loader 和 style-loader 缺一不可,一个负责将 css 加载,一个负责将样式添加到 DOM 中

//npm install --save-dev css-loader
//需要注意的是,本地安装的是webpack3.6.0,默认的css-loader版本过高,需要使用 3.0版本的,否则会出现无法解析的错误
npm install css-loader@3.0.0 --save-dev
npm install --save-dev style-loader

webpack.config.js

const path = require('path')

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      {
        //正则表达式
        test: /\.css$/,
        // css-loader只负责将css文件进行加载,若只有css-loader 则不会加载出样式
        // style-loader负责将样式添加到DOM中
        // 使用多个loader时, 是从右向左的顺序读取的,如果顺序错误会直接报错
        use: [ 'style-loader', 'css-loader' ]
      }
    ]
  }
}

package.json

  "devDependencies": {
    "css-loader": "^3.0.0",
    "style-loader": "^2.0.0",
    "webpack": "^3.6.0"
  }

二、less-loader

image-20210122170508613

首先创建less文件

@fontSize: 50px;
@fontColor: orange;

body {
  font-size: @fontSize;
  color: @fontColor;
}

然后在main中引用

require('./css/special.less')

less会被loader解析为css,则需要 less-loader、css-loader、style-loader,另外还需要安装 less(因为 webpack 会使用 less 对 less 文件编译)

package.json

  "devDependencies": {
    "css-loader": "^2.0.2",
    "less": "^3.9.0",
    "less-loader": "^4.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^3.6.0"
  }

webpack.config.js

module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ]
      },
      {
        test: /\.less$/,
        use: [{
          loader: "style-loader", // 从JS字符串创建样式节点
        }, {
          loader: "css-loader" // 将 CSS 编译为 less
        }, {
          loader: "less-loader", // 将 less 编译为 CSS
        }]
      },
      {
        test: /\.(png|jpg|gif)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192
            }
          }
        ]
      }
    ]
  }

三、url-loader和file-loader

image-20210122174447518

normal.css中,将背景换为图片 test.jpg小于13kb,timg.jpg大于13kb

body{
    /*background-color: red;*/
    background: url("../img/test.jpg");
}

package.json

  "devDependencies": {
    "css-loader": "^2.0.2",
    "file-loader": "^3.0.1",
    "less": "^3.9.0",
    "less-loader": "^4.1.0",
    "style-loader": "^0.23.1",
    "url-loader": "^1.1.2",
    "webpack": "^3.6.0"
  }

图片处理,url-loader来处理,依然先安装 url-loader

然后修改webpack.config.js 配置文件

 {
        test: /\.(png|jpg|gif|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {
              // 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
              // 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
              limit: 13312 //13kb 
            }
          }
        ]
      }

然后发现可以显示出来:背景图片为:text.jpg

image-20210123150323400

如果换为timg.jpg(大于13kb),发现报错

image-20210123150528659

提示需要 file-loader,下面我们安装

npm install --save-dev file-loader

然后修改webpack.config.js

      {
        test: /\.(png|jpg|gif|jepg)$/,
        use: [
          {
            loader: 'file-loader',
            options: {
              //图片文件处理-修改文件名称
              name: 'img/[name].[hash:8].[ext]'
            }
          }
        ]
      }

发现还是报错:

image-20210123150747265

此处file-loader需要降级到3.0.1,其实正常开发 url 和 file loader一般只留一个,否则很容易出现图片无法正常加载的问题。

卸载原本安装的 file-loader

npm uninstall --save-dev file-loader

然后安装 3.0.1 的版本

npm install --save-dev file-loader@3.0.1 

关于图片文件名称修改 和 图片路径添加 dist/

image-20210123155428010

这样就可以显示出来了

image-20210123151353686

四、ES6转ES5用babel

webpack打包的 js 文件中,ES6的语法并没有转换为ES5的语法,这意味着可能一些对ES6还不支持的浏览器没有办法完全运行代码

要讲ES6的语法转换为ES5的,需要babel

在webpack中,直接使用babel对应的loader即可

npm install --save-dev babel-loader@7 babel-core babel-preset-es2015

webpack.config.js

      {
        test: /\.js$/,
        // exclude: 排除
        // include: 包含
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['es2015']
          }
        }
      },

然后重新打包,发现其中的内容都变成了ES5的语法

五、webpack配置vue

在项目中使用Vuejs,那么必然需要对其有依赖,所以需要先进行安装

因为后续在实际项目中也会使用vue,所以并不是开发时依赖

npm install vue --save

image-20210126130756476

导入vue依赖包,然后打包之后运行,浏览器中报错,错误提示时我们默认使用的是runtime-only版本的Vue

runtime-only 其实只能识别 render 函数,不能识别 template,.vue文件中的也是被 vue-template-compiler 翻译成了render函数,所以只能在.vue里写 template

我们修改webpack.config.js,添加如下内容,在model之外添加

 resolve: {
    //扩展名
    extensions: ['.js', '.css', '.vue'],
    //别名,使用vue.esm.js版本的Vue,基于构建工具使用的完整版
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
1、el和template的区别

正常运行之后,又出现了另一个问题

  1. 如果我们希望将data中的数据显示在界面中,就必须是修改index.html
  2. 如果我们后面自定义了组件,也必须修改index.html来使用组件
  3. 但是html模板在之后的开发中,并不希望手动的来频繁修改,如何做到呢?

定义template属性:

在前面的Vue实例中,我们定义了 el 属性,用于和 index.html 中的 #app 进行绑定,让Vue实例之后可以管理它其中的内容

这里,我们可以将div元素中的{{message}}内容删掉,只保留一个基本的 id 为div的元素

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

但是如果我依然希望在其中显示{{message}}的内容,应该怎么处理呢?

image-20210126211825878

Vue实例中同时指定了template,那么template模板的内容会替换掉挂载的对应 el 的模板

就像下面动图中展示的那样:template模板的内容会替换掉挂载的对应el的模板

这样做之后我们就不需要在以后的开发中再次操作index.html,只需要在template中写入对应的标签即可

对于上面的代码,书写 template 模块非常麻烦怎么办?

使用组件化的思想,我们可以将template模板中的内容进行抽离。

main.js

//使用vue进行开发
import Vue from 'vue'

// 1.抽离出一个组件
let App = {
  template: `
    <div>
      <h2>{{message}}</h2>
      <button @click="btnClick">按钮</button>
    </div>
  `,
  data() {
    return {
      message: 'hello world'
    }
  },
  methods: {
    btnClick() {
      console.log('---');
    }
  }
}

let app = new Vue({
  el: '#app',
  template: '<App></App>', // 3.使用
  components:{ // 2.在Vue根实例中进行注册
    App
  }
})

这样 main.js 看起来不简洁,我们可以将代码抽取到一个js文件中,并且导出,在main.js中导入

创建一个app.js文件

export default {
  template: `
    <div>
    <h2>{{ message }}</h2>
    <button @click="btnClick">按钮</button>
    </div>
  `,
  data() {
    return {
      message: 'hello world'
    }
  },
  methods: {
    btnClick() {
      console.log('---');
    }
  }
}

然后在 main.js 文件中进行引入

//使用vue进行开发
import Vue from 'vue'

import App from './vue/app'

let app = new Vue({
  el: '#app',
  template: '<App></App>',
  components:{
    App
  }
})

但是上边的代码中,一个组件以一个 js 对象的形式进行组织和使用的时候是非常不方便的

  • 一方面编写template模块非常的麻烦
  • 另外一方面如果有样式的话,我们写在哪里比较合适呢?

现在,我们以一种全新的方式来组织一个vue的组件:创建一个 App.vue 文件

2、.vue文件封装处理

安装 vue-loader 和 vue-template-compiler

npm install vue-loader vue-template-compiler --save-dev

webpack.config.js

      {
        test: /\.vue$/,
        use: ['vue-loader']
      }

完整webpack.config.js

const path = require('path')

module.exports = {
  entry: './src/main.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    publicPath: 'dist/'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ]
      },
      {
        test: /\.vue$/,
        use: ['vue-loader']
      }
    ]
  },
  resolve: {
    //扩展名
    extensions: ['.js', '.css', '.vue'],
    //别名,使用vue.esm.js版本的Vue,基于构建工具使用的完整版
    alias: {
      'vue$': 'vue/dist/vue.esm.js'
    }
  }
}

package.json

{
  "name": "vuep",
  "version": "1.0.0",
  "description": "",
  "main": "webpack.config.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "vue": "^2.6.12"
  },
  "devDependencies": {
    "css-loader": "^2.0.2",
    "style-loader": "^2.0.0",
    "vue-loader": "^13.0.0",     //若使用的vue-loader是大于15.0.0的,需要额外安装插件使用
    "vue-template-compiler": "^2.6.12",
    "webpack": "^3.6.0"
  }
}

需要注意的是,若使用的vue-loader是大于15.0.0的,需要额外安装插件使用

e、plugin的使用

plugin是插件的意思,通常是对某个现有的框架进行扩展

webpack中的插件,就是对webpack现有功能的各种扩展,比如打包优化,文件压缩等

loader和plugin区别

  1. loader主要用于转换某些类型的模块,它是一个转换器
  2. plugin是插件,它是对webpack本身的扩展,是一个扩展器

plugin的使用过程:

  • 步骤一:通过npm安装需要使用的plugins(某些webpack已经内置的插件不需要安装)
  • 步骤二:在webpack.config.js中的plugins中配置插件

一、横幅注释插件

BannerPlugin是在 dist/bundle.js 文件的头部添加横幅注释的插件,属于webpack自带的插件

webpack.config.js

image-20210131111225662

打包程序后,查看bundle.js

image-20210131111251779

二、打包html的插件

目前,我们的index.html文件是存放在项目的根目录下的。

我们知道,在真实发布项目时,发布的是dist文件夹中的内容,但是dist文件夹中如果没有index.html文件,那么打包的js等文件也就没有意义了。

所以,我们需要将index.html文件打包到dist文件夹中,这个时候就可以使用 HtmlWebpackPlugin 插件

HtmlWebpackPlugin插件可以为我们做这些事情:

  1. 在 dist 文件夹自动生成一个index.html文件(可以指定模板来生成)
  2. 将打包的js文件,自动通过script标签插入到body中

安装HtmlWebpackPlugin插件,默认生成的版本过高,需要加上指定版本

npm install html-webpack-plugin --save-dev@3.2.0

image-20210131114250560

安装后打包,这是 不使用模板 自动生成的 index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Webpack App</title>
  </head>
  <body>
  <script type="text/javascript" src="dist/bundle.js"></script>
  </body>
</html>

可以看出,自动插入了 script 标签

但是没有使用

这一标签

我们可以给他加一个模板属性:template

new htmlWebpackPlugin({
        template: 'index.html'
      })

这样我们原本在项目中写的index.html就会作为模板生成 dist/index.html

项目中作为模板的 index.html,不用导入script标签了,HtmlWebpackPlugin 插件会自动为 dist/index.html 导入

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

重新打包,生成的dist目录下的index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app"></div>
<script type="text/javascript" src="dist/bundle.js"></script></body>
</html>

观察项目结构,发现 dist 目录下的 index.html 中,script标签中的 src 会出现路径错误,正确路径应该直接为 bundle.js

image-20210131114734858

所以我们需要删除 webpack.config.js 中 output 属性的 publicPath

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    //publicPath: 'dist/'
  },

三、压缩JS的插件

在项目发布之前,我们通常需要对js等文件进行文件压缩

我们使用一个第三方的插件uglifyjs-webpack-plugin,并且版本号指定1.1.1,和CLI2保持一致

npm install uglifyjs-webpack-plugin@1.1.1 --save-dev

修改webpack.config.js文件,使用插件:

image-20210131121818534

查看打包后的 bundle.js 发现已经打包完成,删除了空行,空格,注释等,变量名也变得更加简洁

f、搭建本地服务器

首先安装模块

npm install --save-dev webpack-dev-server@2.9.1

配置webpack.config.js

  devServer: {
    contentBase: './dist',
    inline: true
  }

devserver也是作为webpack中的一个选项,选项本身可以设置如下属性:

  • contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
  • port:端口号
  • inline:页面实时刷新
  • historyApiFallback:在SPA页面中,依赖HTML5的history模式

可以再加上一个 script

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    //"dev": "webpack-dev-server"
    //加上 --open  命令行输入npm run dev 后会自动在浏览器打开网址,不需要手动点击
    "dev": "webpack-dev-server --open"
  },

g、webpack配置分离

image-20210220220850198

安装配置文件合并工具

npm install webpack-merge@4.1.5 --save-dev

base.config.js

//公共部分的配置

const path = require('path')
//html打包插件
const htmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    entry: './src/main.js',
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: 'bundle.js',
        //publicPath: 'dist/'
    },
    module: {
        rules: [
            {
                test: /\.css$/,
                use: [ 'style-loader', 'css-loader' ]
            },
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },
    resolve: {
        //扩展名
        extensions: ['.js', '.css', '.vue'],
        //别名,使用vue.esm.js版本的Vue,基于构建工具使用的完整版
        alias: {
            'vue$': 'vue/dist/vue.esm.js'
        }
    },
    plugins: [
        new htmlWebpackPlugin({
            template: 'index.html'
        })
    ],
}

dev.config.js

//开发时依赖
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')

//合并,将baseConfig与后面代码合并
module.exports = webpackMerge(baseConfig,{
    devServer: {
        contentBase: './dist',
        //自动刷新
        inline: true
    }
})

因为删除了 webpack.config.js 所以需要手动修改配置文件的位置

  "dev": "webpack-dev-server --open --config ./build/dev.config.js"

这次打包到了build文件夹中的dist文件夹,与之前不同,因为配置里面默认在当前文件夹下创建build文件夹

即:

  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    //publicPath: 'dist/'
  },

修改配置文件

    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: 'bundle.js',
        //publicPath: 'dist/'
    },

然后就会打包到项目第一级目录下的build文件夹

10、Vue CLI

Vue CLI 使用前提需要安装 8.9以上版本的Node和Webpack

a、安装和初始化Vue脚手架

npm install -g @vue/cli

默认安装的是Vue CLI3以上的版本,如果想按照Vue CLI2 的方式初始化项目是不可以的

可以安装Vue CLI3版本后,再拉取2.x的模板,全局安装一个桥接工具

image-20210307165858203

Vue CLI2初始化项目

  vue init webpack 项目名称

Vue CLI3初始化项目

vue create 项目名称

b、VueCLI 2 详解

image-20210307170139598

一、目录结构详解

image-20210307170334614

二、Runtime-Compiler 和 Runtime-Only 的区别

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

不同项目使用两种版本的 main.js

image-20210307170947290

其实 h 就是 createElement 的简写

可以看出runtime-compiler模式里的Vue实例的模板,和注册的组件,都被一个render函数替换掉了

template在vue内部是怎么样渲染成页面的?

在这里插入图片描述

由上图可知,template会被解析成ast,再被编译成一个render函数,这个render函数会构造一个virtual dom(虚拟dom),最后virtual dom会转换为真实dom,进行页面展示。

runtime-compiler的步骤:
template -> ast -> render -> virtual dom -> 真实dom

runtime-only的步骤
render -> virtual dom -> 真实dom

使用runtime-only虽然不能够自己编译template模板,但是因为之前安装的插件vue - template - compiler ,系统内部会自动编译template模板

三、rander函数的使用

image-20210307171743020

四、路径别名

image-20210706163245969

这个路径别名在import导入的时候可以直接用,但是在其他地方前面需要加上~(如写 src 的时候)

image-20210706163704718

c、VueCLI3 详解

vue-cli 3 与 2 版本有很大区别

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

创建时在命令行输入:

vue create 项目名称

image-20210307172554195

设置保存的地址: C:\Users\季秋.vuerc

一、目录结构详解

image-20210307173649055

image-20210307174205764

二、配置文件

在VueCLI3中,配置文件不需要那么麻烦,启动配置服务器 :命令行输入 vue ui

image-20210307173824780

image-20210307173736902

在 15-vuecli3-text\node_modules@vue\cli-service\lib\Service.js 中

如果需要更改配置,手动创建一个 vue.config.js 文件,然后将手动修改的配置导出,Service.js会自动进行合并

11、vue-router

a、认识路由

路由是一个网络工程里面的术语

路由就是通过互联的网络把信息从源地址传输到目的地址的活动

b、网站发开阶段

一、后端路由阶段

image-20210508181501545

后端路由的缺点:

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

后端路由:

  • 前端每次跳转到不同的url地址,都会重新访问服务器
  • 服务器根据前端的路由,返回不同的数据,或者是HTML页面

二、前后端分离阶段

image-20210508182632649

前后端分离阶段:

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

三、前端路由阶段

前端路由阶段也就是单页面富应用阶段(SPA)

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

前端路由的核心:改变URL,但是页面不进行整体的刷新

c、前端路由的规则

一、URL的hash

URL的hash也就是锚点, 本质上是改变window.location的href属性

我们可以通过直接赋值location.hash来改变href, 虽然网址发生了变化,但是页面不发生刷新

img

可以看出,更改了URL,但是并没有刷新页面

二、HTML5的 history 模式:

pushState

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

history.pushState()

这种方法类似于压栈,若用history.back会弹出最晚进栈的那一个URL

image-20210508204830381

replaceState 替换网址,与pushState不同的是,不可返回

image-20210508210720895

history.go()

history.back

history.forward()

image-20210508210934314

d、vue-router基础

一、认识vue-router

vue-router是基于路由和组件的

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

image-20210516182726157

创建vue-router实例

index.js

import Vue from 'vue'
import VueRouter from 'vue-router'


//1.通过Vue.use(插件),安装插件
Vue.use(VueRouter)

//2.创建VueRouter对象
const routes = [{
  path:xxx
  components:xxx
}]
const router = new VueRouter({
  //配置路由和组件之间的应用关系
  routes,

})

//3.将router对象导出,传入到Vue实例
export default router

image-20210516182958877

创建路由组件

image-20210516183014849

image-20210516183029809

使用路由

image-20210516183108652

image-20210516183117576

二、细节处理

①、路由的默认路径

配置多一个映射即可,利用重定向


//2.创建VueRouter对象
const routes = [
  {
    path: '/',
    //重定向,设置路由的默认路径
    redirect: '/home'
  },
  {
    path: '/home',
    component: Home
  },
  {
    path: '/about',
    component: About
  }
]
②、HTML5的history模式

image-20210516183358147

③、router-link的补充

image-20210516183612057

④、修改linkActiveClass

image-20210516183917370

还可以利用此性质,使其点击哪个地方哪个变红

<style>
.Active{
  color: #f00;
}
</style>

image-20210523180528003

⑤、路由代码跳转

image-20210516184058740

⑥、动态路由

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

/user/aaaa或/user/bbbb,除了有前面的/user之外,后面还跟上了用户的ID

这种path和Component的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)

User.Vue

<template>
<div>
  <p>用户</p>
  <h2>用户用户用户</h2>
  <h2>{{userId}}</h2>
</div>
</template>

<script>
export default {
	name: "User",
  //计算属性
  computed: {
    userId() {
      //利用当前路由的对象中的params方法接收参数
      return this.$route.params.id
    }
  }
}
</script>

<style scoped>

</style>

index.js中添加

  {
    //动态路由,后面跟上匹配的ID
    path: '/user/:id',
    component: User
  }

App.vue

<template>
  <div id="app">
    <router-link to="/home">首页</router-link>
    <router-link to="/about">关于</router-link>
    <!--此处可以利用v-bind: 简写为: 动态绑定-->
    <router-link :to="'/user/'+id">用户</router-link>
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  name: 'App',
  data() {
    return {
      id: 'lisi'
    }
  }
}
</script>

image-20210527000242291

e、路由懒加载

打包构建应用时,Javascript 包会变得非常大,影响页面加载。

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

路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块,只有在这个路由被访问到的时候, 才加载对应的组件

image-20210529110554285

f、路由嵌套

image-20210529141030455

image-20210529200328189

image-20210529200913809

打开网页

image-20210530230626995

路由嵌套的默认路径

image-20210531163547093

g、传递参数

image-20210531173958917

准备工作

image-20210531163625061

传递参数方式一:

image-20210531172843321

传递参数二:利用JavaScript

image-20210531173448797

参数获取通过$route.query/param

h、 $router与route的区别

我们在项目中使用了vue-router,这时,当前项目的每一个vue组件,都会自动获取到一个$route数据,这个数据中存放的是匹配当前路由的信息对象,可以通过它,得到当前的动态路由参数 Params 或查询参数 Query

$route对象表示的是当前路由信息

$router对象是全局路由的实例,是router构造方法的实例,可以使用push,go等方法

i、导航守卫

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

网页标题是通过来显示的, 但是SPA只有一个固定的HTML, 切换不同的页面时, 标题并不会改变.

但是我们可以通过JavaScript来修改的内容 window.document.title = ‘新的标题’.

那么在Vue项目中, 在哪里修改? 什么时候修改比较合适呢?

普通的修改方式:

我们比较容易想到的修改标题的位置是每一个路由对应的组件.vue文件中.

通过mounted生命周期钩子, 执行对应的代码进行修改即可,例如在创建回调函数时更改标题

但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码).

有没有更好的办法呢? 使用导航守卫即可.

vue-router提供的导航守卫主要用来监听监听路由的进入和离开的.

vue-router提供了beforeEach和afterEach的钩子函数, 它们会在路由即将改变前和改变后触发.

我们可以使用beforeEach来完成标题的修改,在生命周期执行函数之前执行的函数
首先, 我们可以在钩子当中定义一些标题, 可以利用meta来定义
其次, 利用导航守卫,修改我们的标题

导航钩子的三个参数解析:
to: 即将要进入的目标的路由对象.
from: 当前导航即将要离开的路由对象.
next: 调用该方法后, 才能进入下一个钩子,beforeEach必须调用,afterEach中函数内部自身调用,不用调用,所以后置钩子只有两个参数,to和from

image-20210601232439771

j、keep-alive保存组件状态

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

它们有两个非常重要的属性:

  1. include - 字符串或正则表达,只有匹配的组件会被缓存
  2. exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存

点击首页的非默认嵌套路由,再点击其他页面,再点击首页的时候,页面不变,即:

QQ录屏20210703121221

// 这两个函数, 只有该组件被保持了状态使用了keep-alive时, 才是有效的,与生命周期函数写到一层
//当前路由活跃,调用此函数
    activated() {
      this.$router.push(this.path);
      console.log('activated');
    },

//当前路由不活跃,调用此函数
  /*  deactivated() {
      console.log('deactivated');
    },*/

//记录离开前的path
    beforeRouteLeave (to, from, next) {
      console.log(this.$route.path);
      this.path = this.$route.path;
      next()
    }

image-20210601234916283

被keep-alive包含的组件会被缓存

// 只缓存组件name为a或者b的组件,这个地方逗号后面不能随便加空格
<keep-alive include="a,b"> 
 	  <router-view/>
</keep-alive>

// 组件name为c的组件不缓存(可以保留它的状态或避免重新渲染)
<keep-alive exclude="c"> 
	  <router-view/>
</keep-alive>

// 如果同时使用include,exclude,那么exclude优先于include, 下面的例子只缓存a组件
<keep-alive include="a,b" exclude="b"> 
	  <router-view/>
</keep-alive>

// 如果缓存的组件超过了max设定的值5,那么将删除第一个缓存的组件
<keep-alive exclude="c" max="5"> 
	  <router-view/>
</keep-alive>

12、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’} : {}

完成图片效果:

image-20210705115531419

项目基本结构

image-20210705120850146

image-20210705185240303

APP.vue

<template>
  <div id="app">
<!--插槽调用-->
    <tab-bar>
      <tab-bar-item>
        <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>
  /*
  base.css中只写了bady这一个样式
  
  body {
  padding: 0;
  margin: 0;
}
  
  */
  @import "./assets/css/base.css";
</style>

功能截图

image-20210705190329635

实现下一步,利用利用跳转页面,并且点击下方视图导航图片变色

功能结构截图

image-20210706101524072

TabBar.vue不改

<template>
  <div id="tab-bar">
    <slot></slot>
  </div>
</template>

<script>
  export default {
    name: "TabBar"
  }
</script>

<style scoped>
  #tab-bar {
    display: flex;
    background-color: #f6f6f6;

    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;

    box-shadow: 0 -1px 1px rgba(100,100,100,.2);
  }
</style>

TabBarItem更改

<template>
<!--  点击之后调用itemClick函数,使链接跳转-->
  <div class="tab-bar-item" @click="itemClick">
    <div v-if="!isActive"><slot name="item-icon"></slot></div>
    <div v-else><slot name="item-icon-active"></slot></div>
    <div :style="activeStyle"><slot name="item-text"></slot></div>
  </div>
</template>

<script>
  export default {
    name: "TabBarItem",
    props: {
      path: String,
      activeColor: {
        type: String,
        default: 'red'
      }
    },

    computed: {
      isActive() {
        //当前活跃路由的地址是否是this.path(由MainTabBar中的tab-bar-item中的path传入),若是,则返回true
        return this.$route.path.indexOf(this.path) !== -1
      },
      activeStyle() {
        //当前活跃路由下方字体(首页、分类...)变色
        return this.isActive ? {color: this.activeColor} : {}
      }
    },
    methods: {
      itemClick() {
        //跳转/替换 地址
        this.$router.replace(this.path)
      }
    }
  }
</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;
    vertical-align: middle;
    margin-bottom: 2px;
  }
</style>

MainTabBar填写插槽

<template>
  <tab-bar>
    <!--传入path和activeColor-->
    <tab-bar-item path="/home" activeColor="pink">
      <img slot="item-icon" src="../../assets/img/tabbar/home.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/home_active.svg" alt="">
      <div slot="item-text">首页</div>
    </tab-bar-item>
    <tab-bar-item path="/category" activeColor="pink">
      <img slot="item-icon" src="../../assets/img/tabbar/category.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/category_active.svg" alt="">
      <div slot="item-text">分类</div>
    </tab-bar-item>
    <tab-bar-item path="/cart" activeColor="pink">
      <img slot="item-icon" src="../../assets/img/tabbar/shopcart.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/shopcart_active.svg" alt="">
      <div slot="item-text">购物车</div>
    </tab-bar-item>
    <tab-bar-item path="/profile" activeColor="deepPink">
      <img slot="item-icon" src="../../assets/img/tabbar/profile.svg" alt="">
      <img slot="item-icon-active" src="../../assets/img/tabbar/profile_active.svg" alt="">
      <div slot="item-text">我的</div>
    </tab-bar-item>
  </tab-bar>
</template>

<script>
  import TabBar from 'components/tabbar/TabBar'
  import TabBarItem from 'components/tabbar/TabBarItem'

  export default {
    name: "MainTabBar",
    components: {
      TabBar,
      TabBarItem
    }
  }
</script>

<style scoped>

</style>

image-20210706115342824

13、Promise

a、Promise的基本使用

image-20210714173522364

image-20210714173539009

image-20210714173741644

  1. 如果是成功的,那么通常我们会调用 resolve(messsage),这个时候,我们后续的 then 会被回调。
  2. 如果是失败的,那么通常我们会调用 reject(error),这个时候,我们后续的 catch 会被回调。
  3. 其中data即为 resolve函数的形参,err 为 catch函数的形参
<script>
  // 使用setTimeout
  // setTimeout(() => {
  //   console.log('Hello World');
  // }, 1000)

  //改为使用Promise
  // 什么情况下会用到Promise?
  // 一般情况下是有异步操作时,使用Promise对这个异步操作进行封装
  // new -> 构造函数(1.保存了一些状态信息  2.执行传入的函数)
  // 在执行传入的回调函数时, 会传入两个参数, resolve, reject.本身又是函数
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // 成功的时候调用resolve
      // resolve('Hello World')

      // 失败的时候调用reject
      reject('error message')
    }, 1000)
  }).then((data) => {
    // 处理代码
    console.log(data);
    console.log(data);
    console.log(data);
    console.log(data);
    console.log(data);
  }).catch((err) => {
    console.log(err);
  })
</script>

另一种写法,可以省略catch,多个异步操作时看起来更加清晰

<script>
  new Promise((resolve, reject) => {
    setTimeout(() => {
      // resolve('Hello Vuejs')
      reject('error message')
    }, 1000)
    //then(函数1,函数2)  其中函数1表示成功后调用,函数2发生错误时调用
  }).then(data => {
    console.log(data);
  }, err => {
    console.log(err);
  })
</script>

b、Promise链式调用

<script>

  // 参数 -> 函数(resolve, reject)
  // resolve, reject本身它们又是函数
  // 链式编程
  new Promise((resolve, reject) => {
    
    // 第一次网络请求的代码
    setTimeout(() => {
      resolve("hello world")
    }, 1000)
  }).then((data) => {
    
    // 第一次拿到结果的处理代码
    console.log('data'); //hello world

    
    return new Promise((resolve, reject) => {
      
      // 第二次网络请求的代码
      setTimeout(() => {
        resolve(data + '111')
      }, 1000)
    })
  }).then((data) => {
    
    // 第二次处理的代码
    console.log('data'); //hello world111

    
    return new Promise((resolve, reject) => {
      
      // 第三次网络请求的代码
      setTimeout(() => {
        
        resolve(data + '222')
      })
    })
  }).then((data) => {

    // 第三处理的代码
    console.log(data); //hello world111222
    
    return new Promise((resolve, reject) => {
      
      // 第四次网络请求的代码
      setTimeout(() => {
        
        reject(data + 'error')
      })
    })
  }).then((data) => {

    // 第四处理的代码
    console.log(data); //不执行

  },(err)=> {
    console.log(err); //hello world111222error
  })
</script>

链式调用简写省略

image-20210714180026808

再简写:

image-20210714175142520

c、Promise的all方法使用

<script>
Promise.all([
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({name: 'why', age: 18})
      }, 2000)
    }), //results[0]
    new Promise((resolve, reject) => {
      setTimeout(() => {
        resolve({name: 'kobe', age: 19})
      }, 1000)
    }) //results[1]
  ]).then(results => {
  	console.log(results[0]);
    console.log(results);
  })
</script>

image-20210714180654381

14、vuex

a、vuex的基本概念

vuex,你可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面

然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用

Vuex就是为了提供这样一个在多个组件间共享状态的插件,且实现了响应式

image-20210714211124086

Vuex状态管理图例

image-20210722173738211

b、vuex的基本实现

image-20210717194115767

image-20210717194125799

image-20210717194132724

c、Vuex的核心概念

vuex 中最关键的是store对象,这是vuex的核心。可以说,vuex这个插件其实就是一个store对象,每个vue应用仅且仅有一个store对象。

store是Vuex.Store这个构造函数new出来的实例。在构造函数中可以传一个对象参数。这个参数中可以包含5个对象:

1.state – 存放状态

2.getters – state的计算属性

3.mutations – 更改状态的逻辑,同步操作

4.actions – 提交mutation,异步操作

5.mudules – 将store模块化

关于store,需要先记住两点:

  1. store 中存储的状态是响应式的,当组件从store中读取状态时,如果store中的状态发生了改变,那么相应的组件也会得到更新;
  2. 不能直接改变store中的状态。改变store中的状态的唯一途径是提交(commit)mutations。这样使得我们可以方便地跟踪每一个状态的变化。

一个完整的store的结构是这样的

const store = new Vuex.Store({
  state: {
    // 存放状态
  },
  getters: {
    // state的计算属性
  },
  mutations: {
    // 更改state中状态的逻辑,同步操作
  },
  actions: {
    // 提交mutation,异步操作
  },
  // 如果将store分成一个个的模块的话,则需要用到modules。
   //然后在每一个module中写state, getters, mutations, actions等。
  modules: {
    a: moduleA,
    b: moduleB,
    // ...
  }
});

一、state

state上存放的,说的简单一些就是变量,也就是所谓的状态。没有使用 state 的时候,我们都是直接在 data 中进行初始化的,但是有了 state 之后,我们就把 data 上的数据转移到 state 上去了。另外有些状态是组件私有的状态,称为组件的局部状态,我们不需要把这部分状态放在store中去。

const store = new Vuex.Store({
  state: {
    counter: 1000,
    students: [
      {id: 110, name: 'why', age: 18},
      {id: 111, name: 'kobe', age: 24},
      {id: 112, name: 'james', age: 30},
      {id: 113, name: 'curry', age: 10}
    ]
  }

在组件中获取store中的状态

image-20210727140457585

二、getters

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数。此时可以用到getters,getters可以看作是store的计算属性,其参数为state。

image-20210727140611400

image-20210727140620932

getters默认是不能传递参数的,如果希望传递参数, 那么只能让getters本身返回另一个函数

image-20210727140734540

获取getters里面的状态

image-20210727141420039

三、mutations

更改Vuex中的state的唯一方法是,提交mutation,即store.commit(‘increment’)

image-20210727141551878

1、mutations传递参数

image-20210727145911654

2、mutations提交风格

image-20210727150353956

3、mutations响应规则

image-20210727151023418

image-20210731190950170

下面的方式可以使state为响应式的

image-20210801170650449

4、Mutation常量类型

我们来考虑下面的问题:
在mutation中, 我们定义了很多事件类型(也就是其中的方法名称).
当我们的项目增大时, Vuex管理的状态越来越多, 需要更新状态的情况越来越多, 那么意味着Mutation中的方法越来越多.
方法过多, 使用者需要花费大量的经历去记住这些方法, 甚至是多个文件间来回切换, 查看方法名称, 甚至如果不是复制的时候, 可能还会出现写错的情况.

如何避免上述的问题呢?
在各种Flux实现中, 一种很常见的方案就是使用常量替代Mutation事件的类型.
我们可以将这些常量放在一个单独的文件中, 方便管理以及让整个app所有的事件类型一目了然.

具体怎么做呢?
我们可以创建一个文件: mutation-types.js, 并且在其中定义我们的常量.
定义常量时, 我们可以使用ES2015中的风格, 使用一个常量来作为函数的名称.

image-20210801173002346

5、Mutation同步函数

image-20210801173123716

首先将插件切换到Vuex

image-20210801181648464

image-20210801175851596

image-20210801173410129

根据规则,我们不会在mutation中进行异步操作,但是某些时候仍然需要在Vuex中进行一些异步操作,这时候可以用Action

d、Action

image-20210802183809662

根据图中规则,首先 组件 dispatch Actions 然后 Actions Commit Mutations,再返回给状态

我们强调, 不要在Mutation中进行异步操作。

但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的。这个时候怎么处理呢?

Action类似于Mutation, 但是是用来代替Mutation进行异步操作的。

context是什么?

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

我们定义了actions,,然后又在actions中去进行commit,这样的代码是否多此一举呢?事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了。

Action的基本使用代码如下: index.js

const store = new Vuex.Store({
	state: {
		count: 0
	},
	mutations: {
		increment(state) {
			state.count++
		}
	},
  //首先dispatch actions,然后actions commit mutations
	actions: {
		increment(context) {
			context.commit('increment')
		}
	}
})	

App.vue 在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch

  methods:{
    increment: function (){
      this.$store.dispatch('increment')
    },
  }
  • 同样的, 也是支持传递payload
methods: {
	increment() {
		this.$store.dispatch('increment',{cCount: 5})
	}
}
mutations: {
	increment(state,payload) {
		state.count += payload.Count
	}
},
actions: {
	increment(context,payload) {
		setTimeout(() => {
			context.commit('increment',payload)
		},5000)
	}
}

Action返回的Promise

学习ES6语法的时候,Promiss经常用于异步操作

  • Action中, 我们可以将异步操作放在一个Promise中, 并且在成功或者失败后, 调用对应的resolvereject

  • 来看下面的代码:

actions: {
	increment(context) {
		return new Promise((resolve) => {
			setTimeout(() => {
				context.commit('increment')
				resolve()
			},1000)
		})
	}
}
methods: {
	increment() {
		this.$store.dispatch('increment').then(res => {
			console.log('完成了更新操作')
		})
	}
}

e、Module

认识Vuex的Module

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

我们按照什么样的方式来组织模块呢?

const moduleA = {
	state: { ... },
	mutations: { ... },
	actions: { ... },
	getters: { ... }
}

const moduleB = {
	state: { ... },
	mutations: { ... },
	actions: { ... }
}

const store = new Vuex.Store({
	modules: {
		a: moduleA,
		b: moduleB
	}
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
store.state   // -> 全局state的状态

Module的局部状态

  • 上面的代码中, 我们已经有了整体的组织结构, 下面我们来看看具体的局部模块中的代码如何书写.
    • 我们在moduleA中添加statemutationsgetters
    • mutationgetters接收的第一个参数是局部状态对象
const moduleA = {
	state: {
		count: 0
	},
	mutations: {
		increment(state) {
			state.count++
		}
	},
	getters: {
		doubleCount(state) {
			return state.count * 2
		}
	}
}

const moduleB = {
}

const store = new Vuex.Store({
	state: {
		gCount: 111
	},
	modules: {
		a: moduleA,
		b: moduleB
	}
})

export default store;
<script>
	export default {
		name: 'App',
		components: {
		},
		computed: {
			count() {
				return this.$store.getters.doubleCount
			}
		},
		methods: {
			increment() {
				this.$store.commit('increment')
			}
		}
	}
</script>

注意:

  • 虽然, 我们的doubleCountincrement都是定义在对象内部
  • 但是在调用的时候, 依然是通过this.$store来直接调用的

Module的Actions写法

actions的写法呢? 接收一个context参数对象

局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

const moduleA = {
	// ...
	actions: {
		incrementIfOddOnRootSum ({ state,commit,rootState }) {
			if ((state.count + rootState.count) % 2 === 1) {
				commit('increment')
			}
		}
	}
}	

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

const moduleA = {
	// ...
	getters: {
		sumWithRootCount (state,getters,rootState) {
			return state.count + rootState.count
		}
	}
}

f、项目结构

image-20210803092900209

index.js中的代码,安装插件,创建对象,导出store

import Vue from 'vue'
import Vuex from 'vuex'

import mutations from './mutations'
import actions from './actions'
import getters from './getters'
import moduleA from './modules/moduleA'

// 1.安装插件
Vue.use(Vuex)

// 2.创建对象
const state = {
  counter: 1000,
  students: [
    {id: 110, name: 'why', age: 18},
    {id: 111, name: 'kobe', age: 24},
    {id: 112, name: 'james', age: 30},
    {id: 113, name: 'curry', age: 10}
  ],
  info: {
    name: 'kobe',
    age: 40,
    height: 1.98
  }
}
const store = new Vuex.Store({
  state,
  mutations,
  actions,
  getters,

  modules: {
    a: moduleA
  }
})

// 3.导出store独享
export default store


// 对象的结构
const obj = {
  name: 'why',
  age: 18,
  height: 1.88,
  address: '洛杉矶'
}

const {name, height, age} = obj;

15、网络封装请求

a、网络模块的选择

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

  1. 选择 Ajax
    传统的Ajax是基于XMLHttpRequest(XHR)
    为什么不用它呢?
    非常好解释, 配置和调用方式等非常混乱.
    编码起来看起来就非常蛋疼.
    所以真实开发中很少直接使用, 而是使用jQuery-Ajax
  2. 选择 jQuery-Ajax
    在前面的学习中, 我们经常会使用jQuery-Ajax,相对于传统的Ajax非常好用.
    为什么不用它呢?
    首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
    那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
    jQuery的代码1w+行.
    Vue的代码才1w+行.
    完全没有必要为了用网络请求就引用这个重量级的框架.
  3. 选择 Vue-resource
    官方在Vue1.x的时候, 推出了Vue-resource.
    Vue-resource的体积相对于jQuery小很多.
    另外Vue-resource是官方推出的.
    为什么不选择它呢?
    在Vue2.0退出后, Vue作者就在GitHub的Issues中说明了去掉vue-resource, 并且以后也不会再更新.
    那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新和维护.
    对以后的项目开发和维护都存在很大的隐患.
  4. 选择 axios
    在说明不再继续更新和维护vue-resource的同时, 作者还推荐了一个框架: axios
    axios有非常多的优点, 并且用起来也非常方便.

b、axios的使用

1. 认识 axios

  • 为什么选择axios

    • 功能特点:
      • 在浏览器中发送 XMLHttpRequests 请求
      • node.js 中发送 http请求
      • 支持 Promise API
      • 拦截请求和响应
      • 转换请求和响应数据
      • 等等
  • axios请求方式

    • 支持多种请求方式:

    • get,获取数据的方式
      
      post,提交数据的方式(表单提交以及文件上传)
      
      put,更新数据的方式(提交所有的数据)
      
      patch,提交数据的方式 (提交修改的数据)
      
      delete,删除数据的方式
      

2. 发送基本请求

  • 发送get请求演示,默认的就是get请求方式

  • //1.axios的基本使用
    //不带参数
    axios({
      url: 'http://123.207.32.32:8000/home/multidata',
    
    }).then(res => {
      console.log(res);
    })
    
    //携带参数
    axios({
      url: 'http://123.207.32.32:8000/home/data',
      // 专门针对get请求的参数拼接,若用post则用data
      params: {
        type: 'pop',
        page: 1
      }
    }).then(res => {
      console.log(res);
    })
    

    发送并发请求演示

    • 有时候, 我们可能需求同时发送两个请求
      • 使用axios.all, 可以放入多个请求数组.
      • axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2
    //2.axios发送并发请求
    axios.all([axios({
      url: 'http://123.207.32.32:8000/home/multidata'
    }), axios({
      url: 'http://123.207.32.32:8000/home/data',
      params: {
        type: 'sell',
        page: 5
      }
    })]).then(results => {
      console.log(results);
      console.log(results[0]);
      console.log(results[1]);
    })
    

3. 全局配置

在上面的示例中, 我们的BaseURL固定

  • 事实上, 在开发中可能很多参数都是固定的.
  • 这个时候我们可以进行一些抽取, 也可以利用axios全局配置
//3.使用全局的axios和对应的配置在进行网络请求
axios.defaults.baseURL = 'http://123.207.32.32:8000'
axios.defaults.timeout = 5000

axios.all([axios({
  url: '/home/multidata'
}), axios({
  url: '/home/data',
  params: {
    type: 'sell',
    page: 5
  }
})]).then(axios.spread((res1, res2) => {
  console.log(res1);
  console.log(res2);
}))

常见的配置选项:

image-20210805172855621

4. axios的实例

为什么要创建axios的实例呢?

  • 当我们从axios模块中导入对象时, 使用的实例是默认的实例.
  • 当给该实例设置一些默认配置时, 这些配置就被固定下来了.
  • 但是后续开发中, 某些配置可能会不太一样.
  • 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等.
  • 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.
//4.创建对应的axios的实例
const instance1 = axios.create({
  baseURL: 'http://123.207.32.32:8000',
  timeout: 5000
})

instance1({
  url: '/home/multidata'
}).then(res => {
  console.log(res);
})

instance1({
  url: '/home/data',
  params: {
    type: 'pop',
    page: 1
  }
}).then(res => {
  console.log(res);
})


const instance2 = axios.create({
  baseURL: 'http://222.111.33.33:8000',
  timeout: 10000,
  // headers: {}
})

5. axios的封装

image-20210807213212436

还可以直接 return instance,因为instance的返回类型就为 Promise

image-20210807214306107

image-20210807214113029

c、拦截器的使用

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)), 必须携带一些特殊的信息

    //拦截到并修改为想要的数据之后,需要将 config 返回出去
    return config
  }, err => {
     console.log(err);
  })

  // 2.2.响应拦截  
  //服务器已经响应过了,传入结果result: res
  instance.interceptors.response.use(res => {
    // console.log(res);
    return res.data
  }, err => {
    console.log(err);
  })

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

image-20210807215228839


default {
name: ‘App’,
components: {
},
computed: {
count() {
return this.KaTeX parse error: Expected 'EOF', got '}' at position 30: …doubleCount }̲ }, methods…store.commit(‘increment’)
}
}
}


注意:

- 虽然, 我们的`doubleCount`和`increment`都是定义在`对象内部`的
- 但是在调用的时候, 依然是通过`this.$store`来直接调用的

==Module的Actions写法==

actions的写法呢? 接收一个context参数对象 

局部状态通过 context.state 暴露出来,根节点状态则为 **context.rootState**

````js
const moduleA = {
	// ...
	actions: {
		incrementIfOddOnRootSum ({ state,commit,rootState }) {
			if ((state.count + rootState.count) % 2 === 1) {
				commit('increment')
			}
		}
	}
}	

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

const moduleA = {
	// ...
	getters: {
		sumWithRootCount (state,getters,rootState) {
			return state.count + rootState.count
		}
	}
}

f、项目结构

在这里插入图片描述

index.js中的代码,安装插件,创建对象,导出store

import Vue from 'vue'
import Vuex from 'vuex'

import mutations from './mutations'
import actions from './actions'
import getters from './getters'
import moduleA from './modules/moduleA'

// 1.安装插件
Vue.use(Vuex)

// 2.创建对象
const state = {
  counter: 1000,
  students: [
    {id: 110, name: 'why', age: 18},
    {id: 111, name: 'kobe', age: 24},
    {id: 112, name: 'james', age: 30},
    {id: 113, name: 'curry', age: 10}
  ],
  info: {
    name: 'kobe',
    age: 40,
    height: 1.98
  }
}
const store = new Vuex.Store({
  state,
  mutations,
  actions,
  getters,

  modules: {
    a: moduleA
  }
})

// 3.导出store独享
export default store


// 对象的结构
const obj = {
  name: 'why',
  age: 18,
  height: 1.88,
  address: '洛杉矶'
}

const {name, height, age} = obj;

15、网络封装请求

a、网络模块的选择

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

  1. 选择 Ajax
    传统的Ajax是基于XMLHttpRequest(XHR)
    为什么不用它呢?
    非常好解释, 配置和调用方式等非常混乱.
    编码起来看起来就非常蛋疼.
    所以真实开发中很少直接使用, 而是使用jQuery-Ajax
  2. 选择 jQuery-Ajax
    在前面的学习中, 我们经常会使用jQuery-Ajax,相对于传统的Ajax非常好用.
    为什么不用它呢?
    首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
    那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
    jQuery的代码1w+行.
    Vue的代码才1w+行.
    完全没有必要为了用网络请求就引用这个重量级的框架.
  3. 选择 Vue-resource
    官方在Vue1.x的时候, 推出了Vue-resource.
    Vue-resource的体积相对于jQuery小很多.
    另外Vue-resource是官方推出的.
    为什么不选择它呢?
    在Vue2.0退出后, Vue作者就在GitHub的Issues中说明了去掉vue-resource, 并且以后也不会再更新.
    那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新和维护.
    对以后的项目开发和维护都存在很大的隐患.
  4. 选择 axios
    在说明不再继续更新和维护vue-resource的同时, 作者还推荐了一个框架: axios
    axios有非常多的优点, 并且用起来也非常方便.

b、axios的使用

1. 认识 axios

  • 为什么选择axios

    • 功能特点:
      • 在浏览器中发送 XMLHttpRequests 请求
      • node.js 中发送 http请求
      • 支持 Promise API
      • 拦截请求和响应
      • 转换请求和响应数据
      • 等等
  • axios请求方式

    • 支持多种请求方式:

    • get,获取数据的方式
      
      post,提交数据的方式(表单提交以及文件上传)
      
      put,更新数据的方式(提交所有的数据)
      
      patch,提交数据的方式 (提交修改的数据)
      
      delete,删除数据的方式
      

2. 发送基本请求

  • 发送get请求演示,默认的就是get请求方式

  • //1.axios的基本使用
    //不带参数
    axios({
      url: 'http://123.207.32.32:8000/home/multidata',
    
    }).then(res => {
      console.log(res);
    })
    
    //携带参数
    axios({
      url: 'http://123.207.32.32:8000/home/data',
      // 专门针对get请求的参数拼接,若用post则用data
      params: {
        type: 'pop',
        page: 1
      }
    }).then(res => {
      console.log(res);
    })
    

    发送并发请求演示

    • 有时候, 我们可能需求同时发送两个请求
      • 使用axios.all, 可以放入多个请求数组.
      • axios.all([]) 返回的结果是一个数组,使用 axios.spread 可将数组 [res1,res2] 展开为 res1, res2
    //2.axios发送并发请求
    axios.all([axios({
      url: 'http://123.207.32.32:8000/home/multidata'
    }), axios({
      url: 'http://123.207.32.32:8000/home/data',
      params: {
        type: 'sell',
        page: 5
      }
    })]).then(results => {
      console.log(results);
      console.log(results[0]);
      console.log(results[1]);
    })
    

3. 全局配置

在上面的示例中, 我们的BaseURL固定

  • 事实上, 在开发中可能很多参数都是固定的.
  • 这个时候我们可以进行一些抽取, 也可以利用axios全局配置
//3.使用全局的axios和对应的配置在进行网络请求
axios.defaults.baseURL = 'http://123.207.32.32:8000'
axios.defaults.timeout = 5000

axios.all([axios({
  url: '/home/multidata'
}), axios({
  url: '/home/data',
  params: {
    type: 'sell',
    page: 5
  }
})]).then(axios.spread((res1, res2) => {
  console.log(res1);
  console.log(res2);
}))

常见的配置选项:

在这里插入图片描述

4. axios的实例

为什么要创建axios的实例呢?

  • 当我们从axios模块中导入对象时, 使用的实例是默认的实例.
  • 当给该实例设置一些默认配置时, 这些配置就被固定下来了.
  • 但是后续开发中, 某些配置可能会不太一样.
  • 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等.
  • 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.
//4.创建对应的axios的实例
const instance1 = axios.create({
  baseURL: 'http://123.207.32.32:8000',
  timeout: 5000
})

instance1({
  url: '/home/multidata'
}).then(res => {
  console.log(res);
})

instance1({
  url: '/home/data',
  params: {
    type: 'pop',
    page: 1
  }
}).then(res => {
  console.log(res);
})


const instance2 = axios.create({
  baseURL: 'http://222.111.33.33:8000',
  timeout: 10000,
  // headers: {}
})

5. axios的封装

在这里插入图片描述

还可以直接 return instance,因为instance的返回类型就为 Promise

在这里插入图片描述

在这里插入图片描述

c、拦截器的使用

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)), 必须携带一些特殊的信息

    //拦截到并修改为想要的数据之后,需要将 config 返回出去
    return config
  }, err => {
     console.log(err);
  })

  // 2.2.响应拦截  
  //服务器已经响应过了,传入结果result: res
  instance.interceptors.response.use(res => {
    // console.log(res);
    return res.data
  }, err => {
    console.log(err);
  })

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

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值