1 认识组件化
1.1 什么是组件化
人面对复杂问题的处理方式:
任何一个人处理信息的逻辑能力都是有限的。所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
但是,我们人有一种天生的能力,就是将问题进行拆解。如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
组件化也是类似的思想︰
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。
我们将一个完整的页面分成很多个组件。每个组件都用于实现页面的一个功能块。而每一个组件又可以进行细分。
3.1.2 Vue组件化思想
- 组件化是Vue.js中的重要思想
- 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
- 任何的应用都会被抽象成一棵组件树。
- 组件化思想的应用:
- 有了组件化的思想,我们在之后的开发中就要充分的利用它
- 尽可能的将页面拆分成一个个小的、可复用的组件。
- 这样让我们的代码更加方便组织和管理,并且扩展性也更强。
2 组件化基础
注册组件的基本步骤
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
- 我们来看看通过代码如何注册组件查看运行结果:
口和直接使用一个div看起来并没有什么区别。
口但是我们可以设想,如果很多地方都要显示这样的信息,我们是不是就可以直接使用来完成呢?
2.1 注册组件
2.1.1 注册的基本步骤
<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>
// ES6中新加了 ``包裹字符串,可以换行
// 1.创建组件构造器对象
const cpnConstructor = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
<p>内容2</p>
</div>`
})
// 2.注册组件
// 参数1:组件的标签名
// 参数2:组件构造器对象
Vue.component('my-cpn', cpnConstructor);
const app = new Vue({
el:'#app',
})
</script>
注册组件步骤解析
这里的步骤都代表什么含义呢?
1.Vue.extend() :
- 调用Vue.extend()创建的是一个组件构造器。
- 通常在创建组件构造器时,传入template代表我们自定义组件的模板。
- 该模板就是在使用到组件的地方,要显示的HTML代码。
- 事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。
2.vue.component() :
- 调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
- 所以需要传递两个参数:1、注册组件的标签名2、组件构造器
3.组件必须挂载在某个Vue实例下否则它不会生效。(见下页)
- 我们来看下面我使用了三次
- 而第三次其实并没有生效:
2.1.2 全局和局部组件
全局组件:在Vue的实例之外将构造器注册为组件。
可以在多个Vue的实例下面使用
局部组件:在Vue的实例中,使用templates选项将构造器注册为组件。
只可以在注册为组件的实例下面使用。
<div id="app">
<!-- 3.使用组件 -->
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// ES6中新加了 ``包裹字符串,可以换行
// 1.创建组件构造器对象
const cpnConstructor = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
<p>内容2</p>
</div>`
})
const app = new Vue({
el:'#app',
components: {
// 2.注册组件
// cpn:使用组件时的标签名;cpnConstructor:组件构造器对象
cpn: cpnConstructor
}
})
</script>
在实际开发中,用的最多的是局部组件,一般只有一个Vue实例。
2.1.2 父组件和子组件
在前面我们看到了组件树:组件和组件之间存在层级关系,而其中一种非常重要的关系就是父子组件的关系。
我们来看通过代码如何组成的这种层级关系:
<div id="app">
<!-- 3.使用组件 -->
<cpn2></cpn2>
<!-- 不能以子标签的形式在Vue实例中使用,会报错 -->
<!-- <cpn1></cpn1> -->
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建cpnC1构造器--子组件构造器
const cpnC1 = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
</div>`
})
// 2.创建cpnC2构造器--父组件构造器
const cpnC2 = Vue.extend({
template: `
<div>
<h2>标题</h2>
<p>内容2</p>
<cpn1></cpn1>
</div>
`,
// 在父组件构造器中,注册子组件
components: {
cpn1: cpnC1
}
})
const app = new Vue({
el:'#app',
// 在Vue实例app中注册父组件
components: {
// cpn:使用组件时的标签名;cpnConstructor:组件构造器对象
cpn2: cpnC2
}
})
</script>
父子组件错误用法:以子标签的形式在Vue实例中使用
因为当子组件注册到父组件的components时,Vue会编译好父组件的模块
该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)
<child-cpn></child-cpn>
是只能在父组件中被识别的。
类似这种用法(以子标签的形式在Vue实例中使用),<child-cpn></child-cpn>
是会被浏览器忽略的。
2.1.2 注册组件语法糖
Vue提供注册的语法糖,简化过程。
主要是省去了调用Vue.extend()的步骤,而是直接使用一个对象来代替。(内部传给了extend方法)
- 全局组件注册的语法糖
<div id="app">
<!-- 使用组件 -->
<cpn></cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.全局组件注册的语法糖
Vue.component('cpn',{
template: `
<div>
<h2>标题</h2>
<p>内容1</p>
</div>`
});
const app = new Vue({
el:'#app'
})
</script>
- 注册局部组件的语法糖
<div id="app">
<!-- 使用组件 -->
<cpn2></cpn2>
</div>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
// 注册局部组件的语法糖
components: {
cpn2: {
template: `
<div>
<h2>标题</h2>
<p>内容2</p>
</div>`
}
}
})
</script>
2.1.2 模板的分离写法
在JS代码里,template模板写法中,有很多HTML模板,看起来很乱。
使用分离写法,将模板分离出来写,挂载到对应的组件上,结构会更加清晰。
Vue 提供了两种方案定义HTML模板内容:
- 使用
<script>
标签
<div id="app">
<!-- 使用组件 -->
<cpn></cpn>
</div>
<!-- 1.使用script标签定义模板内容 -->
<!-- type 设置为"text/x-template"-->
<!-- id 设置为标签名-->
<script type="text/x-template" id="myCpn">
<div>
<h2>标题</h2>
<p>内容2</p>
</div>
</script>
<script src="../js/vue.js"></script>
<script>
// cpn是标签名
Vue.component('cpn',{
// 因为是id,所以用id选择器,id名前加上“#“
template: '#myCpn'
});
const app = new Vue({
el:'#app'
})
</script>
局部组件的模板分离写法:
<div id="app">
<!-- 使用组件 -->
<cpn></cpn>
</div>
<!-- 1.使用script标签定义模板内容 -->
<!-- type 设置为"text/x-template"-->
<!-- id 设置为标签名-->
<script type="text/x-template" id="myCpn">
<div>
<h2>标题</h2>
<p>内容2</p>
</div>
</script>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
// 注册局部组件的语法糖
components: {
cpn: {
template: myCpn
}
}
})
</script>
- 使用
<template>
标签
<div id="app">
<!-- 使用组件 -->
<cpn></cpn>
</div>
<!-- 2.使用template标签 -->
<template id="myCpn">
<div>
<h2>标题</h2>
<p>内容2</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 1.全局组件注册的语法糖
Vue.component('cpn',{
template: '#myCpn'
});
const app = new Vue({
el:'#app'
})
</script>
2.1.2 组件的其他属性
data属性
组件是一个单独功能模块的封装,有属于自己的HTML模板,也应该有属于自己的数据data。
组件中不能访问Vue实例中的data,通过组件自己的data选项,保存数据。
组件数据的存放
组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
只是这个data属性必须是一个函数
而且这个函数返回一个对象,对象内部保存着数据
<script>
// 1.全局组件注册的语法糖
Vue.component('cpn',{
template: '#myCpn',
data() {
return {
title: 'hello'
}
}
});
const app = new Vue({
el:'#app'
})
</script>
为什么组件data必须是函数
2.2 数据传递
-
子组件是不能引用父组件或者Vue实例的数据的。
-
但是,在开发中,往往一些数据确实需要从上层传递到下层︰
- 比如在一个页面中,我们从服务器请求到了很多的数据。
- 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
- 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组伟)将数据传递给小组件(子组件)。
-
如何进行父子组件间的通信呢?Vue官方提到
- 通过props向子组件传递数据
- 通过事件向父组件发送消息
Patent(父组件)
Child([水组件)
-----$emit Events------
在下面的代码中,我直接将Vue实例当做父组件,并且其中包含子组件来简化代码。
真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的。
父子组件的访问方式:$children
- 有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
- 父组件访问子组件:使用
$children
或$refs
- 子组件访问父组件:使用
$parent
- 父组件访问子组件:使用
- 我们先来看下
$children
的访问this.$children
是一个数组类型,它包含所有子组件对象。- 我们这里通过一个遍历,取出所有子组件的
message
状态。
<div id="app">
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
<button @click="btnClick">按钮</button>
</div>
<template id="cpn">
<div>我是子组件</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data: {
message: 'hello'
},
methods: {
btnClick() {
console.log(this.$children);
for (let item of this.$children) {
console.log(item.name);
item.showMessage();
}
}
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是子组件的name'
}
},
methods: {
showMessage() {
console.log('showMessage');
}
}
}
}
})
</script>
$children需要通过下标值去拿子组件,在实际开发中下标值会变化,用的非常少(比如拿到所有的子组件)。
父子组件的访问方式:$refs
<div id="app">
<cpn></cpn>
<cpn></cpn>
// 在该子组件上加上ref属性
<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: 'hello'
},
methods: {
btnClick() {
// $refs 默认是空白的
// 在某个组件上加一个ref属性,可以通过this.$refs.aaa(属性值)拿到该组件
console.log(this.$refs.aaa);
}
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是子组件的name'
}
},
methods: {
showMessage() {
console.log('showMessage');
}
}
}
}
})
</script>
子访问父:$parent
和$root
<div id="app">
<cpn></cpn>
</div>
<template id="cpn">
<div>
<ccpn></ccpn>
</div>
</template>
<template id="ccpn">
<div>
<h2>我是子组件</h2>
<button @click="btnClick">按钮</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el:'#app',
data: {
name: '我是根组件的name'
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是父组件的name'
}
},
components: {
ccpn: {
template: '#ccpn',
methods: {
btnClick() {
// 1.访问父组件 $parent
console.log(this.$parent);
console.log(this.$parent.name);
// 2.访问根组件 $root
console.log(this.$root);
console.log(this.$root.name);
}
}
}
}
}
}
})
</script>
3.组件化高级
3.1 插槽slot
3.1.1 编译作用域
3.1.1 为什么使用slot
-
slot(发音为[slɒt])翻译为插槽:
- 在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
- 插槽的目的是让我们原来的设备具备更多的扩展性。
- 比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
-
组件的插槽:
- 组件的插槽也是为了让我们封装的组件更加具有扩展性。
- 让使用者可以决定组件内部的一些内容到底展示什么。
-
例子︰移动网站中的导航栏。
- 移动开发中,几乎每个页面都有导航栏。
- 导航栏我们必然会封装成一个插件,比如nav-bar组件。
- 一旦有了这个组件,我们就可以在多个页面中复用了。
-
但是,每个页面的导航是一样的吗?No,我以京东M站为例
-
如何去封装这类的组件呢?
- 它们也很多区别,但是也有很多共性。
- 如果,我们每一个单独去封装一个组件,显然不合适︰比如每个页面都返回,这部分内容我们就要重复去封装。
- 但是,如果我们封装成一个,好像也不合理︰有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等。
-
如何封装合适呢?抽取共性,保留不同。
- 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
- 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
- 是搜索框,还是文字,还是菜单。由调用者自己来决定。
这就是为什么我们要学习组件中的插槽slot的原因。
3.1.1 slot的基本使用
- 插槽的基本使用:在组件里定义一个
<slot></slot>
,在使用该组件时,在中间插入要替换的元素。 - 插槽的默认值:
<slot><button>按钮</button></slot>
。如果在该组件中没有插入任何其他内容,就默认显示默认值。 - 如果有多个值,同时放入到组件进行替换时,一起作为替换元素。
<div id="app">
<cpn><button>按钮</button></cpn>
<cpn><span>span</span></cpn>
<cpn>
<!-- 3.如果有多个值,同时放入到组件进行替换时,一起作为替换元素。 -->
<i>i</i>
<div>div</div>
<p>p</p>
</cpn>
<cpn></cpn>
<cpn></cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>我是组件h2</h2>
<p>我是组件p</p>
<!-- 1.插槽的基本使用:在组件里定义一个`<slot></slot>` -->
<!-- 2.插槽的默认值:`<slot><button>按钮</button></slot>` -->
<slot><button>按钮</button></slot>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
components: {
cpn: {
template: '#cpn'
}
}
})
</script>
3.1.1 slot的具名插槽
- 当子组件的功能复杂时,子组件的插槽可能并非是一个。
- 比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
- 那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
- 这个时候,我们就需要给插槽起一个名字
- 如何使用具名插槽呢?
- 非常简单,只要给slot元素一个
name
属性即可 <slot name='myslot'></slot>
- 非常简单,只要给slot元素一个
示例代码如下:
<div id="app">
<cpn>
<!-- 只会替换没有名字的slot -->
<span>标题</span>
<!-- 只会替换名字为left的slot -->
<button slot="left">按钮</button>
<span slot="center">替换</span>
<p slot="right">p</p>
</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',
components: {
cpn: {
template: '#cpn'
}
}
})
</script>
3.1.1 slot作用域插槽
3.1.1.1 编译作用域
在真正学习插槽之前,我们需要先理解一个概念∶编译作用域。
官方对于编译的作用域解析比较简单,我们自己来通过一个例子来理解这个概念︰我们来考虑下面的代码是否最终是可以渲染出来的:
<my-cpn v-show="isShow"></my-cpn>
中,我们使用了isShow属性。isShow属性包含在组件中,也包含在Vue实例中。
<div id="app">
<!-- isShow使用的是实例中的属性,而不是子组件的属性-->
<!-- 查找变量时,看是在哪个模板里(Vue实例的模板) -->
<!-- 可以当做普通的div来看 -->
<cpn v-show="isShow"></cpn>
</div>
<template id="cpn">
<!-- 给div设置 v-show="isShow" ,仍然是显示的,不知道为什么 -->
<div>
<!-- 会使用子组件里的isShow -->
<h2 v-show="isShow">我是子组件</h2>
<p>我是内容</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: 'hello',
isShow: true
},
components: {
cpn: {
template: '#cpn',
data() {
return {
isShow: false
}
}
}
}
})
</script>
官方给出了—条准则∶父组件模板的所有东西都会在父级作虑域内编译;子组件模板的所有东西都会在子级作用域内编译。
3.1.1.2 作用域插槽:准备
- 父组件替换插槽的标签,但是内容由子组件来提供。
- 我们先提一个需求∶
- 子组件中包括一组数据,比如: pLanguages: [‘JavaScript’ , ‘Python’, ‘Swift’ , ‘Go’,‘C++’]
- 需要在多个界面进行展示︰
- 某些界面是以水平方向——展示的,
- 某些界面是以列表形式展示的,
- 某些界面直接展示一个数组
- 内容在子组件,希望父组件告诉我们如何展示,怎么办呢?
- 利用slot作用域插槽就可以了