Vue随堂笔记

视频地址:https://www.bilibili.com/video/BV15741177Eh
讲师:codewhy(B站搜索)
资料:https://pan.baidu.com/s/1yBhquzLNs91-8fWFpwe4Bg
提取码:em9k

Vue简介

Vue (读音 /vjuː/,类似于 view)

Vue是一个渐进式的框架

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

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

  • 解耦视图和数据

  • 可复用的组件

  • 前端路由技术

  • 状态管理

  • 虚拟DOM

Vue的安装

CDN引入

CDN全称Content Delivery Network,即内容分发网络。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN的关键技术主要有内容存储和分发技术。

开发环境版本,包含有帮助的命令行警告

<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

生产环境版本,优化了尺寸和速度

<script src="https://cdn.jsdelivr.net/npm/vue@2.6.12"></script>

下载引入

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

NPM安装

在用 Vue 构建大型应用时推荐使用 NPM 安装。NPM 能很好地和诸如 webpackBrowserify 模块打包器配合使用。同时 Vue 也提供配套工具来开发单文件组件

# 最新稳定版
$ npm install vue

命令行工具 (CLI)

Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。

它为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。

更多详情可查阅 Vue CLI 的文档

Vue初体验

我的第一个Vue实例

<script src="../JS/vue.js"></script>
<div id="app">
    {{message}}<br/>
    您的年龄是{{age}},<br/>
    您的住址是{{adddress}}
</div>

<script>
    // let变量,const 常量
    const app = new Vue({
        el:"#app", //挂载要管理的元素
        data:{//定义数据
            message:"你好,時光!!",
            age:20,
            adddress:"河南商丘"
        }
    })
</script>

执行结果

编程范式

编程范式(Programming paradigm)是指计算机中编程的典范模式或方法。

不同的编程语言也会提倡不同的“编程范型”。一些语言是专门为某个特定的范型设计的,如Smalltalk和Java支持面向对象编程。而Haskell和Scheme则支持函数式编程。现代编程语言的发展趋势是支持多种范型,如 C#、Java 8+、Kotlin、 Scala、ES6+ 等等。

主要的编程范式有三种:命令式编程,声明式编程和函数式编程。

命令式编程

命令式编程的主要思想是关注计算机执行的步骤,即一步一步告诉计算机先做什么再做什么。

声明式编程

声明式编程是以数据结构的形式来表达程序执行的逻辑。它的主要思想是告诉计算机应该做什么,但不指定具体要怎么做。SQL 语句就是最明显的一种声明式编程的例子。

函数式编程

函数式编程和声明式编程是有所关联的,因为他们思想是一致的:即只关注做什么而不是怎么做。但函数式编程不仅仅局限于声明式编程。

函数式编程最重要的特点是“函数第一位”,即函数可以出现在任何地方,比如可以把函数作为参数传递给另一个函数,不仅如此你还可以将函数作为返回值。

Vue 列表展示

<div id="app">
    <ul>
        <li v-for="item in movies">《{{item}}》</li>
    </ul>
</div>
<script>
   const app = new Vue({
       el:'#app',
       data:{
           message:'Hello world',
           movies:['我和我的祖国','流浪地球','我不是药神','一分钟','送你一朵小红花']
       }
   })
</script>

结果是响应式的

计数器案例

<div id="app">
    <h2>当前计数:{{counter}}</h2>
    <button v-on:click="counter++">+</button>
    <button v-on:click="counter--">-</button>
</div>
<script src="../JS/vue.js"></script>
<script>
    const app = new Vue({
        el:'#app',
        data:{
            counter:10
        }
    })
</script>


绑定函数

methods:用于在Vue中定义方法

@clickv-on的语法糖,相当于v-on:click的简写,用于监听某个元素的点击事件,使用时需要指定要执行的方法,通常是methods中定义的方法。

<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:10
        },
        methods:{
            add:function(){
                console.log("add方法被执行了!")
                this.counter++
            },
            sub:function(){
                console.log("sub方法被执行了!")
                this.counter--
            }
        }
    })
</script>

MVVM

MVVM是什么

Model-View-ViewModel

它是一种基于前端开发的架构模式,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定
Vue.js 是一个提供了 MVVM 风格的双向数据绑定的 Javascript 库,专注于View 层。它的核心是 MVVM 中的 VM,也就是 ViewModel。 ViewModel负责连接 View 和 Model,保证视图和数据的一致性,这种轻量级的架构让前端开发更加高效、便捷。


View层

视图层

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

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

Model层

数据层

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

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

ViewModel层

视图模型层

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

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

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

计时器案例中的MVVM

Vue的options

详细介绍:https://cn.vuejs.org/v2/api/

常用选项

el

类型:string | HTMLElement

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

data

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

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

methods

类型:{ [key: string]: Function }

作用:定义属于Vue的一些方法,可以在其他地方调用,也可以在指令中使用。

如何区分方法与函数

方法:method,一般定义在外部

函数:function,一般定义在类里

Vue生命周期

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

hook:钩子,生命周期函数又称为钩子函数

创建Vue的template

File => Settings => Editor =>live Templates

模板语法

插值操作

Mustache语法(也就是双大括号{{}})

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

<div id="app">
  <h2>{{message}}</h2>
  <h2>你好鸭,{{firstName}}</h2>
  <h2>{{firstName+' '+lastName}}</h2>
  <h2>{{firstName}} {{lastName}}</h2>
  <h2>{{counter*2}}</h2>
  <h2></h2>
  <h2></h2>

</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          firstName:'Linus',
          lastName:'Benedict Torvalds',
          counter:100
      }
  })
</script>

v-once

该指令后面不需要跟任何表达式

该指令表示元素和组件只渲染一次,不会随着数据的改变而改变。

<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:'Hello World'
      }
  })
</script>

v-html

该指令后面往往会跟上一个string类型

会将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:'Hello World',
          url:'<a href="https://bilibili.com">哔哩哔哩</a>'
      }
  })
</script>

v-text

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

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

v-text 会替换标签中的所有内容

<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:'Hello World'
      }
  })
</script>

v-pre

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

<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:'Hello World'
      }
  })
</script>

v-cloak

作用:防止直接显示未编译的Mustache标签

Vue解析之前v-cloak属性存在

Vue解析之后v-cloak属性消失

<style>
  [v-cloak]{
      display:none;
  }
</style>
<div id="app">
  <h2 v-cloak>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
    setTimeout(function(){
        const app = new Vue({
            el:'#app',
            data:{
                message:'Hello World'
            }
        })
    },3000)//3秒钟后加载数据

</script>

若没有v-cloak属性,页面将显示未经熏染的数据,网页前三秒种显示的内容为{{message}}

用了v-cloak属性后css样式将页面未渲染时的数据隐藏。

绑定属性

v-bind

作用:动态绑定属性(比如动态绑定a元素的href属性,img元素的src属性)

缩写::

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

参数:attrOrProp (optional)

<div id="app">
  <h2>{{message}}</h2>
<!--  <p><img v-bind:src="imgUrl" alt="logo"></p>-->
<!--  <p><a v-bind:href="href">我的随堂笔记</a></p>-->
  <p><img :src="imgUrl" alt="logo"></p>
  <p><a :href="href">我的随堂笔记</a></p>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello Vue',
          imgUrl:'https://cn.vuejs.org/images/logo.png',
          href:'https://www.cnblogs.com/an-shiguang/p/14307317.html'
      }
  })
</script>

绑定方式:对象语法

对象语法的含义是: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>
<style>
  .red{
      color:red;
  }
  .blue{
      color:blue;
  }
  .shadow{
      text-shadow: 5px 5px 5px #FF0000;
  }
  .myFont{
      font-family: 楷体;
  }
</style>
<div id="app">
  <h2 class="red">{{message}}</h2>
  <h2 v-bind:class="red">{{message}}</h2>
  <h2 class="myFont" :class="blue">{{message}}</h2>
<!--  <h2 :class="{key1:value1,key2:value2}">{{message}}</h2>-->
<!--  <h2 :class="{类名1:boolean,类名2:boolean}">{{message}}</h2>-->
<!--  <h2 :class="{shadow:isbind,myFont:true}">{{message}}</h2>-->
  <h2 :class="getClasses()">{{message}}</h2>
  <button @click="clearShadow">去除阴影</button>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          red:'red',
          blue:'blue',
          isbind:true
      },
      methods:{
          clearShadow:function(){
              this.isbind=!this.isbind
          },
          getClasses:function(){
              return {shadow:this.isbind,myFont:this.isbind}
          }
      }
  })
</script>

绑定语法:数组语法

<div id="app">
<!--  <h2 :class="[active,line]">{{message}}</h2>-->
  <h2 :class="getList()">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          active:'aaa',
          line:'bbb'
      },
      methods:{
          getList:function(){
              return [this.active,this.line]
          }
      }
  })
</script>

v-bind绑定style

对象语法

在写CSS属性名的时候,可以使用驼峰式 (fontSize)

或者用短横线分隔 (font-size,记得用单引号括起来)

