【Vue】(二)Vue组件化思想、全局组件与局部组件、父子组件、父子组件通信、slot-插槽

组件

1.什么是组件化:

  • 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的扩展和管理。
  • 但如果,我们将一个页面拆分成一个个小的功能块,每个功能模块只需要完成属于自己独立的那一部分功能,那么后续管理和维护这个页面就会变得非常容易了。
  • 我们将一个完整的页面分成很多个组件,每个组件都用于实现页面中的一个功能模块,而每一个组件又可以进行细分。例如下图:

image-20220913150404713

2.Vue组件化思想:

  • 组件化是vue中的重要思想
    • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。
    • 任何的应用都会被抽象成一颗组件树。

image-20220913151956817

3.组件化思想的应用:

  • 有了组件化的思想,我们在之后的开发中就要充分的利用它。
  • 尽可能的将页面拆分成一个个小的、可复用的组件。
  • 这样让我们的代码更加方便组织和管理,并且扩展性也更强。

所以,组件是Vue开发中,非常重要的一个篇章。

全局组件和局部组件

image-20220913164322211

4.全局组件和局部组件:

1.全局组件:意味着可以在多个vue实例下面使用

<div id="app">
    <cnp></cnp><!--使用组件-->
</div>
<script>
    //1.创建组件构造器
    const cnp = Vue.extend({
        template:`
        <div>
        	<h2>我是标题</h2>
        	<p>我是内容</p>
    	</div>
        `
    })
    //2.注册组件(全局组件,意味着可以在多个vue实例下面使用!)
    Vue.component('cnp',cnp)
</script>

2.局部组件:直接挂载在具体某个Vue实例下面创建,这样的话这个组件就只能在这个vue实例中使用!(开发中用的比较多的就是局部组件)

<div id="app">
    <cnp></cnp><!--使用组件-->
</div>
<script>
    //1.创建组件构造器
    const cnp1 = Vue.extend({
        template:`
        <div>
        	<h2>我是标题</h2>
        	<p>我是内容</p>
    	</div>
        `
    })
    const app = new Vue({
        el: "#app",
        data: {
            message: "你好!"
        },
        components: {
            cpn: cnp1//cpn为组件的标签名,cnp1为组件构造器
        }
    })
</script>

5.父组件与子组件:

  • 父组件中注册了子组件,所以形成了父子组件的关系
  • 需要注意作用域的问题,父组件中注册的子组件只作用于当前的父组件,子组件想单独渲染必须在根组件中的components属性下进行注册!
<div id="app">
    <cnp2></cnp2><!--使用组件-->
    <cnp1></cnp1><!--子组件需要在根组件下的components属性下进行注册才可以渲染成功!-->
</div>
<script>
    //1.创建组件构造器cnp1(子组件)
    const cnp1 = Vue.extend({
        template:`
        <div>
        	<h2>我是标题</h2>
        	<p>我是内容</p>
    	</div>
        `
    })
    
    //2.创建组件构造器cnp2(父组件)
    const cnp2 = new Vue({
        template: `
        <div>
        	<h2>我是标题</h2>
        	<p>我是内容</p>
    	</div>
    	<cnp1></cnp1>
        `,
        components: {
            cnp1: cnp1//在cnp2里注册cnp1
        }
    })
    
    //root组件(根组件)
    const app = new Vue({
        el: "#app",
        data: {
            message: "你好!"
        },
        components: {
            cnp2: cnp2//第一个cnp2为组件的标签名,第二个cnp2为组件构造器
            cnp1: cnp1//子组件需要在根组件下的components属性下进行注册才可以渲染成功,就是在当前根组件中进行注册!
        }
    })
</script>

6.全局组件和局部组件的语法糖:

概括

  • Vue为了简化这个过程,提供了注册的语法糖。
  • 主要是省去了调用Vue.extend()的步骤,而是可以直接使用一个对象来代替。

1.全局组件的语法糖:省去了组件构造器的步骤,直接放在注册组件(Vue.component())的后面(template),其实底层还是用了Vue.extend(),只是它底层帮我们做了这个步骤。

<script>
    <!--注册组件-->
Vue.component("cnp1",{
    template: `
            <div>
                <h2>我是标题</h2>
                <p>我是内容</p>
            </div>
            <cnp1></cnp1>
            `
})
</script>

2.局部组件的语法糖:将组件构造器参数直接用template那部分代码代替。

<script>
     //root组件(根组件)
    const app = new Vue({
        el: "#app",
        data: {
            message: "你好!"
        },
        components: {
            cnp2: {
                template: `
                    <div>
                        <h2>我是标题</h2>
                        <p>我是内容</p>
                    </div>
                    `
            }
        }
    })
