0%

Vue学习笔记(二)-组件

2 Vue组件

2.1 简介

人面对复杂问题的处理方式:

  • 任何一个人处理信息的逻辑能力都是有限的
  • 所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容
  • 但是,我们人有一种天生的能力,就是将问题进行拆解。口如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。

组件也是似的思想:

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

组件化是Vue.js中的重要思想:

  • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
  • 任何的应用都会被抽象成一棵组件树

components

组件化思想的应用:

  • 尽可能的将页面拆分成一个个小的、可复用的组件
  • 让我们的代码更加方便组织和管理,并且扩展性也更强

2.2 组件开发的步骤

组件的使用分成三步:

  • 创建组件构造器
  • 注册组件
  • 使用组件

即:

  1. 调用Vue.extend() 方法,创建组件构造器
  2. 调用Vue.component() 方法,注册组件
  3. 在Vue实例的作用范围内使用组件

示例:

<div id="app">
<!-- 3.使用组件 -->
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
</div>
<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: {
}
})
</script>
  1. Vue.extend:
  • 调用Vue.extend)创建的是一个组件构造器。

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

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

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

2.Vue.component

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

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

如果需要使组件只能局部使用而非全局使用,则在对应的vue实例中注册它即可:

const app = new Vue({
el: '#app',
components: {
cpn: cpnC // 局部注册组件
},
data: {
}
})

2.3 父子组件

前面我们看到了组件树:

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

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

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

注册组件语法糖写法