<div id="app">
  <h2 :style="{fontSize:'50px',color:'blue'}">{{message}}</h2>
  <h2 :style="{fontSize:finalSize+'px',color:finalColor}">{{message}}</h2>
  <h2 :style="getStyle()">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          finalSize:100,
          finalColor:'red'
      },
      methods:{
          getStyle:function(){
              return {fontSize:this.finalSize+'px',color:this.finalColor}
          }
      }
  })
</script>

数组语法

<div v-bind:style="[baseStyles, overridingStyles]"></div>

style后面跟的是一个数组类型
多个值之间用逗号分割

<div id="app">
  <h2 :style="[myStyle,myFont]">{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          myStyle:{color:'orange'},
          myFont:{fontFamily:'楷体'}
      }
  })
</script>

计算属性

<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:'Linus',
          lastName:'Benedict Torvalds',
      },
      computed:{
          FullName:function(){
              return this.firstName+' '+this.lastName;
          }
      },
      methods:{
          getFullName:function(){
              return this.firstName+' '+this.lastName;
          }
      }
  })
</script>
<div id="app">
  <h2>总价格:{{totalPrice}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          books:[
              {id:'1001',name:'码出高效',price:120},
              {id:'1002',name:'计算机组成原理',price:80},
              {id:'1003',name:'java核心技术',price:90},
              {id:'1004',name:'现代操作系统',price:60}
          ]
      },
      computed:{
          totalPrice:function(){
              let price =0;
              for(let i=0; i<this.books.length;i++){
                  price+=this.books[i].price
              }
              return price
          }
      }
  })
</script>

setter和getter

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

<body>

<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: function(newValue) {
          // console.log('-----', newValue);
          const names = newValue.split(' ');
          this.firstName = names[0];
          this.lastName = names[1];
        },
        get: function () {
          return this.firstName + ' ' + this.lastName
        }
      }
    }
  })
</script>

</body>

计算属性的缓存

计算属性会进行缓存,如果多次使用时,计算属性只会调用一次

Es6补充

let/var

Es5中的var没有块级作用域,Es6中的let有块级作用域

JS中使用var来声明一个变量时, 变量的作用域主要是和函数的定义有关

针对于其他块定义来说是没有作用域的,比如if/for等,这在我们开发中往往会引起一些问题

<body>
    
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>

<script>
  // ES5中的var是没有块级作用域的(if/for)
  // ES6中的let是由块级作用的(if/for)

  // ES5之前因为if和for都没有块级作用域的概念, 所以在很多时候, 我们都必须借助于function的作用域来解决应用外面变量的问题.
  // ES6中,加入了let, let它是有if和for的块级作用域.
  // 1.变量作用域: 变量在什么范围内是可用.
  // {
  //   var name = 'shiguang';
  //   console.log(name);
  // }
  // console.log(name);

  // 2.没有块级作用域引起的问题: if的块级
  // var func;
  // if (true) {
  //   var name = 'shiguang';
  //   func = function () {
  //     console.log(name);
  //   }
  //   // func()
  // }
  // name = 'linus'
  // func()
  // // console.log(name);

  var name = 'shiguang'
  function abc(bbb) { // bbb = 'shiguang'
    console.log(bbb);
  }
  abc(name)
  name = 'linus'

  // 3.没有块级作用域引起的问题: for的块级
  // 为什么闭包可以解决问题: 函数是一个作用域.
  // var btns = document.getElementsByTagName('button');
  // for (var i=0; i<btns.length; i++) {
  //   (function (num) { // 0
  //     btns[i].addEventListener('click', function () {
  //       console.log('第' + num + '个按钮被点击');
  //     })
  //   })(i)
  // }

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

</script>

</body>

const的使用

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

使用const修饰的标识符为常量, 不可以再次赋值

当我们修饰的标识符不会被再次赋值时, 就可以使用const来保证数据的安全性

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

注意点

  1. const修饰的标识符被赋值后不能修改(当const修饰的是一个对象时,对象内部的属性可以修改)
const a = 15; //给a赋值为15
a = 66; //错误,赋值后不可修改
  1. 使用const定义标识符,必须进行赋值
const name; //错误,const修饰的标识符必须赋值

对象增强写法

属性初始化简写

ES6之后键和值一致时可以简写

//ES6之前
let name = 'shiguang'
let age = 20
let obj1 = {
    name:name,
    age:age
}

//ES6之后
let obj2 = {
    name,age
}

方法的简写

//ES6之前
let obj1 = {
    test:function(){
        console.log('我是obj1的test函数');
    }
}
obj1.test()

//ES6之后
let obj2 = {
    test(){
        console.log('我是obj2的test函数');
    }
}
obj2.test()

事件监听

v-on介绍

作用:绑定事件监听器

缩写:@

预期:Function | Inline Statement | Object

参数:event

下面的代码中,我们使用了v-on:click="counter++”

另外,我们可以将事件指向一个在methods中定义的函数

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

<div id="app">
  <h2>点击次数:{{counter}}</h2>
<!--  <button v-on:click="counter++">+</button>-->
<!--  <button v-on:click="btnClick">+</button>-->
  <button @click="counter++">+</button>
<!--  <button @click="btnClick">+</button>-->
<!--  <button @click="btnClick()">+</button>-->
  <button @click="btnClick(123,$event)">+</button>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          counter:0
      },
      methods:{
          btnClick(abc,eventTest){
              this.counter++
              console.log(abc,eventTest)
          }
      }
  })
</script>

当方法未调用参数时方法后的()可以省略,但方法本身是需要一个参数的,这时Vue会默认将浏览器生产的event事件对象作为参数传入到方法中

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

v-on的修饰符

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

.stop - 调用 event.stopPropagation()

.prevent - 调用 event.preventDefault()

.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调

.native - 监听组件根元素的原生事件

.once - 只触发一次回调

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!--阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!-- 串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">
    
<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">
    
<!-- 点击回调只会触发一次 -->
<button @click.once="doThis"></button>

条件判断

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

这三个指令与JavaScript的条件语句if、else、else if类似

Vue的条件指令可以根据表达式的值在DOM中渲染或销毁元素或组件

v-if的原理

v-if后面的条件为false时,对应的元素以及其子元素不会渲染。

也就是根本没有不会有对应的标签出现在DOM中。

<div id="app">
  <h2 v-if="flag">{{message}}</h2>
  <h2 v-else>flag为false时显示我</h2>
  <h2 v-if="score>=90">优秀</h2>
  <h2 v-else-if="score>=80">良好</h2>
  <h2 v-else-if="score>=60">及格</h2>
  <h2 v-else>不及格</h2>
  <h2>{{result}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          score:90,
          flag:true
      },
      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="flag">
    <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="flag=!flag">切换</button>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          flag:true
      }
  })
</script>

v-show

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

v-if和v-show对比

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

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

开发中如何选择

当需要在显示与隐藏之间切片很频繁时,使用v-show

当只有一次切换时,通过使用v-if

v-for数组遍历

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

    <ul>
      <li v-for="(item,index) in movies">{{index+1}}.{{item}}</li>
    </ul>

    <ul>
      <li v-for="item in user">{{item}}</li>
    </ul>

    <ul>
      <li v-for="(key,value,index) in user">{{index}}-{{key}}:{{value}}</li>
    </ul>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          movies:['我和我的祖国','流浪地球','我不是药神','一分钟','送你一朵小红花'],
          user:{
              name:'张三',
              age:24,
              address:'上海'
          }
      }
  })
</script>

组件key的属性

官方推荐我们在使用v-for时,给对应的元素或组件添加一个:key属性。那么,为什么需要这个属性呢?

其实这和Vue的虚拟DOM的Diff算法有关系。这里借助React's diff algorithm 中的一张图简单说明。

当某一层有很多相同的节点时,也就是列表节点时,假设我们想要插入一个新的节点,我们希望可以在B和C之间插入一个F。

Diff算法默认执行如下图所示,即把C跟新成F,D更新成C,E更新成D,最后在插入E,这样效率非常低。

所以我们需要使用key为每个节点做一个唯一标识,这样,Diff算法就可以正确地识别每一个节点,找到正确的位置插入新节点。

所以key的作用主要是为了高效更新虚拟DOM

检测数组更新

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

Vue中包含了一组观察数组编译的方法,使用它们改变数组也会触发视图的更新。

  • push()

  • pop()

  • shift()

  • unshift()

  • splice()

  • sort()

  • reverse()

注意:通过索引值修改数组中的元素不会实现响应式更新

