一、关于组件化
1、什么是组件化
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,会发现大的问题也会迎刃而解;
后续的管理以及扩展,但如果将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后
整个页面的管理和维护就变得非常容易了;
2、vue组件化思想
组件化是 Vue.js 中的重要思想 ,它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;
任何的应用都会被抽象成一颗组件树:
有了组件化的思想,我们在之后的开发中就要充分的利用它,尽可能的将页面拆分成一个个小的、可复用的组件,
这样让我们的代码更加方便组织和管理,并且扩展性也更强;
二、注册组件
1、基本使用
组件的使用分成三个步骤:
- 创建组件构造器
- 注册组件
- 使用组件
<body> <div id="app"> <!--步骤三:使用组件两次(在Vue实例的作用范围内使用)--> <my-cpn></my-cpn> <my-cpn></my-cpn> </div> <script src="../js/vue.js"></script> <script> // 步骤一:创建组件构造器 const cpnC = Vue.extend({ // template使用的是反引号 template: ` <div> <h2>标题</h2> <p>内容</p> </div>` }) //步骤二:注册组件,定义组件标签名称 Vue.component('my-cpn', cpnC) const app = new Vue({ el: '#app', data: {} }) </script> </body>
2、全局组件和局部组件
当我们通过调用 Vue.component()
注册组件时,组件的注册是全局的 ,这意味着该组件可以在任意 Vue 实例下使用;
如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件;
上面的例子是全局组件案例,下面看一下局部组件:
<body> <div id="app"> <my-cpn></my-cpn> <my-cpn></my-cpn> </div> <script src="../js/vue.js"></script> <script> const cpnC = Vue.extend({ template: ` <div> <h2>标题</h2> <p>内容</p> </div>` }) //在实例内部注册组件,为局部组件,其他实例不能使用 const app = new Vue({ el: '#app', data: {}, components: { 'my-cpn': cpnC } }) </script> </body>
3、父组件和子组件
<body> <div id="app"> <my-cpn2></my-cpn2> </div> <script src="../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> <my-cpn1></my-cpn1> </div> `, //在组件2中注册组件1 components: { 'my-cpn1': cpnC1 } }) //root组件 const app = new Vue({ el: '#app', data: {}, components: { 'my-cpn2': cpnC2 } }) </script> </body>
4、注册组件语法糖
在上面注册组件的方式,可能会有些繁琐,Vue为了简化这个过程,提供了注册的语法糖,主要是省去了调用Vue.extend()
的步骤,直接使用一个对象来代替;
<body> <div id="app"> <my-cpn1></my-cpn1> </div> <script src="../js/vue.js"></script> <script> // 1. 注册全局组件的语法糖 Vue.component('my-cpn1', { template: ` <div> <h2>标题1</h2> <p>内容</p> </div> ` }) const app = new Vue({ el: '#app', data: {}, //2.注册局部组件的语法糖 components: { 'my-cpn2': { template: ` <div> <h2>组件标题2</h2> <p>组件2中的一个段落内容</p> </div> ` } } }) </script> </body>
5、模板分离写法
使用 script 标签或 template 标签将模板内容从注册时的 template 中抽离出来;
<body> <div id="app"> <cpn-c1></cpn-c1> </div> <!--模板分离方式1:使用script标签--> <script type="text/x-template" id="cpn1"> <div> <h3>标题1</h3> <p>内容</p> </div> </script> <!--模板分离方式2:使用template标签--> <template id="cpn2"> <div> <h3>标题2</h3> <p>内容</p> </div> </template> <script src="../js/vue.js"></script> <script> Vue.component('cpnC1', { template: '#cpn2' }) const app = new Vue({ el: '#app', data: {} }) </script> </body>
三、组件的数据存放
组件是一个单独功能模块的封装,这个模块有属于自己的HTML模板,也应该有属性自己的数据 data,那么组件中的数据是保存在哪里呢?顶层的Vue实例中吗?
1、组件中能不能直接访问 Vue 实例中的 data
组件不能直接访问 Vue 实例中的 data,而且即使可以访问,如果将所有的数据都放在 Vue 实例中,Vue 实例就会变的非常臃肿;
2、组件数据的存放
其实,组件对象也有一个 data 属性,只是这个 data 属性必须是一个函数,而且这个函数返回一个对象,对象内部保存着数据;
<body> <div id="app"> <my-cpn1></my-cpn1> </div> <template id="cpn"> <div> <h2>{{title}}</h2> <p>内容</p> </div> </template> <script src="../js/vue.js"></script> <script> const app = new Vue({ el: '#app', data: {}, components: { 'my-cpn1': { template: '#cpn', data() { return { title: '标题' } } } } }) </script> </body>
2、为什么 data 在组件中必须是一个函数呢?
- 首先,如果不是一个函数,Vue直接就会报错
- 其次,Vue 让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响
四、父子组件的通信
在上一个小节中,我们提到了子组件是不能引用父组件或者Vue实例的数据的。
但是,在开发中,往往一些数据确实需要从上层传递到下层:
比如在一个页面中,我们从服务器请求到了很多的数据。
其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
如何进行父子组件间的通信呢?Vue官方提到
父传子:props
子传父:自定义事件
1、父传子:props
props的值有两种方式:
方式一:字符串数组,数组中的字符串就是传递时的名称。
方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
(1)方式一:字符串数组
首先有一个组件cpn,然后在父组件app里面注册,使用组件时用v-bind将父组件中的数据赋给了 cmovies,
cmovies会将数据传递到props中,然后props又会将数据渲染到模板,最后呈现出来;
<body> <div id="app"> <cpn v-bind:cmovies="movies"></cpn> </div> <template id="cpn"> <div> <ul> <li v-for="item in cmovies">{{item}}</li> </ul> </div> </template> <script src="../js/vue.js"></script> <script> //父传子 props const cpn = { template: '#cpn', props: ['cmovies'], data() { return {} } } //父 const app = new Vue({ el: '#app', data: { movies: ['one', 'two', 'three'] }, components: { cpn } }) </script> </body>
(2)方式二:对象
除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。
当我们有自定义构造函数时,验证也支持自定义的类型;
支持的数据类型:
String
Number
Boolean
Array
Object
Date
Function
Symbol
<body> <div id="app"> <cpn v-bind:cmovies="movies"></cpn> </div> <template id="cpn"> <div> <ul> <li v-for="item in cmovies">{{item}}</li> </ul> </div> </template> <script src="../js/vue.js"></script> <script> //父传子 props const cpn = { template: '#cpn', // props: ['cmovies'], props: { cmovies: { type: Array, //限制类型 // default: 'abc', default () { //默认值;类型是对象或者数组时,默认值必须是一个函数 return [] }, required: true, //是否必须 } }, data() { return {} } } //父 const app = new Vue({ el: '#app', data: { movies: ['one', 'two', 'three'] }, components: { cpn } }) </script> </body>
验证支持的数据类型:
自定义类型:
(3)props驼峰标识
<body> <div id="app"> <!--cMovies在这里要写成c-movies--> <cpn v-bind:c-movies="movies"></cpn> </div> <template id="cpn"> <div> <ul> <li v-for="item in cMovies">{{item}}</li> </ul> </div> </template> <script src="../js/vue.js"></script> <script> //父传子 props const cpn = { template: '#cpn', // props: ['cmovies'], props: { // cMovies cMovies: { type: Array, //限制类型 // default: 'abc', default () { //默认值;类型是对象或者数组时,默认值必须是一个函数 return [] }, required: true, //是否必须 } }, data() { return {} } } //父 const app = new Vue({ el: '#app', data: { movies: ['one', 'two', 'three'] }, components: { cpn } }) </script> </body>
2、子级向父级传递数据
自定义事件流程:
- 在子组件中,通过
$emit()
来触发事件 - 在父组件中,通过
v-on
来监听子组件事件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <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: 'aaa', name: '热门推荐'}, {id: 'bbb', name: '手机数码'}, {id: 'ccc', name: '家用家电'}, {id: 'ddd', 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> </html>
3、双向绑定
<body> <!-- 父组件模板 --> <div class="app"> <cpn :number1="num1" :number2="num2" @num1change="num1change" @num2change="num2change"></cpn> </div> <!-- 子组件模板 --> <template id="cpn"> <div> <h2>props:{{number1}}</h2> <h2>data:{{dnumber1}}</h2> <!-- <input type="text" v-model="dnumber1"> --> <input type="text" :value="dnumber1" @input="num1Input"> <h2>props:{{number2}}</h2> <h2>data:{{dnumber2}}</h2> <!-- <input type="text" v-model="dnumber2"> --> <input type="text" :value="dnumber2" @input="num2Input"> </div> </template> <script src="../js/vue.js"></script> <script> // 父组件 const app = new Vue({ el: '.app', data: { num1: 1, num2: 0 }, methods: { num1change(value) { this.num1 = parseFloat(value) }, num2change(value) { this.num2 = parseFloat(value) } }, // 子组件 components: { cpn: { template: '#cpn', // props中的数据只能父组件修改,通过使用标签动态绑定 props: { // 父传子 number1: Number, number2: Number }, data() { return { dnumber1: this.number1, dnumber2: this.number2, } }, methods: { num1Input(event) { // 将input中的vlaue赋值给dnumber1 this.dnumber1 = event.target.value // 发射一个事件,让父组件可以修改值(子传父) this.$emit('num1change', this.dnumber1) // 修改dnumber2的值 this.dnumber2 = this.dnumber1 * 100 this.$emit('num2change', this.dnumber2) }, num2Input(event) { this.dnumber2 = event.target.value this.$emit('num2change', this.dnumber2) this.dnumber1 = this.dnumber2 / 100 this.$emit('num1change', this.dnumber1) } } } } }) </script> </body>
五、父子组件访问
1、父组件访问子组件
父组件访问子组件有两种方式:
- $children(不常用)
- $refs(常用)
(1)$children
this.$children
是一个数组类型,它包含所有子组件对象,可以通过遍历,取出所有子组件的信息;
<body> <div id="app"> <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: {}, methods: { btnClick() { console.log(this.$children); for (let c of this.$children) { console.log(c.name) //访问子组件中的属性 this.$children[0].showMessage() //调用子组件中的方法 } } }, components: { cpn: { template: '#cpn', data() { return { name: '我是子组件的name' } }, methods: { showMessage() { console.log('子组件中showMessage方法') } } }, } }) </script> </body>
$children 的缺陷:
通过 $children 访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值,但是当子组件过多,我们需要拿到其中一个时,
往往不能确定它的索引值,甚至还可能会发生变化;
有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用 $refs
(2)$refs
$refs 和 ref 指令通常是一起使用的:
首先,我们通过 ref 给某一个子组件绑定一个特定的 ID,然后通过 this.$refs.ID
就可以访问到该组件了;
<body> <div id="app"> <cpn></cpn> <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', methods: { btnClick() { // 2.$refs => 对象类型, 默认是一个空的对象 ref='bbb' console.log(this.$refs.aaa.name); } }, components: { cpn: { template: '#cpn', data() { return { name: '我是子组件的name' } }, methods: { showMessage() { console.log('showMessage'); } } }, } }) </script> </body>
2、子组件访问父组件(不常用)
子组件访问父组件通过:$parent
如果是向访问根组件,通过:$root
<body> <div id="app"> <cpn></cpn> </div> <template id="cpn"> <div> <h2>我是cpn组件</h2> <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: { message: '你好啊' }, components: { cpn: { template: '#cpn', data() { return { name: '我是cpn组件的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.message); } } } } } } }) </script> </body>
注意:
- 尽管在 Vue 开发中,我们允许通过 $parent 来访问父组件,但是在真实开发中尽量不要这样做,因为这样耦合度太高了;
- 子组件应该尽量避免直接访问父组件的数据,如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题 ;
- 外,通过 $parent 直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于调试和维护 ;
- 不常用 $root 来访问根组件(即 vue 实例),因为根组件中一般只存放路由等重要数据,不存放其他信息;