// 1. 创建组件构造器对象并使用语法糖形式注册
Vue.component('cpn1', {
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容1</p>
<p>我是内容1</p>
</div>`})

组件模板抽离写法

直接上代码:

<body>
<div id="app">
<!-- 组件使用 -->
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>

<!-- 一个写好的组件 -->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
</script>

<script src="./js/vue.js"></script>
<script>
// 全局引用这个组件
Vue.component('cpn', { // 语法糖写法
template: '#cpn' // 使用对应id名挂载
})
</script>
</body>

template写法(最常见)

其实和前面一样,只是用template标签包裹对应组件内容

<template id='cpn'>
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
</template>

之后像前面那样注册挂载即可

2.4 组件可以访问Vue实例数据吗

组件时一个单独功能模块的封装:

  • 这个模块有属于自己的HTML模板,也应该有属性自己的数据data

组件中的数据是保存在哪里呢?顶层的Vue实例中吗?

  • 我们先来测试一下组件中能不能直接访问Vue实例中的data:

    <template id="myCpn">
    <div>消息: {{ message }}</div>
    </template>
    <script>
    let app = new Vue({
    el: '#app',
    data: {
    message: "hello world"
    },
    components: {
    'my-cpn': {
    template: 'myCpn'
    }
    }
    })
    </script>

    解析

    组件去访问message,message定义在Vue,我们发现最终并没有显示结果

    结论: 组件不能直接访问Vue实例中的data数据

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

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

2.5 组件中的data为什么必须是函数

(待补充)

2.6 父子组件间传值

在前面我们提到了子组件是不能引用父组件或者Vue实例的数据的

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

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

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

  1. 通过props向子组件传递数据
  2. 通过事件向父组件发送消息

image-20200131121652504

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

<div id="app">
<!-- 组件实例, 注意此处要使用v-bind语法绑定变量 -->
<cpn :child-movies="movies" :child-message="message"></cpn>
</div>

<template id="cpn">
<div>
<p>{{childMovies}}</p>
<h2>{{childMessage}}</h2>
</div>
</template>

<script src="./js/vue.js"></script>
<script>
<!-- 父传子使用props -->
const cpn = {
template: '#cpn',
props: ['childMovies', 'childMessage'],
data() {
return {}
}
}

const app = new Vue({
el: '#app', // 用于挂载要管理的元素
data: { // 定义数据
message: "你好啊",
movies: ["海王", "海贼王", "上海贼王"]
},
components: {
cpn
}
})
</script>

props基本用法

在组件中,使用选项props来声明需要从父级接收到的数据

props的值有两种方式:

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

props数据验证

在前面的示例中,我们的props选项是使用一个数组;我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型验证时,就需要对象写法了

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

type 可以是下列原生构造函数中的一个:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol

你可以为 props 中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:

Vue.component('my-component', {
props: {
// 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})

子级向父级传递事件

props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中,这个时候我们需要使用自定义事件来完成

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

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

自定义事件的流程

  • 在子组件中,通过$emit()来触发事件
  • 在父组件中,通过v-on来监听子组件事件

来看一个简单的例子:

<div id="app">
<!-- 组件实例 -->
<cpn @item-click="cpnClick"></cpn>
</div>

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

<script src="./js/vue.js"></script>
<script>
<!-- 父传子使用props -->
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{ id: 1, name: "亚洲高清" },
{ id: 2, name: "欧美经典" },
{ id: 3, name: "国产自拍" },
{ id: 4, name: "日语学习" }
]
}
},
methods: {
btnClick(item) {
console.log(item)
this.$emit('item-click', item) //item-click是要向父组件传递事件的名称,后面的item是向父组件传递的参数
}
}
}

const app = new Vue({
el: '#app', // 用于挂载要管理的元素
data: { // 定义数据

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

2.7 父子组件的访问方式

$children

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

  • 父组件访问子组件: 使用$children 或 $refs
  • 我们先来看下$children的访问
    • this.$children是一个数组类型,它包含所有子组件对象
    • 我们这里通过一个遍历,取出所有子组件的message状态
<div id="app">
<!-- 组件实例 -->
<cpn></cpn>
<cpn></cpn>
<cpn ref="ccc"></cpn> # 给组件创建一个ref命名
<button @click="btnClick">按钮</button>
</div>

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

<script src="./js/vue.js"></script>
<script>
<!-- 父传子使用props -->
const cpn = {
template: '#cpn',
methods: {
showMessage() {
console.log('show message')
}
}
}

const app = new Vue({
el: '#app', // 用于挂载要管理的元素
components: {
cpn
},
data: {
message: '你好呀'
},
methods: {
btnClick() {
// 1. $children方式
// console.log(this.$children)
// for (let i in this.$children) {
// console.log(i)
// this.$children[i].showMessage()
// }

// 2.$refs => 对象类型,默认是一个空的对象
// console.log(this.$refs.ccc)
// this.$refs.ccc.showMessage()
}
}
})

在日常的开发中一般使用refs方式较多

2.8 子组件访问父组件(或根组件)

直接看代码吧(一般很少用):

components: {
ccpn: {
template: '#cpn',
methods: {
btnClick() {
console.log(this.$parent) //访问父组件,一般不建议这样使用,会导致耦合性增强,当组件复用等情况下会出现难以预料的问题
console.log(this.$parent.name) // 访问父组件的name属性
console.log(this.$root) // 访问根组件的Vue实例
}
}
}
}

2.9 Slot插槽

slot翻译为插槽

  • 在生活中很多地方都有插槽,比如USB插槽等
  • 插槽的目的是让我们原来的设备具备更多的扩展性
  • 比如电脑的USB插槽可以让我们插入U盘、硬盘、手机等等

组件的插槽

  • 组件的插槽也是为了让我妈封装的组件更加具有扩展性
  • 让使用者可以决定组件内部的一些内容到底展示什么

例子:移动网站中的导航栏

  • 移动开发中,几乎每个页面都有导航栏
  • 导航栏我们必然会封装成一个插件,比如nav-bar组件
  • 一旦有了这个组件,我们就可以在多个页面复用了

image-20200201222644659

如何封装这类组件呢?

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

具体如何封装呢?

核心思想是抽取共性,保留不同

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

这就是我们为什么要学习组件插槽的原因

2.9.1 插槽的基本使用

插槽的基本使用非常简单:

<template id="cpn">
<div>
<h2>我是组件</h2>
<slot></slot>
<p>我是组件的内容,哈哈哈</p>
</div>
</template>

在组件中预留<slot>标签

<div id="app">
<!-- 组件实例 -->
<cpn>
<button>我是插槽中按钮</button> # 插槽中放入button
</cpn>
<cpn>
<span>我是插槽中span</span> # 插槽中放入span
</cpn>
<cpn></cpn>
</div>

插槽中通过插入不同的功能,实现不同的需求

image-20200202223754164

如果插槽中有某个功能是经常重复使用的时候,可以给插槽设置默认值:

<slot><p>我是插槽中的默认内容</p></slot>

这样如果使用插槽时不覆盖,则显示的是默认内容

<cpn>
<button>我是插槽中按钮</button> # 显示按钮
</cpn>
<cpn>
<span>我是插槽中span</span> # 显示span
</cpn>
<cpn></cpn> # 显示默认的<p>标签

2.9.2 具名插槽

有时候一个组件中不止一个插槽,可以通过给插槽命名的方式,来指定修改哪一个插槽的内容:

<div id="app">
<!-- 组件实例 -->
<cpn>
<span slot="left">左边被我改了</span>
<span slot="right">右边被我改了</span>
</cpn>
</div>

<template id="cpn">
<div>
<slot name="left"><p>我是插槽中左边的默认内容</p></slot>
<slot name="center"><p>我是插槽中中间的默认内容</p></slot>
<slot name="right"><p>我是插槽中右边的默认内容</p></slot>
</div>
</template>

如上,通过指定slot的name,可以修改指定插槽的内容,避免混乱。效果:

image-20200202230145397

2.9.3 编译作用域

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

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

<div id="app">
<cpn v-show="isShow"></cpn>
</div>

<template id="cpn">
<div>
<h2>我能不能显示出来呢</h2>
</div>
</template>

<script src="./js/vue.js"></script>
<script>
<!-- 父传子使用props -->
const cpn = {
template: '#cpn',
data() {
return {
isShow: false
}
}
}

const app = new Vue({
el: '#app', // 用于挂载要管理的元素
components: {
cpn
},
data: {
message: '你好呀',
isShow: true
}
})
</script>

在Vue实例中的isShow为true,而子组件的isShow为false,那么组件最终是否会被渲染呢? 答案是最终可以被渲染出来,也就是说v-show使用的是Vue实例中的isShow属性,而非子组件中的isShow属性。

为什么呢?

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

而我们在使用<cpn v-show="isShow"></cpn>的时候,整个组件的使用过程相当于在父组件中出现的

那么它的作用域就是父组件,使用的属性也是父组件的属性

因此,isShow使用的是Vue实例中的属性,而不是子组件的属性

2.9.4 作用域插槽

作用域插槽是slot一个比较难以理解的点 ,官方文档说的也不十分清晰

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

  • 父组件替换插槽的标签,但是内容由子组件来提供

我们先提一个需求:

  • 子组件中包括一组数据,比如:[‘JavaScript’, ‘Python’, ‘Go’, ‘Java’]
  • 需要在多个界面展示:
    • 某些界面是以水平方向展示的
    • 某些界面是以列表形式展示的
    • 某些界面直接展示一个数组

内容在子组件,希望父组件告诉我们如何展示,怎么办呢

  • 利用slot作用域插槽就可以了
<div id="app">
<!-- 以slot默认的列表形式展示 -->
<cpn></cpn>
<!-- 父组件想要获得子组件的language, 并以字符串形式展示 -->
<cpn>
<template slot-scope="slot">
<span>{{slot.data.join(' - ')}}</span>
</template>
</cpn>
</div>

<template id="cpn">
<div>
<slot :data="language">
<ul>
<li v-for="item in language">{{item}}</li>
</ul>
</slot>
</div>
</template>

<script src="./js/vue.js"></script>
<script>
<!-- 父传子使用props -->
const cpn = {
template: '#cpn',
data() {
return {
language: ['JavaScript', 'Python', 'Go', 'C']
}
}
}

欢迎关注我的其它发布渠道