<div id="app">
  <ul>
    <li v-for="item in letters">{{item}}</li>
  </ul>
  <button @click="btnClick">操作</button>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          letters:['a','b','e','c','d']
      },
      methods:{
          btnClick(){
              // 1 push()  从最后一个插入(可以为多个值)
              // this.letters.push('e')
              // this.letters.push('e','f','g')


              // 2 pop() 删除最后一个元素
              // this.letters.pop()

              // 3 shift() 删除第一个元素
              // this.letters.shift()

              // 4 unshift() 在最前面添加元素(可以为多个值)
              // this.letters.unshift("aaa")
              // this.letters.unshift('aaa','bbb','ccc')

              // /** 5 splice() 删除元素|插入元素|替换插入*/
              // 第一个参数代表从第几个元素开始,第二个参数代表要删除的元素个数,其余参数代表要添加的元素
              // 当删除的元素个数与添加的元素个数相同是及完成了替换功能
              // 删除元素:第二个参数传入要删除的元素个数(若不传则删除后面所有元素)
              // this.letters.splice(2)  //保留两个元素,其余的全部删除
              // this.letters.splice(0,2) //删除前两个元素
              // this.letters.splice(1,3,'e','f','g') //替换后三个元素

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

              // 7 reverse() 反转
              // this.letters.reverse()
              // this.letters.sort().reverse()

              // 注意:通过索引值修改数组中的元素不会实现响应式更新
              // 错误做法:this.letters[0]='aaa'
              // 可以用splice()方法实现替换操作
              // this.letters.splice(0,1,'aaa') //将第一个元素替换为aaa

          }
      }
  })
</script>

小案例

任务要求:点击文字后改变当前文字颜色

<style>
    .active{
        color:red;
    }
</style>
<div id="app">
    <ul>
        <li @click="changeColor(index)"
            :class="{active:index===currentIndex}" 
            v-for="(item,index) in movies">
            《{{item}}》
        </li>
    </ul>
</div>
<script src="../js/vue.js"></script>
<script>
   const app = new Vue({
       el:'#app',
       data:{
           message:'Hello world',
           movies:['我和我的祖国','流浪地球','我不是药神','一分钟','送你一朵小红花'],
           currentIndex:0
       },
       methods:{
           changeColor:function(index){
               this.currentIndex=index
           }
       }
   })
</script>

综合案例

任务要求:可以进行购买,移除操作,所有内容移除后显示显示无书籍,可以对价格进行计算

table {
  border: 1px solid #e9e9e9;
  border-collapse: collapse;
  border-spacing: 0;
}

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

th {
  background-color: #f7f7f7;
  color: #5c6b77;
  font-weight: 600;
}
<div id="app">
  <div v-if="list.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 list" :key="item.id">
        <td>{{index + 1}}</td>
        <td>{{item.name}}</td>
        <td>{{item.date}}</td>
        <td>{{item.price}}</td>
        <td>
          <button :disabled="item.count<=0" @click="increment(index)">-</button>
          <span>{{item.count}}</span>
          <button @click="decrement(index)">+</button>
        </td>
        <td><button @click="removeItem(index)">移除</button></td>
      </tr>
      </tbody>
    </table>
    <h2>总价格: {{totalPrice | showPrice}}</h2>
  </div>
  <div v-else>购物车没有书籍!</div>
</div>

<script src="../js/vue.js"></script>
<script src="index.js"></script>
const app = new Vue({
   el: '#app',
   data: {
      list: [
         {
            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
         },
      ]
   },
   computed: {
      totalPrice() {
         return this.list.reduce((preValue, item) => {
            return preValue + item.price * item.count
         }, 0)
      }
   },
   filters: {
      showPrice(price) {
         return '¥' + price.toFixed(2)
      }
   },
   methods: {
      increment(index) {
         this.list[index].count--
      },
      decrement(index) {
         this.list[index].count++
      },
      removeItem(index) {
         this.list.splice(index, 1)
      }
   },
})

JS中的三个高阶函数

filter过滤函数

const nums = [2,3,5,1,77,55,100,200]
//要求获取nums中大于50的数
//回调函数会遍历nums中每一个数,传入回调函数,在回调函数中写判断逻辑,返回true则会被数组接收,false会被拒绝
let newNums = nums.filter(function (num) {
  if(num > 50){
    return true;
  }
  return false;
 })
 //可以使用箭头函数简写
//  let newNums = nums.filter(num => num >50)

filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

注意:filter()内部返回的是布尔类型,最终返回一个检索后的新数组。

注意: filter() 不会对空数组进行检测。

注意: filter() 不会改变原始数组。

map高阶函数

// 要求将已经过滤的新数组每项乘以2
//map函数同样会遍历数组每一项,传入回调函数为参数,num是map遍历的每一项,回调函数function返回值会被添加到新数组中
let newNums2 = newNums.map(function (num) {
  return num * 2
 })
 //简写
//  let newNums2 = newNums.map(num => num * 2)
console.log(newNums2);

map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

map() 方法按照原始数组元素顺序依次处理元素。

注意: map() 不会对空数组进行检测。

注意: map() 不会改变原始数组。

reduce高阶函数

// 3.reduce高阶函数
//要求将newNums2的数组所有数累加
//reduce函数同样会遍历数组每一项,传入回调函数和‘0’为参数,0表示回调函数中preValue初始值为0,回调函数中参数preValue是每一次回调函数function返回的值,currentValue是当前值
//例如数组为[154, 110, 200, 400],则回调函数第一次返回值为0+154=154,第二次preValue为154,返回值为154+110=264,以此类推直到遍历完成
let newNum = newNums2.reduce(function (preValue,currentValue) {
  return preValue + currentValue
 },0)
//简写
// let newNum = newNums2.reduce((preValue,currentValue) => preValue + currentValue)
console.log(newNum);

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

reduce() 可以作为一个高阶函数,用于函数的 compose。

注意: reduce() 对于空数组是不会执行回调函数的。

综合使用

//三个需求综合
let n = nums.filter(num => num > 50).map(num => num * 2).reduce((preValue,currentValue) => preValue + currentValue)
console.log(n);

v-model

v-model作用

表单控件在实际开发中是非常常见的,特别是对于用户信息的提交,需要大量的表单,Vue中使用v-model指令来实现表单元素的双向绑定。

案例解析

<body>
<div id="app">
  <input type="text"  v-model="message">
  <h2>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      }
  })
</script>
</body>

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

当然,我们还可以将v-model用于textarea元素的数据绑定

<body>
<div id="app">
  <textarea v-model="message"></textarea>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      }
  })
</script>
</body>

v-model原理

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

  1. v-bind绑定一个value属性
  2. v-on指令为当前的元素绑定input事件
<input type="text" v-model="message">
<!-- 等同于 -->
<input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
<div id="app">
  <input type="text"  :value="message" v-on:input="valueChange">
  <h2>{{message}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      },
      methods:{
          valueChange(event){
              this.message=event.target.value
          }

      }
  })
</script>

v-model绑定radio

<div id="app">
  <label for="male">男
    <input type="radio" id="male" value="男" v-model="sex">
  </label>
  <label for="female">女
    <input type="radio" id="female" value="女" v-model="sex">
  </label>
  <h2>您选择的性别是:{{sex}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          sex:'男'//设置单选框默认选中男
      }
  })
</script>

单选框在没有绑定v-model时需要有name属性两个单选框才会互斥(即只能选择其中一个)

绑定了v-model后没有name属性两个单选框依旧可以互斥

v-model绑定checkbox

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

单个勾选框

  • v-model为布尔值
  • 此时input的value并不形象v-model的值

多个复选框

  • 因为可以选中多个,所以对应的data中属性是一个数组
  • 当选中某一个时,会将input的value添加到数组中。
<div id="app">
<!--  多个checkbox-->
  <label for="">
    <input type="checkbox" v-model="hobbies" value="唱">唱
    <input type="checkbox" v-model="hobbies" value="跳">跳
    <input type="checkbox" v-model="hobbies" value="rap">rap
    <input type="checkbox" v-model="hobbies" value="篮球">篮球
  </label>
  <h2>您的爱好有:{{hobbies}}</h2>
  <!--  单个checkbox-->
  <label for="agree">是否同意协议
    <input type="checkbox" id="agree" v-model="isAgree">同意
  </label>
  <h2>您的选择是:{{isAgree}}</h2><br>
  <button :disabled="!isAgree">下一步</button>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
         isAgree:false, //单个checkbox返回布尔类型
          hobbies:[] //多个checkbox返回数组类型
      }
  })
</script>

v-model绑定select

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

单选

  • 只能选中一个值,v-model绑定的是一个值。
  • 当选中option中的一个时,会将它对应的value赋值到mySelect中。

多选

  • 可以选中多个值,v-model绑定的是一个数组。
  • 当选中多个值时,会将选中的option对应的value添加到数组mySelects中。
<div id="app">
	<!-- 选择多个值-->
     <select v-model="mySelects" multiple>
         <option value="apple">苹果</option>
         <option value="orange">橘子</option>
         <option value="banana">香蕉</option>
     </select>
 	 <p>您喜欢的水果:{{mySelects}}</p>
    
	<!-- 选择单个值 -->
     <select v-model="mySelect">
         <option value="apple">苹果</option>
         <option value="orange">橘子</option>
         <option value="banana">香蕉</option>
     </select>
     <p>您喜欢的水果:{{mySelect}}</p>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
         mySelect:'apple',
         mySelects:[]
          
      }
  })
</script>

值绑定