</script>

7.模板的分离写法:

有两种写法:

1.第一种写法:通过script标签来写。

<script type="text/x-template" id="cpn">/第一种写法需要注意type的值!
    <div>
         <h2>我是标题</h2>
         <p>我是内容</p>
    </div>
</script>

<script>
    //注册一个全局组件
    Vue.component('cpn',{
        template: '#cpn'
    })
</script>

第二种写法:通过template标签来写。

<template id="cpn">
	<div>
         <h2>我是标题</h2>
         <p>我是内容</p>
    </div>
</template>

<script>
    Vue.component('cpn',{
        template: '#cpn'
    })
</script>

8.组件不能访问Vue实例数据:

  • 组件是一个单独功能模块的封装:
    • 这个模块有属于自己的HTML模板,也应该有属于自己的数据Data。
  • 组件中的数据是保存在哪里呢?顶层的Vue实例当中吗?
    • 组件不能直接访问Vue实例中的Data。
    • 我们发现不能访问,而且即使可以访问,如果将所有的数据都放在Vue实例当中,Vue实例就会变得非常的臃肿!
    • 结论:Vue组件应该有自己保存数据的地方。

9.组件数据的存放:

  • 组件自己的数据存放在哪里呢?
    • 组件对象也有一个data属性(也有methods属性,下面我们会用到!)
    • 只是组件中的这个data属性必须是一个函数
    • 而且这个函数返回一个对象(就是data里的return),对象内部保存着组件的数据

10.组件中的data为什么是函数(面试点):

  • 如果data是函数的话,那么创建组件的时候都会去调用这个data函数,每一次调用data函数都会return一个新的对象,(因为data是一个函数,所以一旦被组件实例调用,返回的都是该组件实例所拥有的数据),然后每个组件实例都能获得自己想要的数据。
  • 可反复调用,反复创建新对象
  • 避免组件间数据冲突

11.父子组件的通信:

  • 子组件是不能引用父组件或者Vue实例的数据的。

  • 但是,在开发中,往往一些数据确实需要从上层传递到下层∶

    • 比如在一个页面中,我们从服务器请求到了很多的数据。
    • 其中一部分数据,并非是我们整个页面的大组件来展示的,而是需要下面的子组件进行展示。
    • 这个时候,并不会让子组件再次发送一个网络请求,而是直接让大组件(父组件)将数据传递给小组件(子组件)。
  • 如何进行父子组件间的通信呢?Vue官方提到

    • 通过props向子组件传递数据
    • 通过事件向父组件发送消息
    • 真实的开发中,Vue实例和子组件的通信和父组件和子组件的通信过程是一样的。

image-20220914141637310

父子组件的通信-父传子props

props的值有两种方式∶

  • 方式一︰字符串数组,数组中的字符串就是传递时的名称。
  • 方式二︰对象,对象可以设置传递时的类型,也可以设置默认值等。
<div id="app">
    <cpn v-bind:cmessage="message" :cmovies="movies"></cpn>
</div>

<template id="cpn">
	<div>
        <ul>
        	<li v-for="items in cmovies">{{items}}</li>    
    	</ul>
        <p>{{cmovies}}</p>
    </div>
</template>

<script>
    //父传子props
    const cpn = {
        template: '#cpn',
       // props: ['cmessage','cmovies'],//props的数组形式
        props: {     //props的对象形式
        //1.类型限制
        //cmovies: Array,
        //cmessage: String,
        
        //2.提供一些默认值以及必传值
        cmessage: {
        type: String,
        default: '指定默认值',
        required: true //用的时候必须传这个变量,否则报错!
    },
    	cmovies: {
       	type: Array,
        //类型是对象或者数组时,默认值必须是一个函数!
        default(){
              return[]
            }
    }
    }
        data(){
            return{}
        },
        methods: {
            
        }
    }
    
    const app = new Vue({
        el: '#app',
        data: {
            message: '你好啊',
            movies: ['篮球','吉他','音乐']
            components:{
            	cpn
        }
        }
    })
</script>
props数据验证:

在前面,我们的props选项是使用一个数组。

  • 我们说过,除了数组之外,我们也可以使用对象,当需要对props进行类型等验证时,就需要对象写法了。

  • 验证都支持哪些数据类型呢?

    • String

    • Number

    • Boolean

    • Array

    • Object

    • Date

    • Function

    • Symbol

props中的驼峰标识:

