文章目录
一、组件化概述
对组件化的简单理解
组件化开发就是对一个页面功能进行细分的意思,每个细分出来的部分就叫组件,组件可以是一个普通的 DOM
元素 ( 比如就是一个单独的输入框 ),也可以是一系列的 DOM
元素 ( 比如一个完整的菜单栏的实现 ),细分的粒度是没有具体定义的,但我认为一个有意义的前端组件至少要包含 HTML
、CSS
、Javascript
三部分
组件化的优势
下图,以京东 <商品列表页> 为例, 我们设计时可以将其拆分为头部组件、侧边栏组件、底部组件、展示组件四部分,甚至对每个组件内部仍可以继续细分,这么做的好处是显而易见的:
1) 可复用易组合,组件间可以随意组合,形成一个新的页面
2) 高内聚易维护,拆分合理的组件,在其修改时,只需要考虑组件内的代码就可以
组件化与模块化的不同
组件化更多的是以 UI
界面为维度,针对页面的某一部分进行完整的封装,像一个菜单栏的完整实现或一个轮播图的完整实现,而模块化是以代码功能为维度,针对同类功能的 Javascript
进行封装,像发送网络请求相关的 Javascript
可以封装为一个模块,登录相关的 Javascript
也可以封装成一个模块
二、用 Vue 实现组件化
在 Vue 中使用组件化开发,离不开最基础的三个步骤,
1)声明组件
2)注册组件到目标作用域
3)作用域内使用组件
下面我们按照这三个步骤一起学习
1. 组件的声明
Vue 中想声明一个组件,我们要使用Vue.extend
函数,将定义它的源码简化后如下:
Vue.extend = function (extendOptions) {
return function VueComponent() {
this._init(extendOptions)
}
}
可以看到会返回一个 VueComponent
对象,在对象内会调用 this._init(选项)
,this._init(选项)
这个函数在new Vue(options)
时也曾被执行,其作用是匹配 Vue 中预定义的 options
属性,既然此处也使用了该函数,则代表 extendOptions
与 options
应该具有相同结构,事实也是如此,唯一不同的是 data
属性在 extendOptions
中要定义成 函数,而不是对象
options
也好,extendOptions
也罢,我要介绍一下它的另一个预定义属性 template
, 该属性称为模板属性,其接收一个字符串,内容可以是 ID ( 涉及到组件模板抽离,后面再提 ),也可以是 html 代码 ( 实现组件的代码 ),Vue.extend(extendOptions)
中使用该属性,会返回一个组件对象 VueComponent
,组件的模板内容为 template
的值
组件声明的基本结构
组件中的 html 模板,一定要有根元素,一般习惯在最外层套一个 <div>
元素
<script>
const vueComponent = Vue.extend({
template: '<div>' +
'<div>{{message}}</div>' +
'</div>',
data: function () {
return {
message: '这是组件的消息'
}
}
})
</script>
2. 不同作用域下注册组件
组件声明后,可以在不同的作用域内注册,现在分别学习其各种注册方式,至于使用方式都一样,就是在 Vue 对象的挂载范围内写上 <组件名></组件名>
即可,注意:自定义的组件名尽量不要出现驼峰命名
1) 全局注册
使用 Vue.component('自定义组件名', VueComponent对象)
注册后,就可以在任意 Vue 对象的挂载范围内使用组件
效果:
代码:
<body>
<!-- 第一个 Vue 对象的挂载范围 -->
<div id="first">
第一个 Vue 对象:
<my-component></my-component>
</div> <br/><br/>
<!-- 第二个 Vue 对象的挂载范围 -->
<div id="second">
第二个 Vue 对象:
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 组件声明
const vueComponent = Vue.extend({
template: '<div>' +
'<div>{{message}}</div>' +
'</div>',
data: function () {
return {
message: '这是组件的消息'
}
}
})
// 组件全局注册
Vue.component('my-component', vueComponent)
// 第一个 Vue 对象
const first = new Vue({
el: '#first'
})
// 第二个 Vue 对象
const second = new Vue({
el: '#second'
})
</script>
</body>
2) 局部注册
使用 Vue 构造参数 options
的可选属性 components
,就可以在 Vue 对象中实现组件的局部注册,注册后,只有该 Vue 对象的挂载范围内可以使用组件
new Vue({
el: '#vm',
components: {
自定义组件名1 : 组件对象1,
自定义组件名2 : 组件对象2,
}
})
效果:
代码:
<body>
<!-- 第一个 Vue 对象的挂载范围 -->
<div id="first">
第一个 Vue 对象:
<my-component></my-component>
</div> <br /><br />
<!-- 第二个 Vue 对象的挂载范围 -->
<div id="second">
第二个 Vue 对象:
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 组件声明
const vueComponent = Vue.extend({
template: '<div>' +
'<div>{{message}}</div>' +
'</div>',
data: function () {
return {
message: '这是组件的消息'
}
}
})
// 第一个 Vue 对象
const first = new Vue({
el: '#first'
})
// 第二个 Vue 对象
const second = new Vue({
el: '#second',
components: {
'my-component': vueComponent
}
})
</script>
</body>
3) 父子注册
虽然 options
与 extendOptions
为同一结构,但是为了区分,我们先来统一下叫法,说 options
时,就代表 Vue 对象的构造参数,说 extendOptions
就代表 Vue.extend
的参数
我们在 options
中通过可选属性 components
来实现局部注册,现在可以在 extendOptions
中通过 components
来实现父子组件的注册,其实在 options
中通过 components
注册的组件也可以视为父子注册,因为 Vue 对象本身也可以被看成一个组件,通常做为根组件使用
写在 components
中的组件,称为子组件, 定义后,就可以在父组件内使用
效果:
代码:
<body>
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 子组件声明
const son = Vue.extend({
template: '<div>' +
'<div>{{message}}</div>' +
'</div>',
data: function () {
return {
message: '这是子组件的内容'
}
}
})
// 父组件声明
const father = Vue.extend({
template: '<div>' +
'<div>{{message}}</div>' +
'<son></son>' +
'</div>',
data: function () {
return {
message: '这是父组件的内容'
}
},
// 父子组件注册
components: {
son: son
}
})
// 局部注册
new Vue({
el: "#vm",
components: {
father: father
}
})
</script>
</body>
3. 使用语法糖简化组件的声明与注册
上面展示的是最完整的组件使用方式,其实我们在使用时,还可以通过语法糖省略组件声明部分
组件作用域 | 合并前 | 合并后 |
---|---|---|
全局注册 | Vue.component(‘自定义组件名’, 组件对象) | Vue.component(‘自定义组件名’, extendOptions) |
局部注册 | options.compontents.自定义组件名 : 组件对象 | options.compontents.自定义组件名 : extendOptions |
父子注册 | extendOptions.compontents.自定义组件名 : 组件对象 | extendOptions.compontents.自定义组件名 : extendOptions |
全局注册对比
<body>
<div id="vm">
<my-component></my-component>
<simple></simple>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 完整写法
const myComponent = Vue.extend({
template: '<div>组件全局注册 - 完整写法</div>'
})
Vue.component('my-component', myComponent);
// 语法糖写法
Vue.component('simple', {
template: '<div>组件全局注册 - 语法糖写法</div>'
})
new Vue({
el: "#vm"
})
</script>
</body>
局部注册对比
<body>
<div id="vm">
<my-component></my-component>
</div>
<div id="simpleVm">
<simple></simple>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 完整写法
const myComponent = Vue.extend({
template: '<div>组件局部注册 - 完整写法</div>'
})
new Vue({
el: "#vm",
components: {
'my-component': myComponent
}
})
// 语法糖写法
new Vue({
el: "#simpleVm",
components: {
'simple': {
template: '<div>组件局部注册 - 语法糖写法</div>'
}
}
})
</script>
</body>
父子注册对比
<div id="vm">
<father></father>
</div>
<div id="simpleVm">
<simple-father></simple-father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 完整写法
const son = Vue.extend({
template: '<div>组件父子注册 - 完整写法 - 子组件内容</div>'
})
const father = Vue.extend({
template: '<div><son-component></son-component>组件父子注册 - 完整写法 - 父组件内容</div>',
components: {
'son-component': son
}
})
new Vue({
el: "#vm",
components: {
'father': father
}
})
// 语法糖写法
new Vue({
el: "#simpleVm",
components: {
'simple-father': {
template: '<div><simple-son></simple-son>组件父子注册 - 语法糖写法 - 父组件内容</div>',
components: {
'simple-son': {
template: '<div>组件父子注册 - 语法糖写法 - 子组件内容</div>'
}
}
}
}
})
</script>
</body>
三、组件模板抽离
我们在设置 template
属性时,都是直接把模板内容写在字符串内,这样做的缺点就是,字符串内编写时 IDE 不会做语法提示、而且也不能使用 Emmet 语法
或 快捷键,还有就是将 html 的布局代码写在 javascript 中,分离性不是很好,而且当模板内容很复杂时,阅读性也不好
前面提到过,template
属性除了可以赋值布局用的字符串,还可以使用 模板 ID
,现在就要用 模板 ID
的这种方式来实现模板抽离,其步骤为定义模板和通过 模板 ID
引用,现在学习两种常见的方式
1) script 定义模板,自定义 ID,并指定 type 为 text/x-template
<body>
<div id="vm">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 定义模板 -->
<script id="myTemplate" type="text/x-template">
<div>
我是定义的模板
</div>
</script>
<script>
new Vue({
el: "#vm",
components: {
'my-component': {
template: '#myTemplate'
}
}
})
</script>
</body>
2) Vue 的 template 标签定义模板,只需要指定自定义 ID
<body>
<div id="vm">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 定义模板 -->
<template id="myTemplate">
<div>
我是定义的模板
</div>
</template>
<script>
new Vue({
el: "#vm",
components: {
'my-component': {
template: '#myTemplate'
}
}
})
</script>
</body>
四、父子组件间通信
不管是 options
,还是 extendOptions
,它们定义的可选项都有自己的作用域,都只能在自己的组件内访问,当组件关系为父子时,有时需要互相访问,那么则需要一些特殊的方式来达到目的
1. 父子传值
父子组件间最常见的访问目的,无非就是传值了,子组件的内容依赖父组件的数据,或者父组件的内容依赖子组件操作后产生的数据,这都是典型的传值问题,现在分别对这两种情况进行学习
父子传值方式概览图:
1)父传子
实现父传子,主要分两步, 首先在声明子组件时,在 extendOptions
中添加可选属性 props
,然后父组件中使用子组件时,通过 ( v-bind:传值变量 = 欲传值 ) 的方式向子组件传值,注意:如果传值变量是用 -
分割的形式,那么 v-bind 使用时要写成驼峰,如子组件:props:['my-message']
,父组件:v-bind:myMessage
① props 的值可以是数组
只需要把子组件中要用到的传值变量,罗列在数组中就可以,注意,每个传值变量要用引号包裹
示例代码:
<body>
<!-- 根组件 -->
<div id="vm">
<my-component></my-component>
</div>
<!-- 父组件模板 -->
<template id="father">
<div>
<son v-bind:message="'这是父组件传过来的消息'"></son>
<son v-bind:message="send"></son>
</div>
</template>
<!-- 子组件模板 -->
<template id="son">
<div>
{{message}}
</div>
</template>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 子组件
const son = {
template: '#son',
'props': ['message']
}
// 父组件
const father = {
template: '#father',
data: function () {
return {
send: '这也是父组件传过来的消息'
}
},
components: {
'son': son
}
}
// 根组件
new Vue({
el: "#vm",
components: {
'my-component': father
}
})
</script>
</body>
② props 的值可以是对象
这种使用方式,可以对传值变量进行更好的控制,比如值的类型、是否必传、默认值、校验等,具体使用方法,通过示例代码应该琢磨一下就能懂,不再详细解释
示例代码:
<body>
<!-- 根组件 -->
<div id="vm">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 父组件模板 -->
<template id="father">
<div>
<son v-bind:message="'这是父组件传过来的消息'" v-bind:price="101"></son>
</div>
</template>
<!-- 子组件模板 -->
<template id="son">
<div>
{{message}}
{{price}}
</div>
</template>
<script>
// 子组件
const son = {
template: '#son',
'props': {
message: {
type: String,
default: '父组件未传值,这是默认值',
},
price: {
type: [Number, String],
required: true,
validator: function (value) {
return value > 100
}
}
}
}
// 父组件
const father = {
template: '#father',
components: {
'son': son
}
}
// 根组件
new Vue({
el: "#vm",
components: {
'my-component': father
}
})
</script>
</body>
2)子传父
实现子传父,也分两步,首先在子组件中使用 this.$emit(自定义事件名,...传给父组件的参数列表)
给父组件发送一个自定义事件,然后父组件中使用子组件时,通过 v-on:自定义事件名
的方式,监听子组件发送过来的自定义事件,最后做出相应事件处理,注意:自定义组件名不要使用驼峰的方式
示例代码
<!-- 根组件 -->
<body>
<div id="vm">
<my-component></my-component>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 父组件模板 -->
<template id="father">
<div>
<!-- 此处方法的默认参数,不再是浏览器事件对象,而是子组件传过来的参数列表 -->
<son @sub-click="pass"></son>
</div>
</template>
<!-- 子组件模板 -->
<template id="son">
<div>
<button v-on:click="btnClick">点击按钮给父组件传值</button>
</div>
</template>
<script>
// 子组件
const son = {
template: '#son',
methods: {
btnClick: function () {
this.$emit('sub-click', '这是子组件传给父组件的内容')
}
}
}
// 父组件
const father = {
methods: {
// 此处方法的默认参数,不再是浏览器事件对象,而是子组件传过来的参数列表
pass: function (value) {
alert(value)
}
},
template: '#father',
components: {
'son': son
}
}
// 根组件
new Vue({
el: "#vm",
components: {
'my-component': father
}
})
</script>
</body>
2. 访问子组件/父组件对象
有时父子通信并不仅仅为了传值,比如想调用子组件的方法,或者调用父组件的方法,这种时候我们就需要在当前组件下,拿到另一组件的组件对象,既 VueComponent
、甚至是根组件对象 Vue
子组件访问父组件对象
在子组件中可以使用 this.$parent
拿到父组件,或使用 this.$root
拿到根组件,虽然子组件中可以拿到父组件对象,但是强烈不建议这么用,一旦子组件依赖父组件后,就会变成强耦合,不利于子组件的复用
示例代码
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son">
<button @click="get">点击获取父组件和根组件对象</button>
</template>
<!-- 父组件 -->
<template id="father">
<son></son>
</template>
<script>
// 子组件
let son = {
template: '#son',
methods: {
get: function () {
console.log(this.$parent)
console.log(this.$root)
}
}
}
// 父组件
let father = {
template: '#father',
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>
父组件访问子组件对象
父组件访问子组件也有两种常见的方式,this.$children
和 this.$refs
,this.$children
会取出所有的子组件,并以数组形式返回,所以想使用某一个组件时,要通过下标访问,有点不利于扩展,this.$refs
需要在使用子组件时指定一个 ref
属性,然后通过 this.$refs.ref属性
的方式访问
示例代码
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son"></template>
<!-- 父组件 -->
<template id="father">
<div>
<button @click="get">点击获取子组件对象</button>
<son ref="sonComponent"></son>
</div>
</template>
<script>
// 子组件
let son = {
template: '#son'
}
// 父组件
let father = {
template: '#father',
methods: {
get: function () {
console.log(this.$children[0])
console.log(this.$refs.sonComponent)
}
},
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>
五、插槽
有时为了让子组件灵活性更高,子组件内部只会定义一个大框,某些具体内容让父组件自行填充,如下图:
上图中子组件已将大框布局定义好,只有中间部分空出来,让父组件任意填充,这么做的好处就是,该子组件更灵活,可复用的场景会更多
Vue 中想实现这种插拔的效果,就需要学习插槽的用法, Vue 中提供了 普通插槽、默认值插槽、具名插槽、作用域插槽 4 种用法来应对不同场景
1. 普通插槽
比较适合子组件中预留一个空间,父组件使用时必须填充的场景,比如上图中的场景就可以用普通插槽来应对,现分两步,先在子组件的预留空间处写上 <slot></slot>
,然后父组件使用子组件时,直接填充就可以, 按如下方式填充<子组件>填充内容</子组件>
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son">
<div>
<p>元素1</p>
<slot></slot>
<p>元素3</p>
</div>
</template>
<!-- 父组件 -->
<template id="father">
<div>
<!-- 插槽填充 -->
<son>
<p>父组件填充的元素</p>
</son>
</div>
</template>
<script>
// 子组件
let son = {
template: '#son'
}
// 父组件
let father = {
template: '#father',
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>
2. 默认值插槽
见名知意,子组件中仍会预留一个空间,但是如果父组件不主动填充时,会有一个默认填充,实现分两步,先在子组件的预留空间处写上 <slot>默认填充内容</slot>
,然后父组件使用子组件时,按实际需求决定是否主动填充
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son">
<div>
<p>元素1</p>
<slot>这是默认值填充</slot>
<p>元素3</p>
</div>
</template>
<!-- 父组件 -->
<template id="father">
<div>
<!-- 主动填充 -->
<son>
<p>父组件填充的元素</p>
</son>
<!-- 默认值填充 -->
<son> </son>
</div>
</template>
<script>
// 子组件
let son = {
template: '#son'
}
// 父组件
let father = {
template: '#father',
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>
3. 具名插槽
我们之前都是在子元素中定义一个插槽,当子元素中定义有多个插槽时,就可以使用具名插槽,其能指定具体填充哪一个预留空间,实现分两步,先在子组件的预留空间处写上 <slot name="自定义插槽名"></slot>
,然后父组件使用子组件时,指名填充就可以,填充方式: <子组件><div slot="自定义插槽名">填充内容</div></子组件>
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son">
<div>
<slot name="top">默认填充-元素1</slot>
<slot name="middle">默认填充-元素2</slot>
<slot name="bottom">默认填充-元素3</slot>
</div>
</template>
<!-- 父组件 -->
<template id="father">
<div>
<!-- 具名填充 -->
<son>
<p slot="top">父组件填充的元素1</p>
<p slot="bottom">父组件填充的元素3</p>
</son>
</div>
</template>
<script>
// 子组件
let son = {
template: '#son'
}
// 父组件
let father = {
template: '#father',
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>
4. 作用域插槽
当父组件进行插槽填充布局时,如果需要使用子组件中定义的数据,就可以使用作用域插槽
至于适用场景,举个例子吧,子组件插槽中,默认用 <p>
对数据进行展示,现在父组件想替换成 <div>
展示数据,我们用前面学的几种插槽方式确实可以将 <div>
填充进去,但是 <div>
中展示的数据是子组件中定义的,我们在父组件中怎么拿到子组件的数据呢?因为子组件还没渲染完成,所以this.$children
也无法获取,那么这种场景只能使用作用域插槽
语法:
相比其他插槽的使用方式会更麻烦一些,先说子组件,在定义插槽时使用该语法 <slot :导出给父组件使用的自定义变量名="导出的数据"></slot>
,再说父组件,在使用子组件时要使用如下语法:
<子组件>
<template slot-scope="自定义接收变量名-用来接收子元素数据对象">
{{父组件中自定义的接收变量名.子组件中自定义的导出变量名}}
</template>
</子组件>
确实有点绕,现在把刚才说的 div 替换 p 的场景写一个示例代码,耐心研究研究,再动手试一试就懂了
<body>
<!-- 根组件 -->
<div id="vm">
<father></father>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<!-- 子组件 -->
<template id="son">
<div>
<!-- :自定义导出变量名 = 导出变量 -->
<slot :data="message">
<p>{{message}}</p>
</slot>
</div>
</template>
<!-- 父组件 -->
<template id="father">
<son>
<!-- slot-scope="自定义接收变量名-用来接收子元素数据对象" -->
<template slot-scope="childrenData">
<div style="border:1px solid black; width: 200px; height: 200px;">
<!-- 父组件中自定义的接收变量名.子组件中自定义的导出变量名 -->
{{childrenData.data}}
</div>
</template>
</son>
</template>
<script>
// 子组件
let son = {
template: '#son',
data: function () {
return {
message: '这是子组件的数据'
}
}
}
// 父组件
let father = {
template: '#father',
components: {
son
}
}
//根组件
new Vue({
el: "#vm",
components: {
father
}
})
</script>
</body>