值绑定即动态地给value赋值

之前都是在定义input的时候直接给定的,但是真实开发中,这些input的值可能是从网络获取或定义在data中的

所以我们可以通过v-bind:value动态的给value绑定值

<div id="app">
  <label v-for="item in originHobbies" :for="item">
    <input type="checkbox" :id="item" :value="item" v-model="hobbies">{{item}}
  </label>
  <h2>您的爱好有:{{hobbies}}</h2>
</div>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          originHobbies:['唱','跳','rap','篮球'],
          hobbies:[]
      }
  })
</script>

修饰符

lazy修饰符

<input type="text" v-model.lazy="message1">

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

number修饰符

<input type="number" v-model.number="age">

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

trim修饰符

<input type="text" v-model.trim="message">

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

组件化

什么是组件化

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

我们将一个完整的页面分成很多个组件,每个组件都用于实现页面的一个功能块,而每一个组件又可以进行细分。

注册组件的基本步骤

  1. 调用Vue.extend()方法创建组件构造器
  2. 调用Vue.component()方法注册组件
  3. 在Vue实例的作用范围内使用组件
<div id="app">
   <!--  第三步:在实例作用范围内使用组件 -->
  <my-cpn></my-cpn> 
</div>
<script src="../js/vue.js"></script>
<script>
    //第一步:创建组件构造器对象  cpnC(componentController)
    const cpnC = Vue.extend({
        template:`<div>
                    <h2>我是标题</h2>
                    <p>我是内容,我是内容</p>
                    <p>我是内容,我是内容</p>
                 </div>`
    })
    //第二步:注册组件
    Vue.component('my-cpn',cpnC)
  const app = new Vue({
      el:'#app',
      data:{
      }
  })
</script>

注册组件步骤解析

Vue.extend()

调用Vue.extend()创建的是一个组件构造器。通常在创建组件构造器时,传入template代表自定义组件的模板,该模板就是在使用到组件的地方要显示的HTML代码。事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接语法糖。

Vue.component()

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

1、注册组件的标签名 2、组件构造器

在实例范围内使用组件

最后一个组件没有被渲染出来,因为它没有在实例范围内。

全局组件与局部组件

通过调用Vue.component()注册组件时,组件的注册是全局的,这意味着该组件可以在任意Vue实例范围内使用。

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

父组件和子组件

组件和组件之间存在层级关系,而其中一种非常重要的关系就是父子组件的关系。

<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>
    `,
    components: {
      cpn1: cpnC1
    }
  })

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

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

因为当子组件注册到父组件的components时,Vue会编译好父组件的模块,该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)<child-cpn></child-cpn>是只能在父组件中被识别类似这种用法,<child-cpn></child-cpn>是会被浏览器忽略的。

注册组件语法糖

注册全局组件

//原来写法
//1.创建组件构造器
const myComponent = Vue.extend({
    template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
});
//2.注册全局组件
Vue.component('cpn',myComponent);


//使用语法糖 只需一步
Vue.component('cpn', {
    template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
  });

注册局部组件

//原来写法
//1.创建组件构造器
const myComponent = Vue.extend({
    template: `
      <div>
        <h2>我是标题1</h2>
        <p>我是内容, 哈哈哈哈</p>
      </div>
    `
});
//2.在实例中注册
const app = new Vue({
    el:'#app',
    components:{
        'my-cpn':myComponent
    }
})


//语法糖写法
const app = new Vue({
    el:'#app',
    components:{
        'my-cpn1':{
            template: `
                      <div>
                        <h2>我是時光</h2>
                        <p>我是内容, 哈哈哈哈</p>
                      </div>
                    `
        },
        'my-cpn2':{
            template: `
                      <div>
                        <h2>我还是時光</h2>
                        <p>我是内容,呵呵呵</p>
                      </div>
                    `
        }
    }
})

模板的分离写法

语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。如果能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。

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

  • 使用<script>标签
<div id="app">
  <cpn></cpn>
</div>

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

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

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

  const app = new Vue({
    el: '#app',
    data: {
      message: '你好啊'
    }
  })
</script>
  • 使用<template>标签
<div id="app">
  <cpn></cpn>
</div>

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

组件数据存放

组件不能访问Vue实例中的数据

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

<template id="cpn">
  <div>
    <h2>{{message}}</h2>
  </div>
</template>

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

  Vue.component('cpn', {
    template: '#cpn'
  })

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

以上代码使用组件去访问定义在Vue实例中的message,结果并没有渲染出来任何数据。

组件是一个单独模块功能的封装,这个模块有属于自己的HTML模板,也应该有属于自己的data属性。

组件中的数据存放

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

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

<template id="cpn">
  <div>
    <h2>{{title}}</h2>
    <p>我是一个没有感情的段落</p>
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
    
  Vue.component('cpn', {
    template: '#cpn',
    data() {
      return {
        title: '我是時光'
      }
    }
  })

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

组件内的data是函数的原因

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

<template id="cpn">
  <div>
    <h2>当前计数: {{counter}}</h2>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const obj = {
    counter: 0
  }
  Vue.component('cpn', {
    template: '#cpn',
    data() {
      return obj
    },
    methods: {
      increment() {
        this.counter++
      },
      decrement() {
        this.counter--
      }
    }
  })

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

obj对象被单独抽离,意味着上面使用的都是同一个obj,那么,其中的一个组件修改了counter,其他组件的counter值也会发生改变。

Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。

父子组件之间的通讯

我们知道子组件是不能引用父组件或者Vue实例中的数据的,但是在开发中往往有一些数据需要从上层传递到下层,比如在一个页面中,我们从服务器请求到了很多的数据。其中有一部分数据并非整个页面的父组件展示,而是需要下面的子组件进行展示。这个时候,并不会让子组件再次发送一个网络请求,而是直接让父组件将数据传递给子组件。那么,如何进行父子组件间的通信呢?Vue官方提供了通过props向子组件传递数据和通过事件向父组件发送消息的方式进行父子组件间的通信。在真实开发中,Vue实例与子组件的通信和父组件与子组件的通信过程是一样的。

父级向子级传递

在组件中,使用选项props来声明需要从父级接收到的数据。props的值有两种方式。

方式一:字符串数组,数组中的字符串就是传递时的名称。

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
<div id="app">
  <mycpn :cmessage="message" :cmovies="movies"></mycpn>
</div>
<template id="myCpn">
  <div>
    我是子组件
    <p>从父组件传递的数据</p>
    <p>打个招呼:{{cmessage}}</p>
    <p >最新影视:<div v-for="item in cmovies"><<{{item}}>></div></p>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
    const mycpn = {
        template:`#myCpn`,
        props:['cmessage','cmovies']
    }
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World',
          movies:['送你一朵小红花','我和我的家乡','唐人街探案3']
      },
      components:{
          mycpn
      }
  })
</script>
</body>
</html>

注意:组件内数据如果采用驼峰命名,{{}}内可以使用驼峰命名,但传递数据时需要用-拼接(不能使用大写) 

如:数据totalNum 向父组件传递需要用:total-num

方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
</head>
<body>
<script src="../JS/vue.js"></script>
<div id="app">
  <mycpn :cpn-message="message" @item-click="cpnClick"></mycpn>
</div>
<template id="mycpn">
  <div>
    <p>{{cpnMessage}}</p>
    <button v-for="item in categories" :key="item.id" @click="btnClick(item)">{{item.name}}</button>
  </div>
</template>


<script>
    const mycpn = {
        template:`#mycpn`,
        data(){
            return{
                categories:[
                    {id:1001,name:'电子数码'},
                    {id:1002,name:'电脑办公'},
                    {id:1003,name:'零食小吃'},
                    {id:1004,name:'厨房餐具'},
                ]
            }
        },
        methods:{
            btnClick(item){
                this.$emit('item-click',item)
            }
        },
        props:{
            cpnMessage:{
                type:String,
            }
        }
    }
  const app = new Vue({
      el:'#app',
      data(){
          return {
              message:"Hello Vue"
          }
      },
      methods:{
          cpnClick(item){
              console.log(item.id+':'+item.name)
          }
      },
      components:{
          mycpn
      }
  })
</script>
</body>

</html>

props数据验证

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

验证支持以下数据类型

  1. String
  2. Number
  3. Boolean
  4. Array
  5. Object
  6. Date
  7. Function
  8. Symbol
  Vue.component('my-component',{
      props:{
          //基础的类型检查(null 匹配任何类型)
          propA:String,
          //多个可能的类型
          propB:[Number,String],
          //必填的prop
          propC:{
              type:String,
              required:true
          },
          //带有默认值的数字
          propD:{
              type:Number,
              default:0
          },
          //带有默认值的对象
          propE:{
              type:Object,
              //对象或数组默认值必须从一个工厂函数获取
              default: function (){
                  return {
                      message:'hello shiguang'
                  }
              }
          },
          propF:{
              validator:function (value){
                  //这个值必须匹配下列字符串中的一个
                  return ['success','warning','danger'].indexOf(value) !==-1

                  }
              }
          }
  })

