什么是组件化?
任何一个人处理信息的逻辑能力都是有限的
所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
但是,我们人有一种天生的能力,就是将问题进行拆解。
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
组件化也是类似的思想:
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
但如果,我们讲一个页面拆分成一个个小的功能块
每个功能块完成属于自己这部分独立的功能
那么之后整个页面的管理和维护就变得非常容易了。
我们将一个完整的页面分成很多个组件。
每个组件都用于实现页面的一个功能块。
而每一个组件又可以进行细分。
Vue组件化思想
组件化是Vue.js中的重要思想
它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
任何的应用都会被抽象成一颗组件树。
组件化思想的应用:
有了组件化的思想,我们在之后的开发中就要充分的利用它。
尽可能的将页面拆分成一个个小的、可复用的组件。
这样让我们的代码更加方便组织和管理,并且扩展性也更强。
注册组件的基本步骤
创建组件构造器
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容, 哈哈哈哈</p>
<p>我是内容, 呵呵呵呵</p>
</div>`
})
注册组件
Vue.computend('my-cpn', cpnC)
使用组件。
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<body>
<div class="app">
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
<my-cpn></my-cpn>
</div>
<script src="../Vue基础/js/vue.js"></script>
<script>
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容, 哈哈哈哈</p>
<p>我是内容, 呵呵呵呵</p>
</div>`
})
Vue.computend('my-cpn', cpnC)
const app = new Vue({
el: '.app',
data: {
message: '你好啊'
}
})
</script>
</body>
Vue.extend():
调用Vue.extend()创建的是一个组件构造器。
通常在创建组件构造器时,传入template代表我们自定义组件的模板。
该模板就是在使用到组件的地方,要显示的HTML代码。
Vue.component():
调用Vue.component()是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
所以需要传递两个参数:
1、注册组件的标签名
2、组件构造器
组件必须挂载在某个Vue实例下,否则它不会生效。
下面使用了三次<my-cpn></my-cpn>
而第三次其实并没有生效:
全局组件和局部组件
当我们通过调用Vue.component()
注册组件时,组件的注册是全局的
这意味着该组件可以在任意Vue示例下使用。
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件
<body>
<div id="app">
<cpn></cpn>
<cpn></cpn>
</div>
<div id="app2">
<cpn></cpn>
</div>
<script src="../js/vue.js"></script>
<script>
// 1.创建组件构造器
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈哈啊</p>
</div>
`
})
// 2.注册组件(全局组件, 意味着可以在多个Vue的实例下面使用)
// Vue.component('cpn', cpnC)
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
// cpn使用组件时的标签名
cpn: cpnC
}
})
const app2 = new Vue({
el: '#app2'
})
</script>
</body>
此时app2是无法使用组件的 因为组件在app1中注册 是局部组件
父组件和子组件
组件和组件之间存在层级关系
而其中一种非常重要的关系就是父子组件的关系
<body>
<div id="app">
<cpn2></cpn2>
<!--<cpn1></cpn1>-->
</div>
<script src="../Vue基础/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>
</body>
父子组件错误用法:以子标签的形式在Vue实例中使用
<div id="app">
<cpn2></cpn2>
<cpn1></cpn1> //这是错误的
</div>
因为当子组件注册到父组件的components时,Vue会编译好父组件的模块
该模板的内容已经决定了父组件将要渲染的HTML
(相当于父组件中已经有了子组件中的内容了)
注册组件语法糖
在上面注册组件的方式,可能会有些繁琐。
Vue为了简化这个过程,提供了注册的语法糖。
主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
<body>
<div id="app">
<cpn1></cpn1>
<cpn2></cpn2>
</div>
<script src="../js/vue.js"></script>
<script>
// 全局组件注册的语法糖
Vue.component('cpn1', {
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容, 哈哈哈哈</p>
</div>
`
})
// 2.注册局部组件的语法糖
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
'cpn2': {
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容, 呵呵呵</p>
</div>
`
}
}
})
</script>
</body>
模板的分离写法
刚才,我们通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。
如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。
Vue提供了两种方案来定义HTML模块内容:
使用<script>标签
使用<template>标签
<body>
<div id="app">
<cpn></cpn>
</div>
<!-- 1.script标签, 注意:类型必须是text/x-template-->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈</p>
</div>
</script>
<!--2.template标签-->
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容,呵呵呵</p>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 1.注册一个全局组件
Vue.component('cpn', {
template: '#cpn'
})
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
</body>
组件中的数据存放问题
组件是一个单独功能模块的封装:
这个模块有属于自己的HTML模板,也应该有属性自己的数据data。
组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
测试一下,组件中能不能直接访问Vue实例中的data
我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿。
结论:Vue组件应该有自己保存数据的地方。
组件数据的存放
组件对象也有一个data属性(也可以有methods等属性)
只是这个data属性必须是一个函数
而且这个函数返回一个对象,对象内部保存着数据
data属性为什么是一个函数呢?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象
如果是同一个对象的,组件在多次使用后会相互影响。
<body>
<!--组件实例对象-->
<div id="app">
<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>
// 1.注册组件
const obj = {
counter: 0
}
Vue.component('cpn', {
template: '#cpn',
// data() {
// return {
// counter: 0
// }
// },
data() {
return obj
},
methods: {
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
})
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
<script>
function abc() {
return {
name: 'xiao',
}
}
//
let obj1 = abc()
let obj2 = abc()
let obj3 = abc()
obj1.name = 'kobe'
console.log(obj2); //还是xiao
console.log(obj3); //还是xiao 不会互相影响 因为是函数
const obj = {
name: 'why',
age: 18
}
function abc() {
return obj
}
let obj4 = abc()
let obj5 = abc()
let obj6 = abc()
obj4.name = 'kobe'
console.log(obj5); //kobe
console.log(obj6); //kobe 对象指向的同一地址 一个改变都会改变 因为地址改变
</script>
</body>
父子通信-父传子(props)
子组件是不能引用父组件或者Vue实例的数据的。
在开发中,往往一些数据确实需要从上层传递到下层:
比如在一个页面中,从服务器请求到了很多的数据。
其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
Vue官方提到
通过props向子组件传递数据
通过事件向父组件发送消息
可以直接将Vue实例当做父组件,并且其中包含子组件来简化代码。
真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的。
props基本用法
在组件中,使用选项props来声明需要从父级接收到的数据。
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称。
方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
一个最简单的props传递:
props数据验证
前面,我们的props选项是使用一个数组。
除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。
验证都支持哪些数据类型呢?
String
Number
Boolean
Array
Object
Date
Function
Symbol
当我们有自定义构造函数时,验证也支持自定义的类型
<body>
<div id="app">
<!--<cpn v-bind:cmovies="movies"></cpn>-->
<!--<cpn cmovies="movies" cmessage="message"></cpn>-->
<cpn v-bind:cmessage="message" :cmovies="movies"></cpn>
</div>
<template id="cpn">
<div>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
// 父传子: props
const cpn = {
template: '#cpn',
// props: ['cmovies', 'cmessage'],
props: {
// 1.类型限制
// cmovies: Array,
// cmessage: String,
// 2.提供一些默认值, 以及必传值
cmessage: {
type: String,
default: 'aaaaaaaa',
required: true
},
// 类型是对象或者数组时, 默认值必须是一个函数
cmovies: {
type: Array,
default () {
return []
}
}
},
data() {
return {}
},
methods: {
}
}
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
movies: ['宇宙', '海贼王', '海尔兄弟']
},
components: {
cpn
}
})
</script>
</body>
父传子中的props驼峰标识
如果要使用驼峰标识法,比如cInfo 再绑定父组件数据时要改写成c-info
<body>
<div class="app">
<!-- 注意要使用驼峰写法 这里cInfo 要写成c-info -->
<cpn :c-info="info"></cpn>
</div>
<template id="cpn">
<div>
<!-- 这里的cInfo不用改 -->
<h2>{{cInfo}}</h2>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
props: {
cInfo: {
type: Object,
default () {
return {}
}
}
}
}
const app = new Vue({
el: '.app',
data: {
info: {
name: 'xxx',
age: 18
}
},
components: {
cpn
}
})
</script>
</body>
父子通信-子传父(自定义事件 $emit)
props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
如何处理呢?这个时候,我们需要使用自定义事件来完成。
当子组件需要向父组件传递数据时,就要用到自定义事件了。
我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。
自定义事件的流程
在子组件中,通过$emit()来触发事件。
在父组件中,通过v-on来监听子组件事件。
<body>
<!--父组件模板-->
<div id="app">
<cpn @item-click="cpnClick"></cpn>
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in categories" @click="btnClick(item)">{{item.name}}</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 1.子组件
const cpn = {
template: '#cpn',
// 子组件定义的数据
data() {
return {
categories: [{
id: '1',
name: '热门推荐'
}, {
id: '2',
name: '手机数码'
}, {
id: '3',
name: '家用家电'
}, {
id: '4',
name: '电脑办公'
}, ]
}
},
methods: {
btnClick(item) {
// 发射事件: 自定义事件
this.$emit('item-click', item)
}
}
}
// 2.父组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn
},
methods: {
cpnClick(item) {
console.log('cpnClick', item);
}
}
})
</script>
</body>
组件访问-父访问子-($children $refs)
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
父组件访问子组件:使用$children或$refs
子组件访问父组件:使用$parent
this.$children是一个数组类型,它包含所有子组件对象。
<body>
<div class="app">
<cpn></cpn>
<cpn ref='aaa'></cpn>
<!-- 点击按钮访问子组件的属性 -->
<button @click="btnClick"></button>
</div>
<template id="cpn">
<div>
<h2>我是子组件</h2>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '.app',
data: {
},
methods: {
btnClick() {
for (let i of this.$children) {
console.log(i.name);
i.showMessage();
}
}
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: 'xiaoY'
}
},
methods: {
showMessage() {
console.log('showMessage');
}
}
}
}
})
</script>
</body>
$children
的缺陷:
通过$children
访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
$refs
的使用:
$refs和ref指令通常是一起使用的。
首先,我们通过ref给某一个子组件绑定一个特定的ID。
其次,通过this.$refs.ID就可以访问到该组件了。
<cpn ref='aaa'></cpn>
<button @click="btnClick">通过$refs访问子组件</button>
console.log(this.$refs.aaa);
组件访问-子访问父-($parent $root)
如果我们想在子组件中直接访问父组件,可以通过$parent
如果访问根组件 可以使用$root
<body>
<div class="app">
<cpn></cpn>
</div>
<!-- 子组件 -->
<template id="cpn">
<div>
<h2>我是子组件</h2>
<ccpn></ccpn>
</div>
</template>
<!-- 子子组件 -->
<template id="ccpn">
<div>
<h2>我是子子组件</h2>
<button @click="btnClick"></button>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '.app',
data: {
name: 'xiaoY',
message: 'ni hao',
},
components: {
cpn: {
template: '#cpn',
data() {
return {
name: '我是子组件的name'
}
},
components: {
ccpn: {
template: '#ccpn',
methods: {
btnClick() {
console.log(this.$parent);
console.log(this.$root);
console.log(this.$parent.name);
console.log(this.$root.message);
console.log(this.$root.name);
}
}
}
}
}
},
})
</script>
</body>
注意事项:
尽管在Vue开发中,我们允许通过$parent
来访问父组件,但是在真实开发中尽量不要这样做。
子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。
插槽slot
slot翻译为插槽:
在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
插槽的目的是让我们原来的设备具备更多的扩展性。
比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。
组件的插槽:
组件的插槽也是为了让我们封装的组件更加具有扩展性。
让使用者可以决定组件内部的一些内容到底展示什么。
<!--
1.插槽的基本使用 <slot></slot>
2.插槽的默认值 <slot>button</slot>
3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
-->
<body>
<div class="app">
<cpn><span>ashdgjhdgh</span></cpn>
<cpn><i>djkasajks</i></cpn>
<cpn>
<div>546565</div>
</cpn>
<cpn></cpn>
<cpn>
<p>dasdsa</p>
<div>4545</div>
<i>sd</i>
</cpn>
<cpn></cpn>
</div>
<template id="cpn">
<div>
<h2>我是子组件</h2>
<slot><button></button></slot>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '.app',
data: {
},
components: {
cpn: {
template: '#cpn',
}
}
})
</script>
</body>
具名插槽slot
当子组件的功能复杂时,子组件的插槽可能并非是一个。
比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
这个时候,我们就需要给插槽起一个名字
如何使用具名插槽呢?
非常简单,只要给slot元素一个name属性即可
<slot name='myslot'></slot>
<body>
<div id="app">
<cpn><span slot="center">标题</span><button slot="left">返回</button></cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="center"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template: '#cpn'
}
}
})
</script>
</body>
编译作用域
父组件模板的所有东西都会在父级作用域内编译;
子组件模板的所有东西都会在子级作用域内编译。
<body>
<div id="app">
<cpn v-show="isShow"></cpn>
</div>
<template id="cpn">
<div>
<h2>我是子组件</h2>
<p>我是内容, 哈哈哈</p>
<button v-show="isShow">按钮</button>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
isShow: true
},
components: {
cpn: {
template: '#cpn',
data() {
return {
isShow: false
}
}
},
}
})
</script>
</body>
在使用<cpn v-show="isShow"></cpn>
的时候,整个组件的使用过程是相当于在父组件中出现的。
那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。
上图代码中子组件的 isShow为false 所以子组件中的button按钮就不会显示
作用域插槽
作用域插槽是slot一个比较难理解的点,而且官方文档说的又有点不清晰。
一句话对其做一个总结:父组件替换插槽的标签,但是内容由子组件来提供。
示例
子组件中包括一组数据,比如:
pLanguages: ['JavaScript', 'Python', 'Swift', 'Go', 'C++']
需要在多个界面进行展示:
某些界面是以水平方向一一展示的,
某些界面是以列表形式展示的,
某些界面直接展示一个数组
内容在子组件,希望父组件告诉我们如何展示,怎么办呢?
利用slot作用域插槽就可以了
<body>
<div id="app">
<cpn></cpn>
<cpn>
<!--目的是获取子组件中的pLanguages-->
<template slot-scope="slotProps">
<span v-for="item in slotProps.data"> {{item}} - </span>
<!-- <span>{{slotProps.data.join(' - ')}}</span> -->
</template>
</cpn>
<cpn>
<!--目的是获取子组件中的pLanguages-->
<template slot-scope="slotProps">
<!--<span v-for="item in slotProps.data">{{item}} * </span>-->
<span>{{slotProps.data.join(' * ')}}</span>
</template>
</cpn>
<!--<cpn></cpn>-->
</div>
<template id="cpn">
<div>
<slot :data = "pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
<script src="../Vue基础/js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
template: '#cpn',
data() {
return {
pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
}
}
}
}
})
</script>
</body>
父组件使用我们的子组件时,从子组件中拿到数据:
我们通过获取到slotProps属性
在通过slotProps.data就可以获取到刚才我们传入的data了
<slot :data = "pLanguages">
<template slot-scope="slotProps">
<span v-for="item in slotProps.data"> {{item}} - </span>
</template>