三. 组件化开发
三. 组件化开发
1. 认识组件化
2. 组件化基础
1> 注册组件
注册的基本步骤
在ES6中``这个可以引用字符串,类似于单引号的作用,但是同时这个是可以换行的,之前单引号的换行,必须要加上一个加号
const str = '' +
''
这个可以不用加加号
const str2 = `
`
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<!-- 3.使用组件-->
<my-cpn></my-cpn>
</div>
<script src = "../js/vue.js">
</script>
<script>
// const str = '' +
// ''
// const str2 = `
//
// `
//1.创建组件构造器对象
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容1</p>
<p>我是内容2</p>
</div>`
})
//2.注册组件
Vue.component('my-cpn',cpnC)
//3.
const app = new Vue({
el:'#app',
data:{
message :'你好啊'
}
})
</script>
</body>
</html>
全局和局部组件
当我们通过调用Vue.component()注册组件时,组件的注册是全局的
这意味着该组件可以在任意Vue示例下使用。
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
</div>
<div id="app2">
<cpn></cpn>
</div>
<script src="../js/vue.js">
</script>
<script>
//创建组件构造器
const cpnC = Vue.extend({
template: `
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
`
})
//注册组件(全局组件,以为着可以在多个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>
</html>
-
当全局注册的时候,两个不同的Vue实例都可以使用这个模板
//注册组件(全局组件,以为着可以在多个vue实例下面使用) Vue.component('cpn',cpnC)
-
当局部注册的时候
components:{ // cpn是使用组件时的标签名 cpn:cpnC }
父组件和子组件
组件构造器1,放在组件构造器2里面注册,这样可以直接在组件2中使用组件1
//2.创建第二个组件(父组件)
const cpnC2= Vue.extend({
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容</p>
<cpn1></cpn1>
</div>
`,
components:{
cpn1:cpnC1
}
})
//1.创建第一个组件(子组件)
const cpnC1 = Vue.extend({
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容</p>
</div>
`
})
然后把组件构造器2,放到vue的components里面进行注册
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
},
components:{
// cpn1:cpnC1,
cpn2:cpnC2
}
})
root组件:
const app = new Vue({ el: '#app', data: { message: '你好啊', }, components:{ // cpn1:cpnC1, cpn2:cpnC2 } })
这个也可以看成是一个组件,这里面可以写template会替代el,这是最顶层的组件root
在哪里注册,就在哪里使用。
当子组件注册到父组件的components时,Vue会编译好父组件的模块,父组件调用子组件的时候,会现在自己的作用域里面找component,找到的话就会把这些代码替换过去,如果找不到的话,就会到全局的components里面去找。
该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)
注册组件语法糖
Vue为了简化这个过程,提供了注册的语法糖。
主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。
注册全局组件:
//2.注册组件
Vue.component('cpn1', {
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容</p>
</div>
});
注册局部组件:
//注册局部组件的语法糖
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
},
components: {
'cpn2': {
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容</p>
</div>
`
}
}
})
模板的分离写法
我们通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。
如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。
Vue提供了两种方案来定义HTML模块内容:
<script>
//1.注册一个全局组件
Vue.component('cpn',{
template:'#cpn'
})
const app = new Vue({
el:'#app',
data:{
message :'你好啊'
}
})
</script>
使用<script>标签
<!--1.通过script标签,类型一定是text/x-template-->
<script type="text/x-template" id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容</p>
</div>
</script>
使用<template>标签
<!--2.template标签-->
<template id="cpn">
<div>
<h2>我是标题</h2>
<p>我是内容template</p>
</div>
</template>
组件可以访问Vue实例数据吗?
我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例中,Vue实例就会变的非常臃肿。
结论:Vue组件应该有自己保存数据的地方。
组件自己的数据存放在哪里呢?
组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
只是这个data属性必须是一个函数
而且这个函数返回一个对象,对象内部保存着数据
Vue.component('cpn', {
template: '#cpn',
data() {
return {
title: 'abc'
}
}
})
函数在每次调用的时候都会在自己的栈空间里面创建自己的一些变量
<script>
function abc() {
return{
name:'why',
age:18
}
}
let obj1 = abc();
let obj2 = abc();
let obj3 = abc();
</script>
obj1、obj2、obj3的内存地址肯定不同,这里我们没法获取他们的内存地址,但是我们可以
obj1.name = 'kobe'
console.log(obj2)
console.log(obj3)
这里如果我们定义一个const
const obj = {
name: 'why',
age: 18
}
我们可以理解我们之前为什么data在组件中必须是一个函数呢?
首先,如果不是一个函数,Vue直接就会报错。
其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
我们如果想要变量同时变化可以定义一个const在注册组件前面
在data里面return同一个对象就可以了。
这样可以实现同时的加减
组件一般不是要同时进行操作的,应该是在每个地方有我们保存自己对象的一个实例,千万不要让他们相互影响,如果data不是函数的话,就会出现连锁反应。
父子组件的通信
不是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
不可能我一个小组件就发送一个请求,因为我们小组件的请求是统一的,需要其他的部分把数据传递给小组件们,小组件再去做一个展示。
如何进行父子组件间的通信呢?Vue官方提到
父 --》 子 prop
子 --》父 emit
<div id="app">
<cpn v-bind:cmovies="movies" :cmessage = "message"></cpn>
<!--<cpn cmovies="movies" cmessage = "message"></cpn> 这里如果不用v-bind,会直接把这个数据赋值,并不会去找这两个变量所对应的在值-->
</div>
<template id="cpn">
<div>
<ul>
<li v-for="item in cmovies" >{{item}}</li>
</ul>
{{cmovies}}
<h2>{{cmessage}}</h2>
</div>
</template>
<script src = "../js/vue.js">
</script>
<script>
// 父传子props
const cpn ={
template:'#cpn',
props:[
'cmovies','cmessage'
]
}
const app = new Vue({
el:'#app',
data:{
message :'你好啊',
movies:['海王','海贼王','海尔兄弟']
},components:{
cpn
}
})
/*
字面量的增强写法:
const name = 'why'
const obj = {
// name:name
name
}
*/
</script>
// 父传子props
const cpn ={
template:'#cpn',
// 1.数组写法
// props:[
// 'cmovies','cmessage'
// ]
// 2.对象写法
props:{
//1) 这种写法还可以对数据类型做一个限制
// cmovies:Array,
// cmessage:String,
//2)可以提供默认值,以及必传值
cmessage: {
type:String,
default:'aaaa',
//用组件的时候必定要传入这个属性
required:true
},
//类型是对象或者数组的时候,这里一定要是一个函数
cmovies:{
type:Array,
// default: []//vue2.5...以下这样的写法都是正确的
default(){
return []
}
}
}
}
当我们有自定义构造函数时,验证也支持自定义的类型
v-bind不支持驼峰语法
这里引用的时候需要
<div id="app">
<cpn :c-info="info"></cpn>
</div>
<div id="app">
<cpn :c-info="info" :child-my-message ="message"></cpn>
</div>
<template id="cpn">
<div>
<h2>{{cInfo}}</h2>
<h2>{{childMyMessage}}</h2>
</div>
</template>
<script src="../js/vue.js">
</script>
<script>
const cpn = {
template: '#cpn',
props: {
cInfo: {
type: Object,
default() {
return {};
}
},
childMyMessage:{
type:String,
default:''
},
}
}
const app = new Vue({
el: '#app',
data: {
info: {
name: 'why',
age: 18,
height: 1.88
},
message:'aaaaaa'
},components:{
cpn
}
})
</script>
在以前的时候我们省略传入的参数,会把event事件传进去,但是这里会把我们子组件里面emit的第二个参数传入
<div id="app">
<!-- 监听子组件发射出去的事件,在下面处理-->
<cpn @itemclick="cpnClick"></cpn>
</div>
<button v-for="item in categories" @click="btnClick(item)">
{{item.name}}
</button>
props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。
自定义事件的流程:
- 在子组件中,通过$emit()来触发事件。
- 在父组件中,通过v-on来监听子组件事件。
<script>
//
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{id: 'aaa', name: '热门推荐'},
{id: 'bbb', name: '手机数码'},
{id: 'ccc', name: '家用电器'},
{id: 'ddd', name: '电脑办公'},
]
}
},methods:{
btnClick(item){
// console.log(item)
//发射事件:自定义事件
this.$emit('itemclick',item)
}
}
}
//父组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}, components: {
cpn
},methods: {
cpnClick(item){
console.log("cpnClick",item)
}
}
})
</script>
body>
<!--父组件模板-->
<div id="app">
<!-- 监听子组件发射出去的事件,在下面处理-->
<cpn @itemclick="cpnClick"></cpn>
</div>
<!--子组件模板-->
<template id="cpn">
<div>
<button v-for="item in categories" @click="btnClick(item)">
{{item.name}}
</button>
</div>
</template>
组件会渲染,但是最终渲染的代码中没有templete,只有一个render函数。
render函数里面可以写@itemClick。
但是在父子组件里面只能写@item-click,这里不可以用驼峰
2>数据传递
父子组件的访问
案例:
这里通过子组件里面的值
<div id="app">
<cpn :number1="num1" :number2="num2"/>
</div>
<script src = "../js/vue.js">
</script>
<template id = "cpn">
<div>
<input type="text" v-model="number1">
{{number1}}
<br>
<input type="text" v-model="number2">
{{number2}}
</div>
</template>
<script>
const app = new Vue({
el:'#app',
data:{
num1:1,
num2:0
},
components: {
cpn:{
template: '#cpn',
props:{
number1:Number,
number2:Number
}
}
}
})
</script>
这里会报错:
一般我们都是想让父组件里面传入的 值修改我们子组件里面传入的值,但是我们这里是通过子组件传入的值。
在子组件里面双向数据绑定, 这里需要在子组件的data里面进行书写,让data里面的属性再出一个属性,用这个属性来绑定设个
return {
dnumber1: this.number1,
dnumber2: this.number2
}
这样修改之后的代码变为
<div id="app">
<cpn :number1="num1" :number2="num2"></cpn>
</div>
<script src="../js/vue.js">
</script>
<template id="cpn">
<div>
<input type="text" v-model="dnumber1">
props:{{ number1 }}
data: {{ dnumber1 }}
<br>
<input type="text" v-model="dnumber2">
props:{{ number2 }}
data: {{ dnumber2 }}
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
num1: 0,
num2: 1
},
components: {
cpn: {
template: '#cpn',
props: {
number1: Number,
number2: Number
},
data() {
return {
dnumber1: this.number1,
dnumber2: this.number2
}
}
}
}
})
</script>
这样修改之后这个界面变为:
但是我们这里如果还想把子组件里面双向绑定的值再绑定传回父组件,这里就需要把这个值
这里报错是因为这个传入参数的类型默认是String类型, 我们这里需要传入的是number类型,我们可以将其×1,也可以转成number类型,或者用parseInt方法来进行类型的转换。
num1change(value){
this.num1 = parseInt(value);
},
num2change(value){
this.num2 = value*1;
}
在修改number1之后同时改变number2的值,这里可以这样写:
num1Input(event){
//1.将input中的value赋值到dnumber中
this.dnumber1 = event.target.value;
//2.为了让父组件可以修改值,发出一个事件
this.$emit('num1change',this.dnumber1)
//3.同时修饰dnumber2的值
this.dnumber2 = this.dnumber1*20;
this.$emit('num2change',this.dnumber2)
},
//同时修改number2的值
num2Input(event){
this.dnumber2 = event.target.value;
this.$emit('num1change',this.dnumber2)
//同时修改dnumber1的值
this.dnumber1 = this.dnumber2*20;
this.$emit('num2change',this.dnumber1)
}
有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者是子组件访问跟组件。
父组件访问子组件:使用$children
或$refs reference(引用)
子组件访问父组件:使用$parent
我们先来看下$children
的访问this.$children
是一个数组类型,它包含所有子组件对象。
我们这里通过一个遍历,取出所有子组件的message状态。
在这里会打印出一个vue实例,这是因为父组件就是一个Vue实例,之前在子访问父组件的时候会打印VueComponent。
但是如果在components里面在嵌套一个component的话,这个时候的父组件就是component了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn></cpn>
</div>
<script src="../js/vue.js">
</script>
<template id="cpn">
<div>
<ccpn></ccpn>
</div>
</template>
<template id="ccpn">
<div>
<h2>我是ccpn组件</h2>
<button @click="btnClick">按钮</button>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn: {
data(){
return {
name:'我是cpn组件的name'
}
},
template: '#cpn',
components: {
ccpn:{
template:'#ccpn',
methods:{
btnClick(){
//1.访问父组件$parent
console.log(this.$parent)
console.log(this.$parent.name);
//2.访问根组件 $root
console.log(this.$root.message)
}
}
},
}
},
}
})
</script>
</body>
</html>
$children
的缺陷:
通过$children
访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
$refs
的使用:
$refs
和ref指令通常是一起使用的。
首先,我们通过ref给某一个子组件绑定一个特定的ID。
其次,通过this.$refs.ID就可以访问到该组件了。
父子组件的访问方式: $parent
如果我们想在子组件中直接访问父组件,可以通过$parent
注意事项:
- 尽管在Vue开发中,我们允许通过
$parent
来访问父组件,但是在真实开发中尽量不要这样做。 - 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
- 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
- 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。
3. 组件化高级
1.插槽slot
slot的基本使用
插槽的目的是让我们原来的设备具备更多的扩展性。
基本使用案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--1.插槽的基本使用<slot></slot>
2.插槽的默认值 <slot></slot>
3.如果有多个值,同时放入到组件中进行替换时,会一起作为替换元素把button给替换掉
-->
<div id="app">
<cpn>
<button>案例1</button>
<div>我是div元素</div>
<p>我是p元素</p>
</cpn>
<cpn>
<p>我是p元素</p>
</cpn>
<cpn>
</cpn>
<cpn>
</cpn>
</div>
<script src="../js/vue.js">
</script>
<template id="cpn">
<div>
<h2>我是组件</h2>
<p>我是组件,哈哈哈哈哈</p>
<slot><button>按钮</button></slot>
</div>
</template>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}, components: {
cpn: {
template: '#cpn'
}
}
})
</script>
</body>
</html>
slot的具名插槽
当子组件的功能复杂时,子组件的插槽可能并非是一个。
比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?
这个时候,我们就需要给插槽起一个名字
非常简单,只要给slot元素一个name属性即可
<slot name='myslot'></slot>
我们来给出一个案例:
解决问题:Github - Stack OverFlow
slot作用域插槽
我们来考虑下面的代码是否最终是可以渲染出来的:
<my-cpn v-show="isShow"></my-cpn>
isShow属性包含在组件中,也包含在Vue实例中。
答案:最终可以渲染出来,也就是使用的是Vue实例的属性。
官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
而我们在使用
<my-cpn v-show="isShow"></my-cpn>
整个组件的使用过程是相当于在父组件template中出现的。
那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。
在template里面无论你使用的是v-show和v-for,在查找变量的时候会先查找这个Vue实例的作用域
父组件替换插槽的标签,但是内容由子组件来提供
在父组件使用我们的子组件时,从子组件中拿到数据:
<template slot-scope="slotProps">
获取到slotProps属性
在通过slotProps.data就可以获取到刚才我们传入的data了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div id="app">
<cpn>
<!-- 目的是获取子组件里面的pLanguage
2.5.x以下必须要有template模板-->
<template slot-scope="slot">
<!-- <span v-for = "item in slot.data">{{item}}</span>-->
<span>{{ slot.data.join(' - ') }}</span>
</template>
</cpn>
</div>
<template id="cpn">
<div>
<slot :data="pLanguage">
<ul>
<li v-for="item in pLanguage">{{ item }}</li>
</ul>
</slot>
</div>
</template>
<script src="../js/vue.js">
</script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}, components: {
cpn: {
template: '#cpn',
data() {
return {
pLanguage: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
}
}
}
},
created() {
this.pLanguages.join(' - ')
}
})
</script>
</body>
</html>
2.使用模块作为出口
我们可以使用将需要暴露到外面的变量,使用一个模块作为出口,什么意思呢?
非常简单,在匿名函数内部,定义一个对象。
给对象添加各种需要暴露到外面的属性和方法(不需要暴露的直接定义即可)。
最后将这个对象返回,并且在外面使用了一个MoudleA接受。
接下来,我们在main.js中怎么使用呢?
我们只需要使用属于自己模块的属性和方法即可
这就是模块最基础的封装,事实上模块的封装还有很多高级的话题:
但是我们这里就是要认识一下为什么需要模块,以及模块的原始雏形。
幸运的是,前端模块化开发已经有了很多既有的规范,以及对应的实现方案。
常见的模块化规范:
CommonJS、AMD、CMD,也有ES6的Modules
CommonJS
模块化有两个核心:导出和导入
CommonJS的导出:
CommonJS的导入
module.exports = {
flag: flag,
sum: sum
}
但是这种方法需要有东西能够给我们解析,这里我们并没有webpack,所以这样子写是没有用的
var aaa = require('./aaa.js')
var flag = aaa.flag;
var sum = aaa.sum;
我们可以使用这种方式来导出,通过aaa来获取这个文件然后再来获取这个文件中的变量。也可以用下面这种写法,这样一步就等于上面的两步了
var {flag, sum} = require('./aaa.js')
export基本使用
export指令用于导出变量,比如下面的代码:
上面的代码还有另外一种写法:
也可以输出函数或者输出类
export default
某些情况下,一个模块中包含某个的功能,我们并不希望给这个功能命名,而且让导入者可以自己来命名
这个时候就可以使用export default
export default在同一个模块中,不允许同时存在多个。
var name = '小明'
var age = 18
var flag = true
function sum(num1,num2) {
return num1 + num2;
}
if (flag){
console.log(sum(20,30));
}
//1.导出方式1
export {
flag,sum
}
//2.导出方式2
export var num1 = 1000;
export var height = 1.88
//3.导出函数 / 类
export function mul(num1,num2) {
return num1 + num2;
}
export class Person{
run(){
console.log('在奔跑');
}
}
//5.export default
const address = '北京市'
//只能导出一个
// export default address;
export default function (argument) {
console.log(argument);
}
//1.导入的{} 中定义的变量
import {flag,sum} from "./aaa.js"
if (flag){
console.log('小明是天才,哈哈哈');
console.log(sum(20,30))
}
//2.直接导入export定义的变量
import {num1,height} from "./aaa.js";
console.log(num1)
console.log(height)
//3.导入 export 的 function
import {mul,Person} from "./aaa.js"
console.log(mul(30, 20));
const p = new Person();
p.run();
//4.导入export default
// import addr from './aaa.js'
//
// console.log(addr);
import f from './aaa.js'
f('你好啊~')
//5.统一全部导入
import * as aaa from './aaa.js'
console.log(aaa.flag)
console.log(aaa.height)