当我们有自定义构造函数时,验证也支持自定义的类型

function persion(firstName,lastName){
      this.firstName = firstName
      this.lastName = lastName
}

Vue.component('blog-post',{
    props:{
        author:persion
    }
})

子级向父级传递

子组件传递数据或事件到父组件中需要使用自定义事件来完成,v-on不仅可以用于监听DOM事件,还可以用于组件间的自定义事件。

自定义事件的流程

  1. 在子组件中,通过$emit()触发事件。

  2. 在父组件中,通过v-on来监听子组件事件。

父子组件通讯之双向绑定

任务要求:实现组件间数据双向绑定,并且第一个输入框数值始终是第二个输入框数值的100倍

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <mycpn :number1="num1" :number2="num2" @num1change="num1change" @num2change="num2change"/>
</div>
<template id="cpn">
  <div>
    <h2>props:{{number1}}</h2>
    <h2>data:{{dnum1}}</h2>
<!--    <input type="text" v-model="dnum1">-->
    <input type="text" :value="dnum1" @input="num1Input">
    <h2>props:{{number2}}</h2>
    <h2>data:{{dnum2}}</h2>
<!--    <input type="text" v-model="dnum2">-->
    <input type="text" :value="dnum2" @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:{
          mycpn:{
              template:'#cpn',
              props:{
                  number1:Number,
                  number2:Number
              },
              data(){
                  return {
                      dnum1:this.number1,
                      dnum2:this.number2
                  }
              },
              methods:{
                  num1Input(event){
                      this.dnum1=event.target.value;

                      this.$emit('num1change',this.dnum1);

                      this.dnum2= this.dnum1*100;
                      this.$emit('num2change',this.dnum2);
                  },
                  num2Input(event){
                      this.dnum2=event.target.value;

                      this.$emit('num2change',this.dnum2);

                      this.dnum1= this.dnum2/100;
                      this.$emit('num1change',this.dnum1);
                  }
              }
          }
      }

  })
</script>
</body>
</html>

watch实现

watch用于监听data中的数据

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<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">
    <h2>props:{{number2}}</h2>
    <h2>data:{{dnumber2}}</h2>
    <input type="text" v-model="dnumber2">
  </div>
</template>

<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data: {
      num1: 1,
      num2: 0
    },
    methods: {
      num1change(value) {
        this.num1 = parseFloat(value)
      },
      num2change(value) {
        this.num2 = parseFloat(value)
      }
    },
    components: {
      cpn: {
        template: '#cpn',
        props: {
          number1: Number,
          number2: Number,
          name: ''
        },
        data() {
          return {
            dnumber1: this.number1,
            dnumber2: this.number2
          }
        },
        watch: {
          dnumber1(newValue) {
            this.dnumber2 = newValue * 100;
            this.$emit('num1change', newValue);
          },
          dnumber2(newValue) {
            this.dnumber1 = newValue / 100;
            this.$emit('num2change', newValue);
          }
        }
      }
    }
  })
</script>

</body>
</html>

父子组件的访问方式:$children​

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

父组件访问子组件:使用$children$refs

子组件访问父组件:使用$parent

我们先来看下$children的访问

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

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

还可以这么用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <mycpn ref="demo"></mycpn>
  <button @click="btnClick">按钮</button>
</div>
<template id="cpn">
  <div>
    {{msg}}
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      },
      components:{
          mycpn:{
              template:'#cpn',
              methods:{
                  show(){
                      console.log(this.msg);
                  }
              },
              data(){
                  return{
                      msg:'Hello Vue'
                  }
              }
          }
      },
      methods: {
          btnClick(){
              this.$children[0].show();//调用子组件方法
              console.log(this.$children);
              console.log(this.$refs.demo);
          }
      }
  })
</script>
</body>
</html>

父子组件的访问方式: $refs

$children的缺陷

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

$refs的使用

$refsref指令通常是一起使用的。

首先,我们通过ref给某一个子组件绑定一个特定的ID。

其次,通过this.$refs.ID就可以访问到该组件了。

<child-cpn1 ref="child1"></child-cpnl>
<child-cpn2 ref="child2"></child-cpn2>
<button @click="showRefsCpn">通过refs访问子组件</button>
showRefsCpn(){
	console.log(this.$refs.child1.message);
	console.log(this.$refs.child2.message);
}

插槽slot

插槽的目的是让我们原来的设备具备更多的扩展性。

封装:抽取共性,保留不同

基本使用

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <cpn></cpn>
  <cpn><span>哈哈</span></cpn>
  <cpn><i>嘻嘻嘻</i></cpn>
  <cpn>
    <div>
      我是一个div
      <p>我是div里的p元素</p>
    </div>
  </cpn>
</div>
<template id="cpn">
  <div>
    <h2>我是组件</h2>
    <slot><button>按钮</button></slot>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      },
      components:{
          cpn:{
              template:'#cpn'
          }
      }
  })
</script>

</body>
</html>

使用方法:在组件内使用<slot></slot>标签预留要插入的元素

可以为插槽添加默认值,如<slot><button>按钮</button></slot>,若插槽处未添加元素则显示默认元素,若插入其他元素则显示插入的元素。

如果一个插槽内放入多个元素都将会显示。

具名插槽

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

如何使用具名插槽呢,非常简单,只要给slot元素一个name属性即可。

<slot name='myslot'></slot>

我们来给出一个案例,这里我们先不对导航组件做非常复杂的封装,先了解具名插槽的用法。

注意:如果没有name属性作为标识,会将所有插槽进行替换

2.6.0版本后slot 用v-slot代替且需要被template标签包裹

<div id="app">
  <cpn>
    <button slot="left">左边按钮</button>
    <template v-slot:right>
      <button >右边按钮</button>
    </template>
  </cpn>
</div>
<template id="cpn">
  <div>
    <slot name="left">左边</slot>
    <slot name="center">中间</slot>
    <slot name="right">右边</slot>
  </div>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
          message:'Hello World'
      },
      components:{
          cpn:{
              template:'#cpn'
          }
      }
  })
</script>

作用域插槽

编译作用域

在真正学习插槽之前,我们需要先理解一个概念:编译作用域。

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

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

<my-cpn v-show="isShow"></my-cpn>中,我们使用了isShow属性。

isShow属性包含在组件中,也包含在Vue实例中。

答案:最终可以渲染出来,也就是使用的是Vue实例的属性。

官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

而我们在使用<my-cpn v-show="isShow"></my-cpn>的时候,整个组件的使用过程是相当于在父组件中出现的,那么他的作用域就是父组件,使用的属性也是属于父组件的属性。因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。组件属性作用域在实例内生效,组件模板作用域在组件内生效。

作用域插槽使用

<div id="app">
  <cpn>
<!--    v-slot用法  v-slot: name值(默认为default)  = "别名"-->
    <template v-slot:default="slotProps">
<!--      将slot内默认值user.lastName 改为user.firstName-->
      {{slotProps.myuser.firstName}}
    </template>

    <template v-slot:demo="slotProps1">
<!--      让name="demo" 的slot默认数据哈哈改为user.lastName-->
      {{ slotProps1.myuser.lastName }}
    </template>

<!--  slot slot-scope在2.6.0版本被弃用-->
<!--    <template slot="default" slot-scope="slotProps2" >-->
<!--      {{ slotProps2.myuser.firstName }}-->
<!--    </template>-->
  </cpn>
</div>
<template id="cpn">
  <span>
<!--    将组件cpn内的数据user动态绑定到myuser上 :myuser="user" 这样才可以在实例app的组件内调用 myuser可以为任意名称-->
<!--    让name="default" 的slot默认数据为user.lastName-->
    <slot :myuser="user">{{user.lastName}}</slot>
<!--    为插槽添加一个标识 name="demo" 若不添加,默认为 name="default"-->
<!--    让name="demo" 的slot默认数据为哈哈-->
    <slot :myuser="user" name="demo">哈哈</slot>
  </span>
</template>
<script src="../js/vue.js"></script>
<script>
  const app = new Vue({
      el:'#app',
      data:{
      },
      components:{
          cpn:{
              template:'#cpn',
              data(){
                  return {
                      user:{
                          firstName:'Torvalds',
                          lastName:'linus'
                      }
                  }
              }
          },

      }
  })
</script>

最终显示结果为:Torvalds linus

注意:slot slot-scope 虽然语法依然支持,但在2.6.0版本后被弃用,推荐使用 v-slot

前端模块化

javaScript原始功能

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

那个时候直接将代码写在<script>标签中即可,随着ajax异步请求的出现,慢慢形成了前后端的分离。客户端需要完成的事情越来越多,代码量也是与日俱增。为了应对代码量的剧增,通常会将代码组织在多个js文件中,进行维护。但是这种维护方式,依然不能避免一些灾难性的问题。比如全局变量同名问题。另外,这种代码的编写方式对js文件的依赖顺序几乎是强制性的。但是当js文件过多,比如有几十个的时候,弄清楚它们的顺序是一件比较困难的事情。而且即使你弄清楚顺序了,也不能避免下面出现的这种尴尬问题的发生。