大写字母改成"-"加上小写字母,例如:cInfo------->c-info

<div id="app"> 
    <cpn :c-info="info" :child-my-message="info"></cpn><!--这里的c-info和child-my-message就是用到了props的驼峰标识-->
</div>

<template id="cpn">
	<h2>{{cInfo}}</h2>
	<p>{{childMyMessage}}</p>
</template>

<script>
    const cpn = {
        template: '#cpn',
        props: {
            cInfo: {
                type: Object,
            	default() {
                	return{}
            }
            },
            childMyMessage: {
                 type: String,
                 default: ''
            }
        }
    }
    
    const app = new Vue({
        el: '#app',
        data: {
            info: {
                name: '机车',
                age: 18,
                height; 1.88
            },
            message: 'aaaaaaa'
        }
    })
</script>
父子组件的通信-子传父(自定义事件):

props用于父组件向子组件传递数据,还有一种比较常见的是子组件传递数据或事件到父组件中。

我们应该如何处理呢?这个时候,我们需要使用自定义事件来完成。
什么时候需要自定义事件呢?

  • 当子组件需要向父组件传递数据时,就要用到自定义事件了。

  • 我们之前学习的v-on不仅仅可以用于监听DOM事件,也可以用于组件间的自定义事件。

    自定义事件的流程∶︰

    • 在子组件中,通过$emit()来触发事件。
    • 在父组件中,通过v-on来监听子组件事件。
<!--父组件模板-->
<div id="app">
    <cpn @item-click="cpnClick(item)"></cpn><!--cpnClick后面的参数item可以选择不传,可以省略掉,因为@item-click是自定义事件,不是浏览器的@click事件,所有在子组件cpn发射事件的时候(this.$emit('item-click',item))它会默认帮你把参数传过来!-->
</div>

<!--子组件模板-->
<template id="cpn">
	<div>
        <button v-for="item in categories"
                @click="btnClick(item)">
        	    {{item.name}}
        </button>
    </div>
</template>

<script>
    
    //子组件
    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)
            }
        }
    }
    
    //父组件
    const app = new Vue({
        el: '#app',
        data: {
            message: '你好阿!'
        },
        components: {
            cpn
        },
        methods: {
            cpnClick(item){
                console.log('cpnClick',item)
            }
        }
    })
</script>
组件访问-父访问子-$children-$refs:

1.开发中使用$refs方式最多

<div id="app">
    <button @click="btnClick">按钮</button>
    <cpn ref="aaa"></cpn>
</div>

<template id="cpn">
	<div>我是子组件</div>
</template>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            message: '你好阿!'
        },
        methods: {
            btnClick(){
                //1.$children
                console.log(this.$children)
                for(let c of this.$children){
                    console.log(c.name)
                    c.showMessage()
                }
                console.log(this.$children[3].name)
                
                //2.$refs ==> 对象类型,默认是一个空的对象,需要在组件标签中指定 ref='aaa'
                console.log(this.$refs.aaa.name)
            }
        },
        components: {
            cpn: {
                template: '#cpn',
                data(){
                    return{
                        name: '我是子组件的name'
                    }
                },
                methods: {
                	showMessage(){
            			console.log('showMessage')
        }
            }
            }
        },
</script>
组件访问-子访问父-$parent-$root:

p a r e n t 开发中用的更少, parent开发中用的更少, parent开发中用的更少,root用的也不是很多

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<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',
                    methods: {

                        },
                    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>
</html>

12.组件化高级-slot- 插槽:

为什么使用插槽?

slot翻译为插槽:

  • 在生活中很多地方都有插槽,电脑的USB插槽,插板当中的电源插槽。
  • 插槽的目的是让我们原来的设备具备更多的扩展性。
  • 比如电脑的USB我们可以插入U盘、硬盘、手机、音响、键盘、鼠标等等。

组件的插槽:

  • 组件的插槽也是为了让我们封装的组件更加具有扩展性。
  • 让使用者可以决定组件内部的一些内容到底展示什么。

例子:移动网站中的导航栏。

  • 移动开发中,几乎每个页面都有导航栏。

  • 导航栏我们必然会封装成一个插件,比如nav-bar组件。一旦有了这个组件,我们就可以在多个页面中复用了。

    但是,每个页面的导航是一样的吗? No,我以京东M站为例

image-20220916115115016