小东在a.js文件中定义了一个变量flag=true,小红在b.js中也定义了一个变量flag,并且让flag=false,小东在main.js中通过flag进行一些判断,完成一些事件处理,但小东发现代码不能正常运行,反复检查自己定义的flag变量也没发现什么问题,殊不知是小红也定义了相同的flag变量影响了小东的代码。

匿名函数解决方法

我们可以使用匿名函数解决重名问题

//在a.js中使用匿名函数
(function(){
    var flag = true
})()

使用模块作为出口

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

var moduleA = ({
    //1.定义一个对象
    var obj = {}
    //2.在对象内部添加变量和方法
    obj.flag = true
    obj.myFunc = function(info){
    console.log(info);
}
	//3.将对象返回
	return obj
    
})()

我们在匿名函数内部,定义了一个对象。给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可)。最后将这个对象返回,并且在外面使用了一个MoudleA接受。接下来,我们在man.js中怎么使用呢?我们只需要使用属于自己模块的属性和方法即可。

if(moduleA.flag){
    console.log('時光是一只小小白');
}

moduleA.myFunc('很高兴与你相遇,交个朋友吧!!')
console.log(moduleA);

这就是模块最基础的封装,事实上模块的封装还有很多高级的话题但是我们这里就是要认识一下为什么需要模块,以及模块的原始雏形。幸运的是,前端模块化开发已经有了很多既有的规范,以及对应的实现方案。

常见的模块化规范:

CommonJS、AMD、CMD,以及ES6的Modules

初识CommonJS

模块化有两个核心:导入和导出

CommonJS的导出

module.exports = {
    flag:true,
    sum(num1,num2){
        return num1+num2
    }
}

CommonJS的导入

//CommonJS模块
let {flag,sum} = require('moduleA');

//等同于
let ma = require('moduleA');
let flag = ma.flat;
let sum = ma.sum;

Es6的导入和导出

导出(export)导入(import)

导出的基本使用

export指令用于导出变量

export let name = '時光'
export let age = 20
export let sex = 'male'

等同于

let name = '時光'
let age = 20
let sex = 'male'

export {name,age,sex}

导出函数或类

上面我们主要变量,其实也可以输出函数或者类。

export function demo(content){
    console.log(content);
}

export class Person{
    constructor(name,age){
        this.name = name,
        this.age = age;
    }
    
    visit(){
        console.log(this.name+"来看時光了");
    }
}

还可以这么写

function demo(content){
    console.log(content);
}
class Person{
    constructor(name,age){
        this.name = name,
        this.age = age;
    }
    
    visit(){
        console.log(this.name+"来看時光了");
    }
}

export {demo,Person}

export default

某些情况下,一个模块中包含某些功能,我们并不希望给这个功能命名,而且可以让导入者自己命名。这个时候可以使用export default

//a.js
export default function(){
    console.log('時光')
}

这样就可以b.js中使用了(myFunc是自定义命名,可以随便起)

import myFunc from './a.js'

myFunc()

注意:export default在同一个模块中不允许同时存在多个。

import的使用

我们使用export指令导出模块对外提供的接口后,可以通过import命令加载导出的模块。

首先,我们需要在HTML代码中引入两个js文件,并且类型需要设置为module

<script src="a.js" type="module"></script>
<script src="b.js" type="module"></script>

import指令用于导入模块中的内容,比如a.js中的代码

import {name,age,sex} from "./a.js"
console.log(name,age,sex);

如果我们希望一次性导入某个模块中所有的信息,可以使用*导入模块中所有的export变量,而且通常情况下需要给*起一个别名方便后续的使用。

import * as a form './a.js'

console.log(a.name,a.age,a.sex);

webpack

什么是webpack

官方的解释: At its core, webpack is a static module bundler for modern JavaScript applications.

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

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

前端模块化

在ES6之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发。并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,将其进行整合打包。而webpack其中一个核心就是可以让我们进行模块化开发,并且会帮助我们处理模块间的依赖关系。而且不仅仅是JavaScript文件,我们的CSS,图片、json文件等在webpack中都可以被当做模块来使用。这就是webpack中模块化的概念。

打包就是将webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等操作。打包的操作grunt/gulp也可以帮助我们完成。

webpack和grunt/gulp的对比

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

下面的task就是将src下面的所有js文件转成ES5的语法。并且最终输出到dist文件夹中。

const gulp = require('gulp');
const babel = require('gulp-babel');

gulp.task('js',() =>
       gulp.src('src/*.js')
         .pipe(babel({
    		presets:['es2015']
		}))
    	.pipe(gulp.dest('dist'))
);

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

grunt/gulp更加强调的是前端流程的自动化,模块化不是它的核心。

webpack更加强调模块化开发管理,而文件压缩合并、预处理等功能,是它附带的功能。

webpack的安装

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

查看node版本

node -v

全局安装webpack(以3.6.0版本为例,因为vue cli2依赖该版本)

npm install webpack@3.6.0 -g

局部安装webpack

cd 对应的目录
npm install webpack@3.6.0 --save-dev

说明:-g 代表全局安装,--save-dev 是开发是依赖,项目打包后不需要使用。

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

webpack初体验

定义三个js文件,分别为aa.js,main.js,mathUtils.js并且main.js依赖aa.js和mathUtils.js

项目结构如下

aa.js

export  let name='张总'
export  let age = 20
export  let height = 180

mathUtils.js

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

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

module.exports = {
    add,mul
}

main.js

const  {add,mul} = require('./mathUtils.js')
console.log(add(10,12));
console.log(mul(10,2));
import {name,age,height} from "./aa";

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

由于浏览器不支持CommonJs语法,所有使用webpack打包处理

使用命令 webpack ./src/main.js ./dist/bundle.js将src/main.js及其依赖文件打包到dist/bundle.js中

在index.html导入bundle.js即可使用

webpack配置

每次使用webpack的命令都需要写上入口和出口作为参数,非常麻烦

可以将这两个参数写到配置中,在运行时,直接读取

创建一个webpack.config.js文件

const path = require('path') //导入path依赖

module.exports = {
    //入口:可以是字符串/数组/对象,这里我们入口只有一个,所以写一个字符串即可
    entry:'./src/main.js',
    //出口:通常是一个对象,里面至少包含两个重要属性,path和filename
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'bundle.js'
    }
}

__dirname(两个底线)代表当前文件夹,'dist'是文件打包目录

使用npm init 生成npm包管理的文件package.json

使用npm install 安装依赖

在终端输入命令webpack即可执行

package.json中定义启动

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

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

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

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

执行命令:npm run build

局部安装webpack

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

通常一个项目,都有自己局部的webpack。

局部安装命令 npm install webpack@3.6.0 --save-dev

使用:通过node_modules/.bin/webpack启动webpack打包

loader使用

什么是loader

loader是webpack中一个非常核心的概念。

在我们之前的实例中,主要是用webpack处理js代码,并且webpack会自动处理js之间相关的依赖。但是,在开发中我们不仅有基本的js代码,也需要加载css、图片,还包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等功能。对于webpack本身的能力来说,对于这些转化是不支持的。但给webpack扩展对应的loader就可以啦。

loader使用过程

步骤一:通过npm安装需要使用的loader

步骤二:在webpack.config.js中的modules关键字下进行配置。

css文件处理

项目开发过程中,我们必然需要添加很多的样式,而样式往往写到一个单独的文件中。在src目录中,创建一个css文件,其中创建一个normal.css文件。我们也可以重新组织文件的目录结构,将零散的js文件放在一个js文件夹中。

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

但是,这个时候normal.css中的样式不会生效,因为我们没有引用它。webpack也不可能找到它,因为我们只有一个入口,webpack会从入口开始查找其他依赖的文件。

在入口文件中引用:

打包错误信息

重新打包,会出现如下错误

因为加载css文件需要有对应的loader。

安装并配置css-loader

官网地址:https://webpack.js.org/

按照官方说明配置webpack.config.js文件

首先,你需要先安装 css-loader

npm install --save-dev css-loader

然后把 loader 引用到你 webpack 的配置中。如下所示:

file.js

import css from "file.css";

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

然后运行 webpack

但是,运行index.html,你会发现样式并没有生效。原因是css-loader只负责加载css文件,并不负责将css具体样式嵌入到文档中。我们还需要一个style-loader帮助我们处理。

安装并配置style-loader

安装 style-loader

npm install --save-dev style-loader

配置webpack.config.js

const path = require('path')

module.exports = {
    //入口:可以是字符串/数组/对象,这里我们入口只有一个,所以写一个字符串即可
    entry:'./src/main.js',
    //出口:通常是一个对象,里面至少包含两个重要属性,path和filename
    output:{
        path:path.resolve(__dirname,'dist'), //path通常是一个绝对路径
        filename:'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
            }
        ]
    }
    
}

注意:style-loader需要放在css-loader的前面。因为webpack在读取使用的loader的过程中,是按照从右向左的顺序读取的。

注意:一定要选择适当版本,版本不兼容会出现错误

webpack3.6.0版本 css-loader选择2.0.2,style-loader选择0.23.1

less文件处理

使用less文件

如果我们希望在项目中使用less、scss、stylus来写样式,webpack是否可以帮助我们处理呢?我们这里以less为例,其他也是一样的。我们还是先创建一个less文件,依然放在css文件夹中。

//special.less
@font-Size:50px;
@fontColor:orange;

body{
   font-size: @font-Size;
   color: @fontColor;
}
//main.js
const  {add,mul} = require('./js/mathUtils.js');
console.log(add(10,12));
console.log(mul(10,2));
require('./css/mycss.css');

//引入less
require('./css/special.less');

document.writeln('<div>Hello World</div>');

在main.js中使用document.writeln()document.write()为页面添加内容

但是我使用这种方式失败了,所以我直接在html页面添加了文本内容:<h2>你好時光!!</h2>

重新编译报错了

因为我们需要安装less-loader

安装并配置less-loader

安装less-loader,还需要安装less,因为webpack会使用less对less文件进行编译

npm install --save-dev less-loader@4.1.0 less 

还需要修改对应的配置文件,添加一个rules选项,用于处理.less文件。

//webpack.config.js

module.exports = {
  	...
    module: {
        rules: [
           ...
            {
                test: /\.less$/,
                use: [{
                    loader: "style-loader" // creates style nodes from JS strings
                }, {
                    loader: "css-loader" // translates CSS into CommonJS
                }, {
                    loader: "less-loader" // compiles Less to CSS
                }]
            }
        ]
    }
}

注意:webpack3.6.0 安装less-loader 版本为4.1.0

每次文件变更后重新编译一次,执行已配置的指令:npm run build

重新加载html页面样式加载成功。

图片文件处理

首先,在项目中加入两张图片,一张较小的图片01.jpg(小于8kb),一张较大的图片bg.jpg(大于8kb)。我们针对这两张图片进行不同的处理。

更改css中的样式

body{
    background: url("../img/bg.jpg");
}

如果此时直接打包,会报错,因为缺少loader对图片进行处理。

安装并配置url-loader

我们使用url-loader对图片进行处理。

npm install --save-dev url-loader@1.1.2

配置webpack.config.js

module.exports = {
    module: {
        rules: [
          ...
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192
                        }
                    }
                ]
            }
            
        ]
    }
   
}

再次打包,背景图片就可以加载出来了,而且背景图是通过base64显示出来的。这也是limit属性的作用,当图片大小小于limit(即当前的8192)时对图片进行base64编码。

安装file-loader

我们将背景图片改成bg.jpg,重新编译运行或报错。

因为图片大小超过limit属性值时会通过file-loader进行处理。所以我们需要安装file-loader。

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

再次打包,就会发现dist文件夹下多了一个图片文件。

修改文件名称

我们发现webpack自动帮助我们生成一个非常长的名字,这是一个32位hash值,目的是防止名字重复。但是,真实开发中,我们可能对打包的图片名字有一定的要求,比如,将所有的图片放在一个文件夹中,跟上图片原来的名称,同时也要防止重复。所以,我们可以在options中添加上如下选项:

img:文件要打包到的文件夹。

name:获取图片原来的名字,放在该位置。

hash:8:为了防止图片名称冲突,依然使用hash,但是我们只保留8位。

ext:使用图片原来的扩展名。

{
        test: /\.(png|jpg|gif)$/,
        use: [
            {
                loader: 'url-loader',
                options: {
                    limit: 8192,
                    name:'img/[name].[hash:8].[ext]'
                }
            }
        ]
}

但是,重新编译后图片并没有显示出来,这是因为图片使用的路径不正确。默认情况下,webpack会将生成的路径直接返回给使用者。但是,我们整个程序是打包在dist文件夹下的,所以这里我们需要在路径下再添加一个dist/

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

注意:并不需要配置file-loader文件

babel的使用

ES6语法处理

如果你仔细阅读webpack打包的js文件,会发现写的ES6语法并没有转成ES5,那么就意味着可能一些对ES6还不支持的浏览器没有办法很好的运行我们的代码。如果希望将ES6的语法转成ES5,那么就需要使用babel。在webpack中,我们直接使用babel对应的loader就可以了。

安装babel-loader

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

配置webpack.config.js

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

重新打包,查看bundle.js发现其中的内容就变成了ES5的语法了。

webpack配置Vue

引入Vue.js

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

npm install vue@2.5.21 --save

接下来就可以使用Vue了。

打包项目错误信息

修改完成后重新打包运行程序报错了。

这个错误说我们使用的时runtime-only版本的Vue,解决这个错误只需添加一个webpack的配置即可。

resolve:{
        alias:{
            'vue$':'vue/dist/vue.esm.js'
        }
    }

runtime-only版本不支持含有template的代码,包括最顶层的Vue实例模板。

el和template的区别

正常运行之后,我们来考虑另外一个问题:如果我们希望将data中的数据显示在界面中,就必须是修改index.html。如果我们后面自定义了组件,也必须修改index.html来使用组件。但是html模板在之后的开发中,我并不希望手动频繁修改,是否可以做到呢?

定义template属性:在前面的Vue实例中,我们定义了el属性,用于和index.html中的#app进行绑定,让Vue实例之后可以管理它其中的内容。这里,我们可以将div元素中的{{message}}内容删掉,只保留一个基本的id为div的元素。但是如果我依然希望在其中显示{{message}}的内容,应该怎么处理呢?

我们可以再定义一个template属性,代码如下:

new Vue({
    el:'#app',
    template:'<div id="app">{{message}}</div>',
    data:{
        message:'時光'
    }
})

重新打包,运行程序,显示一样的结果和HTML代码结构。那么,el和template模板的关系是什么呢?我们知道el用于指定Vue要管理的DOM,可以帮助解析其中的指令、事件监听等。而如果Vue实例中同时指定了template,那么template模板的内容会替换掉挂载的对应el的模板。这样做之后我们就不需要在以后的开发中再次操作index.html,只需要在template中写入对应的标签即可。

还可以将template模板中的内容进行抽离。分成三部分:template、script、style,结构就会变得非常清晰。

const App = {
    template:`
     <div>
          <h2>{{message}}</h2>
          <button @click="btnClick">点我</button>
          <h2>{{name}}</h2>
      </div>
    `,
    data(){
        return {
            message:'hello webpack',
            name:'shiguang'
        }
    },
    methods:{
        btnClick(){

        }
    }
}

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

组件化开发引入

在外部新建一个app.js作为模板文件导出

export default {
    template:`
     <div>
          <h2>{{message}}</h2>
          <button @click="btnClick">点我</button>
          <h2>{{name}}</h2>
      </div>
    `,
    data(){
        return {
            message:'hello webpack',
            name:'shiguang'
        }
    },
    methods:{
        btnClick(){

        }
    }
}

在使用Vue实例的js文件中导入

import App from './vue/app'

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

模板内容抽离

新建一个App.vue的组件,将模板中的内容进行抽离

<template>
  <div>
    <h2 class="title">{{message}}</h2>
    <button @click="btnClick">点我</button>
    <h2>{{name}}</h2>
  </div>
</template>

<script>
export default {
  name: "App",
  data(){
    return {
      message:'hello webpack',
      name:'shiguang'
    }
  },
  methods:{
    btnClick(){

    }
  }
}
</script>

<style scoped>
.title{
  color: green;
}
</style>

在使用Vue实例的js文件中导入

import App from './vue/App.vue'

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

此时重新编译会报错,需要安装vue-loadervue-template-compiler

npm install --save-dev vue-loader@15.4.2 vue-template-compiler@2.5.21

webpack.config.js中添加配置

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

安装并配置之后依然会报错,因为vue-loader在15版本之后需要配合一个 webpack 插件才能正确使用。

有两种解决方案,可以降低vue-loader的版本,或者安装webpack插件

方法一:降低vue-loader版本

你可以手动更改package.json中的vue-loadre版本到v15以下,然后执行npm install更新依赖

或者执行npm uninstall vue-loader卸载原来版本,再安装一个v15以下版本的vue-loader

比如安装v14.0.0 : npm install --save-dev vue-loader@14.0.0

方法二:安装webpack插件

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

到Vue官网 => 生态系统 => 工具 => Vue Loader => 如何从v14迁移有详细的安装说明

需要在webpack.config.js中添加如下代码

// webpack.config.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  // ...
  plugins: [
    new VueLoaderPlugin()
  ]
}

重新编译后就可以正常使用了

webpacke plugin

plugin是什么

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

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

loader和plugin区别

loader主要用于转换某些类型的模块,它是一个转换器。

plugin是插件,它是对webpack本身的扩展,是一个扩展器。

plugin的使用过程

步骤一:通过npm安装需要使用的plugins(某些webpack已经内置的插件不需要安装)

步骤二:在webpack.config.js中的plugins中配置插件。

BannerPlugin

作用