如何封装这类组件呢?slot
  • 如何去封装这类的组件呢?
    • 它们也很多区别,但是也有很多共性。
    • 如果,我们每一个单独去封装一个组件,显然不合适︰比如每个页面都返回,这部分内容我们就要重复去封装。
    • 但是,如果我们封装成一个,好像也不合理∶有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等。
  • 如何封装合适呢?抽取共性,保留不同。
    • 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。
    • 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。
    • 是搜索框,还是文字,还是菜单。由调用者自己来决定。
  • 这就是为什么我们要学习组件中的插槽slot的原因。
slot-插槽的基本使用:
  • 插槽的基本使用:<slot></slot>
  • 插槽的默认值:<slot>这里放默认值</slot>
  • 如果有多个值,同时放入到组件进行替换时,一起作为元素进行替换(会全部替换)。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

    <div id="app">
        <cpn></cpn>
        <cpn><i>酷酷酷</i></cpn>
        <cpn><p>帅刷机</p></cpn>
        <cpn>
            <div>机车</div><!--这三个会全部替换-->
            <i>篮球</i>
            <p>吉他</p>
        </cpn>
    </div>

    <template id="cpn">
        <div>
            <h2>我是组件</h2>
            <p>我是组件,哈哈哈</p>
            <slot><button>按钮</button></slot>
<!--            <slot></slot>-->
        </div>
    </template>

    <script src="../js/vue.js"></script>
    <script>
        const app = new Vue({
            el: '#app',
            data: {
                message: '你好啊'
            },
            components: {
                cpn: {
                    template: '#cpn',
                }
            }
        })
    </script>
</body>
</html>
slot-具名插槽的使用:
<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Title</title>
</head>
<body>
   <div id="app">
       <cpn><span slot="left">标题</span></cpn>
       <cpn><button slot="center">返回</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="../js/vue.js"></script>
   <script>
       const app = new Vue({
           el: '#app',
           data: {
               message: '你好啊'
           },
           components: {
               cpn: {
                   template: '#cpn',
               }
           }
       })
   </script>
</body>
</html>
编译作用域的概念:

在哪个组件模板里调用,就用哪个组件实例的值

  • 在真正学习插槽之前,我们需要先理解一个概念∶编译作用域。

  • 官方对于编译的作用域解析比较简单,我们自己来通过一个例子来理解这个概念∶

  • 我们来考虑下面的代码是否最终是可以渲染出来的︰

    • <my-cpn v-show="isShow"></my-cpn>中,我们使用了isShow属性。
    • isShow属性包含在组件中,也包含在Vue实例中。
  • 答案︰最终可以渲染出来,也就是使用的是Vue实例的属性。

  • 为什么呢?

    • 官方给出了—条准则∶父姐件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
    • 而我们在使用<my-cpn v-show="isShow"></my-cpn>的时候,整个组件的使用过程是相当于在父组件中出现的。
    • 那么他的作用域就是父组件,使用的属性也是属于父组件的属性。
    • 因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。

    代码例子如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!--父组件模板-->
    <div id="app">
        <cpn v-show="isShow"></cpn>
    </div>

    <!--子组件模板-->
    <template id="cpn">
        <div>
            <h1>我是子组件</h1>
            <p>我是内容,哈哈哈哈哈</p>
        </div>
    </template>

    <script src="../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>
</html>
slot-作用域插槽:

作用域插槽是slot一个比较难理解的点,而且官方文档说的又有点不清晰。

这里,我们用一句话对其做一个总结,然后我们在后续的案例中来体会∶

  • 父组件替换插槽的标签,但是内容由子组件来提供。

我们先提一个需求:

  • 子组件中包括一组数据,比如: pLanguages: [‘JavaScript’,‘Python’, ‘Swift’,‘Go’, ‘C++’]
  • 需要在多个界面进行展示︰
    • 某些界面是以水平方向一—展示的,
    • 某些界面是以列表形式展示的,
    • 某些界面直接展示一个数组
  • 内容在子组件,希望父组件告诉我们如何展示,怎么办呢?
    • 利用slot作用域插槽就可以了
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!--父组件模板-->
    <div id="app">
        <cpn></cpn>
        <!--目的是获取子组件中的pLanguages-->
        <cpn>
            <template slot-scope="slot"><!--这里的slot-scope-->
<!--                <span v-for="item in slot.data">{{item}} - </span>-->
                <span>{{slot.data.join(' - ')}}</span><!--这里的slot.data-->
            </template>
        </cpn>

        <cpn>
            <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="pLanguages"><!--这里的:data="pLanguages"-->
                <ul>
                    <li v-for="item in pLanguages">{{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{
                        pLanguages: ['javaScript','c++','java','c','python','Go']
                    }
                }
            }
        }
    })
    </script>
</body>
</html>
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值