为打包的文件添加版权声明。(属于webpack自带的插件)

使用方法

webpack.config.js中引入webpack依赖并配置

const webpack = require('webpack')

module.exports = {
    ...
    
    plugins: [
        new webpack.BannerPlugin('最终版权归xxx所有')
    ]
}

HtmlWebpackPlugin

作用

  • 自动生成一个index.html文件(可以指定模板来生成)

  • 将打包的js文件,自动通过script标签插入到body

安装

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

使用

webpack.config.js中引入依赖并配置plugins内容

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    ...
    
    plugins: [
        new HtmlWebpackPlugin({
            template:'index.html'
        })
    ]
}

template 表示根据什么模板来生成html页面。

template:'index.html' 表示自动在webpack.config.js所在目录下寻找名为index.html的页面作为模板。

注意:结合在output中添加的publicPath属性,否则插入的script标签中的src可能会有问题。

uglifyjs-webpack-plugin

作用

对打包的js文件进行压缩。

安装

此处使用1.1.1版本,和CLI2保持一致。

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

使用

webpack.config.js中引入依赖并配置plugins内容

const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')

module.exports = {
    ...
    
    plugins: [
       new UglifyjsWebpackPlugin()
    ]
}

压缩之后的效果

此插件开发阶段不建议使用,压缩后的文件变量名会被替换为特殊字符,不利于调试

搭建本地服务器

webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,

可以让浏览器自动刷新显示我们修改后的结果。

它是一个单独的模块,在webpack中使用之前需要先安装它。

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

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

contentBase:为哪一个文件夹提供本地服务,默认是根文件夹

port:端口号 默认为8080

inline:页面实时刷新

historyApiFallback:在SPA页面中,依赖HTML5的history模式

webpack.config.js文件配置修改如下

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

因为服务并未安装到全局,所以可以找到指令所在目录运行

也可以将指令配置到package.json中的scripts

还可以添加一些参数 --open会自动在浏览器打开,不添加--open参数会返回服务的地址

使用npm run dev运行服务

终止命令:ctrl+c

此插件仅用于开发阶段,生产阶段并不需要

webpack配置分离

我们可以将webpack.config.js中的配置分离出来,可分为三部分:开发阶段和生产阶段都要用到的基础配置(本例中为base.config.js),开发阶段用到的配置(本例中为dev.config.js)和生产阶段用到的配置(本例中为prod.config.js)

安装webpack-merge

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

使用时导入依赖

const webpackMerge = require('webpack-merge')

base.config.js

const path = require('path')

const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
    //入口:可以是字符串/数组/对象,这里我们入口只有一个,所以写一个字符串即可
    entry:'./src/main.js',
    //出口:通常是一个对象,里面至少包含两个重要属性,path和filename
    output:{
        path:path.resolve(__dirname,'dist'),
        filename:'bundle.js',
        // publicPath:'dist/'
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
            },
            {
                test: /\.less$/,
                use: [{
                    loader: "style-loader" // creates style nodes from JS strings
                }, {
                    loader: "css-loader" // translates CSS into CommonJS
                }, {
                    loader: "less-loader" // compiles Less to CSS
                }]
            },
            {
                test: /\.(png|jpg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 8192,
                            name:'img/[name].[hash:8].[ext]'
                        }
                    }
                ]
            },
            {
                test: /\.js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }
            },
            {
                test:/\.vue$/,
                use:['vue-loader']
            }

        ],
    },
    resolve:{
        alias:{
            'vue$':'vue/dist/vue.esm.js'
        }
    },
    plugins: [
        new VueLoaderPlugin(),
        new webpack.BannerPlugin('仅用于学习和交流,请勿用于商业用途,最终版权归時光所有'),
        new HtmlWebpackPlugin({
            template:'index.html'
        })
    ]
}

dev.config.js

const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')

module.exports = webpackMerge(baseConfig,{
    devServer:{
        contentBase:'./dist',
        inline:true
    }
})

prod.config.js

const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')


module.exports = webpackMerge(baseConfig,{
    plugins: [
        new UglifyjsWebpackPlugin()
    ]
})

将配置文件分离后,删除原来的webpack.config.js,运行之前配置好的指令会报错。

因为我们需要为package.json中的指令指明分离出来的配置文件路径。

设置好路径之后指令就可以正常运行了

注意:原来的webpack.config.js配置文件在根目录,编译时将文件打包到根目录的dist文件夹内

现将配置文件抽离到了根目录下的build文件夹内,所以需要重新修改编译时的打包路径

Vue CLI

如果你只是简单地写几个简单的demo程序,那么你不需要CLI

如果你要开发大型项目,那么你必须要使用Vue CLI

使用Vue.js开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。

如果每个项目都要手动完成这些工作,效率比较低效,所以通常我们会使用一些脚手架工具来帮助完成这些事情。

CLI是什么

CLI是Command-Line Interface, 翻译为命令行界面,俗称脚手架

Vue CLI是一个官方发布 vue.js 项目脚手架。

使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置。

Vue CLI使用前提

前提一:NodeJs

安装NodeJS

网址: http://nodejs.cn/download/

检测安装的版本

默认情况下自动安装Node和NPM

Node环境要求8.9以上或者更高版本

使用命令node -v查看当前Node版本

什么是NPM

NPM的全称是 Node Package Manager

是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。

可以使用NPM安装一些开发过程中需要的依赖包.

cnpm

由于国内直接使用 npm 的官方镜像非常慢,推荐使用淘宝 NPM 镜像。

可以使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm:

npm install -g cnpm --registry=https://registry.npm.taobao.org

这样就可以使用 cnpm 命令来安装模块了

cnpm install [name]

前提二:webpack

Vue.js官方脚手架工具使用了webpack模板

  • 对所有的资源进行压缩等优化操作

  • 它在开发过程中提供了一套完整的功能,能够使开发过程变得高效。

Webpack的全局安装

npm install webpack -g

Vue CLI的使用

安装Vue脚手架

npm install -g @vue/cli

查看当前版本: vue --version

拉取 2.x 模板 (旧版本)

Vue CLI >= 3 和旧版使用了相同的 vue 命令,所以 Vue CLI 2 (vue-cli) 被覆盖了。如果你仍然需要使用旧版本的 vue init 功能,你可以全局安装一个桥接工具:

npm install -g @vue/cli-init

初始化项目

Vue CLI2

vue init webpack my-project

Vue CLI3及以上

vue create my-project

Vue CLI2详解

初始化项目详解

初始化项目命令

vue init webpack my-project

初始化项目选项介绍

目录结构详解

关闭Eslint检查,将config/index.js 下的useEslint改为false重新编译即可。

Runtime-Compiler和Runtime-only的区别

在本地初始化两个项目,一个采用runtime+compiler运行环境,一个采用runtime-only运行环境。

使用npm run dev指令创建一个本地服务。

执行效果如下

选用两种不同的模式产生的main.js对比

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

这里就要说明一下template在vue内部是如何进行渲染的,下图是vue程序运行过程。

当我们把template传给vue时,vue会将其保存至options中,然后解析成ast(abstract syntax tree ,即抽象语法树),然后将ast编译为render函数,进而将其转为虚拟Dom树,最终渲染为真是Dom。

runtime-compiler模式下执行步骤:
template -> ast -> render -> virtual dom -> 真实Dom

runtime-only模式下执行步骤
render -> virtual dom -> 真实Dom

明显可以看出runtime-only模式执行步骤更少执行效率更高,这也就意味着用于处理事务逻辑的代码量更少,使用runtime-only模式时,App.vue中的template最终会被vue-template-compiler解析为render函数。在main.js中使用import App from './App'引入的App并非template而是其被解析后的render函数。

所以说,在开发中如果依然使用template,就需要选择Runtime-Compiler,如果使用.vue文件夹开发,可以选择Runtime-only

接下来简单了解一下render函数的使用。

//main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false

/* eslint-disable no-new */

const cpn ={
  template:  `<div> {{message}}</div>`,
  data(){
    return {
      meessage:'hello vue'
    }
  }
}
new Vue({
  el: '#app',
  // 1.普通用法:createElement('标签',{标签的属性},[])
  // render:function (creatElement){
  //   return creatElement('h2',
  //     {
  //       class:'box'
  //     },
  //     ['hello',creatElement('button',['我是按钮'])]
  //   )
  // }

    // 2.传入组件对象
  // render:function (createElement){
  //   return createElement(cpn)
  // }


  render:function (createElement){
    return createElement(App)
  }
})

具体可参考:https://blog.csdn.net/kkae8643150/article/details/52910389

npm run buildnpm run dev 执行流程

npm run build

npm run dev

webpack.base.conf.js起别名

resolve:{
  extensions:['.js','.vue','.json'],
  alias: {
    '@':resolve('src'),
    'pages':resolve('src/pages'),
    'common':resolve('src/common'),
    'components':resolve('src/components'),
    'network':resolve('src/network')
  }
}
posted @ 2021-01-21 12:18  時光心向阳  阅读(225)  评论(0编辑  收藏  举报