从零开始学习Vue(二)

641 阅读12分钟

组件模块化

简介

如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。但如果,我们讲一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了。

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

组件化思想的应用:

  • 尽可能将页面拆分成一个个小的、可复用的组件。
  • 代码更方便组织和管理,扩展性更强。

组件的使用

组件的使用分成三个步骤:

  • 调用Vue.extend()方法创建组件构造器
  • 调用Vue.component()方法注册组件
  • 在Vue实例的作用范围内使用组件
// HTML
<div id="app">
    <!--步骤三:Vue实例的作用范围内使用组件-->
    <my-cpn></my-cpn>
</div>
<!--作用范围外,无法渲染-->
<my-cpn></my-cpn>


<script>
    <!--步骤一:创建组件构造器-->
    <!--传入template代表我们自定义组件的模板。-->
    <!--这种写法在Vue2.x的文档中几乎已经看不到了-->
    const myComponent = Vue.extend({
        template: `
            <div>
                <h2>组件标题</h2>
            </div>
        `
    });
    
    <!--步骤二:注册组件,并且定义组件标签的名称-->
    <!--传递两个参数:1、注册组件的标签名   2、组件构造器-->
    Vue.component('my-cpn', myComponent);
    
    const app = new Vue({
        el: '#app'
    })
</script>

全局组件和局部组件

当我们通过调用**Vue.component()**注册组件时,组件的注册时全局的。这意味着该组件可以在任意Vue实例下使用。
如果我们注册的组件时挂载在某个实例中,那么就是一个局部组件。

<div id="app">
    <my-cpn></my-cpn>   // 注册在#app下的局部组件,可以渲染
    <my-cpn1></my-cpn1> //全局组件,可以渲染
</div>
<div id="app1">
    <my-cpn></my-cpn>   // 无法渲染
    <my-cpn1></my-cpn1> // 可以渲染
</div>


<script>
    const myComponent = Vue.extend({
        template: `
            <div>
                <h2>组件标题</h2>
            </div>
        `
    });
    
    <!--全局注册-->
    Vue.component('my-cpn1', myComponent);
    
    const app = new Vue({
        el: '#app',
        components: {
            'my-cpn': myComponent
        }
    })
    const app1 = new Vue({
        el: '#app1'
    })
</script>

父组件和子组件

组件和组件之间存在层级关系,其中一种非常重要的关系就是父子组件的关系。

<div id="app">
    <parent-cpn></parent-cpn>
    
    <!--错误用法,子组件标签只能在父组件中被识别-->
    <child-cpn></child-cpn>
</div>


<script>
    const parentComponent = Vue.extend({
        template: `
            <div>
                <h2>父组件标题</h2>
                <!--当子组件注册到父组件的components时,Vue会编译好父组件的模块,将子组件标签替换为子组件的模板内容-->
                <child-cpn></child-cpn>
            </div>
        `,
        components: {
            'child-cpn': childComponent
        }
    });
    const childComponent = Vue.extend({
        template: `
            <div>
                <h2>父组件标题</h2>
            </div>
        `
    });
    
    const app = new Vue({
        el: '#app',
        components: {
            'parent-cpn': parentComponent
        }
    })
</script>

注册语法糖

Vue为了简化祖册组件的过程,提供了注册的语法糖,省去了调用VUe.extend()的步骤,而是直接使用一个对象来代替。

<!--语法糖注册全局组件和局部组件-->
<!--全局组件-->
Vue.component('myCpn', {
    template:`
        <div>
            <h2>父组件标题</h2>
        </div>
    `
})
<!--局部组件-->
const app = new Vue({
    el: "#app",
    components: {
        'my-cpn': {
            template: `
            <div>
                <h2>父组件标题</h2>
            </div>
        `
        }
    }
})

模板的分离写法

通过语法糖简化了Vue组件的注册过程,但是也导致了template模块写法较为麻烦. Vue提供了两种方案来将其中的HTML分离出来,然后再挂载到对应的组件上,这样解构会变得非常清晰.

  • 使用<script>标签
  • 使用<template>标签
<div id="app">
    <my-cpn></my-cpn>
</div>

<!--方案一: 使用<script>标签-->
<script type="text/x-template" id="myCpn">
    <div>
        <h2>组件标题</h2>
    </div>
</script>

<!--方案二: 使用<template>标签-->
<template id="myCpn">
    <div>
        <h2>组件标题</h2>
    </div>
</template>

<script>
    const app = new Vue({
        el: '#app',
        components: {
            'my-cpn': {
                template: '#myCpn'
            }
        }
    })
</script>

组件中的数据访问

组件时一个单独功能模块的封装,这个模块有属于自己的HTML模板,也应该有属于自己的数据data.组件不能直接访问Vue实例中的data.

组件对象也有一个data属性,只是这个data属性必须时一个函数,而且这个函数返回一个对象,对象内部保存着数据.

原因: 首先,如果不是一个函数,Vue直接会报错;其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响.

<div id="app">
    <my-cpn></my-cpn>
</div>
<template id="myCpn">
    <div>
        <h2>{{message}}</h2>
    </div>
</template>

<script>
    const app = new Vue({
        el: '#app',
        components: {
            'my-cpn': {
                template: '#myCpn',
                data() {
                    return {
                        message: 'hello world'
                    }
                }
            }
        }
    })
</script>

父子组件的通信

父子组件间的通信:

  • 通过props向子组件传递数组
  • 通过时间向父组件发消息

父传子——props基本用法

在组件中,使用选项props来声明需要从父级接收到的数据.
props的值有两种方式:

  • 字符串数组,数组中的字符串就是传递时的名称
  • 对象,对象可以设置传递时的类型,也可以设置默认值等.
字符串数组用法
<div id="app">
    <my-cpn :message="pMassage"></my-cpn>
</div>
<template id="myCpn">
    <div>
        <h2>{{message}}</h2> // hello world
    </div>
</template>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            pMassage: 'hello world'
        }
        components: {
            'my-cpn': {
                template: '#myCpn',
                props: ['message']
            }
        }
    })
</script>
对象用法

类型验证支持的类型:

  • String
  • Number
  • Boolean
  • Array
  • Object
  • Date
  • Function
  • Symbol
function Person (fName, lName) {
    this.firstName = fName;
    this.lastName = lName;
}
Vue.component('my-cpn', {
    props: {
        <!--基础的类型检查(null匹配任何类型)-->
        propA: Number,
        author: Person,
        <!--多个可能的类型-->
        propB: [Number, String]
        <!--必填的字符串-->
        propC: {
            type: String,
            required: true
        },
        <!--带有默认值的数字-->
        propD: {
            type: Number,
            defalut: 100
        },
        <!--带有默认值的对象-->
        propE: {
            type: Object,
            default() {
                return {message: 'hello'}
            }
        },
        <!--自定义验证函数-->
        propF: {
            validator(value) {
                return ['success', 'warning', 'danger'].indexof(value) !== -1;
            }
        }
    }
})

子传父——自定义事件

当子组件需要向父组件传递数据时,需要自定义事件,可以通过v-on监听组件间的自定义事件。

自定义事件的流程:

  • 在子组件中,通过$emit()来触发事件。
  • 在父组件中,通过v-on来监听子组件事件。
<div id="app">
    <child-cpn @increment="changeTotal" @decrement="changeTotal"></child-cpn>
    <h2>{{total}}</h2>
</div>

<template id="childCpn">
    <div>
        <button @click = "increment">+1</button>
        <button @click = "decrement">-1</button>
    </div>
</template>

<script>
    const app = new Vue({
        el: '#app',
        data: {
            total: 0
        },
        methods: {
          changeTotal(value) {
              this.total = value;
          }  
        },
        components: {
            'child-cpn': {
                template: '#childCpn',
                data() {
                  counter: 0  
                },
                methods: {
                    increment() {
                        this.counter++;
                        this.$emit('increment', this.counter);
                    },
                    decrement() {
                        this.counter--;
                        this.$emit('decrement', this.counter);
                    }
                }
            }
        }
    })
</script>

父子组件的访问

有时候我们需要父组件直接访问子组件,子组件直接访问父组件,或者子组件访问根组件。
父组件访问子组件:使用$children或$refs
子组件访问父组件:使用$parent
子组件访问根组件:使用$root

父组件访问子组件

$children

this.$children是一个数组类型,它包含所有子组件对象.
缺陷: 通过$children访问子组件时,访问其中的子组件必须通过索引值,但是当子组件过多时,往往不能确定它的索引值,甚至可能发生变化.

<div id="app">
    <parent-cpn></parent-cpn>
</div>

<!--父组件template-->
<template id="parentCpn">
    <child-cpn1></child-cpn1>
    <child-cpn2></child-cpn2>
    <button @click="showChildCpn">显示所有子组件信息</button>
</template>

<!--子组件1 template-->
<template id="childCpn1">
    <h2>我是子组件1</h2>
</template>

<!--子组件2 template-->
<template id="childCpn2">
    <h2>我是子组件2</h2>
</template>

<script>
    Vue.component('parent-cpn', {
        template: '#parentCpn',
        methods: {
            showChildCpn() {
                console.log(this.$children); // [VueComponent, VueConponent]
            }
        },
        components: {
            'child-cpn1': '#childCpn1',
            'child-cpn2': '#childCpn2'
        }
    })
    
    const app = new Vue({
        el: '#app'
    })
</script>
$refs(推荐使用)

使用:

  • $refs和ref指令通常是一起使用的.
  • 通过ref给某一个子组件绑定一个特定的ID
  • 通过this.$ref.ID就可以访问到该组件了
<child-cpn ref="child1"></child-cpn>
<child-cpn ref="child2"></child-cpn>
<button @click="showRefsCpn">通过refs访问子组件</button>


showRefsCpn() {
    console.log(this.$refs.child1);
    console.log(this.$refs.child2);
}

子组件访问父组件($parent)

注意事项:

  • 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真是开发中尽量不要这么做.
  • 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了.
  • 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题.
  • 另外,更不好做的事通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,不利于调试和维护.
<div id="app">
    <parent-cpn></parent-cpn>
</div>

<!--父组件template-->
<template id="parentCpn">
    <child-cpn1></child-cpn>
</template>

<!--子组件1 template-->
<template id="childCpn">
    <button @click="showParent">显示父组件信息</button>
</template>

<script>
    Vue.component('parent-cpn', {
        template: '#parentCpn',
        data() {
          return {}  
        },
        components: {
            'child-cpn': '#childCpn',
            methods: {
                showParent() {
                    console.log(this.$parent);  //访问父组件
                    console.log(this.$root);    //访问根组件
                }
            }
        }
    })
    
    const app = new Vue({
        el: '#app'
    })
</script>

编译作用域

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译.

<div id="app">
    <!--渲染成功-->
    <parent-cpn v-show="isShow"></parent-cpn>
</div>

<template id="parentCpn">
    <h2>显示组件内容</h2>
</template>


<script>
    Vue.component('parent-cpn', {
        template: '#parentCpn',
        data() {
          return {
              isShow: false
          }  
        }
    })
    
    const app = new Vue({
        el: '#app',
        data: {
            isShow: true
        }
    })

插槽slot

组件的插槽可以让我们封装的组件更加具有扩展性.
最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽,由使用者根据自己的需求决定插槽中插入什么内容.

基本使用

<div id="app">
    <!--无替换元素,显示插槽默认内容-->
    <my-cpn></my-cpn>
    
    <!--替换插槽的内容-->
    <my-cpn>
        <h2>我是替换内容</h2>
        <p>我也是替换内容</p>
    </my-cpn>
</div>

<template id="myCpn">
    <div>
        <slot>我是一个插槽中的默认内容</slot>
    </div>
</template>

<script>
    Vue.component('my-cpn', {
        template: '#myCpn'
    })
    
    let app = new Vue({
        el: '#app'
    })
</script>

具名插槽slot

当子组件的功能复杂时,子组件的插槽可能并非一个,这时候我们就需要使用具名插槽(给slot元素一个name属性)来对插槽进行区分,从而能在指定位置替换内容.

<div id="app">
    <!--无替换元素,显示插槽默认内容-->
    <my-cpn></my-cpn>
    
    <!--替换无名插槽的内容-->
    <my-cpn>
        <h2>我是替换内容</h2>
        <p>我也是替换内容</p>
    </my-cpn>
    
    <!--替换指定插槽的内容-->
    <my-cpn>
        <h2 slot="left">替换左边插槽</h2>
    </my-cpn>
    
    <my-cpn>
        <h2 slot="left">替换左边插槽</h2>
        <h2 slot="mid">替换中间插槽</h2>
        <h2 slot="right">替换右边插槽</h2>
    </my-cpn>
</div>

<template id="myCpn">
    <div>
        <slot name="left">左边插槽</slot>
        <slot name="mid">中间插槽</slot>
        <slot name="right">右边插槽</slot>
        <!--一个不带 name 的 <slot> 出口会带有隐含的名字“default”。-->
        <slot>无名插槽</slot>
    </div>
</template>

<script>
    Vue.component('my-cpn', {
        template: '#myCpn'
    })
    
    let app = new Vue({
        el: '#app'
    })
</script>

作用域插槽

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

<div id="app">
    <!--1. 列表形式展示-->
    <my-cpn>
        <!--获取到slotProps属性-->
        <template slot-scope="slotProps">
            <ul>
                <li v-for="item in slotProps.data">{{item}}</li>
            </ul>
        </template>
    </my-cpn>
    
    <!--2. 水平展示-->
    <my-cpn>
        <!--获取到slotProps属性-->
        <template slot-scope="slotProps">
            <span v-for="item in slotProps.data">{{item}} </span>
        </template>
    </my-cpn>
</div>

<template id="myCpn">
    <div>
        <slot :data="pLanguage"></slot>
    </div>
</template>

<script>
    Vue.component('my-cpn', {
        template: '#myCpn',
        data() {
            pLanguage: ['JS', 'HTML', 'CSS']
        }
    })
    
    let app = new Vue({
        el: '#app'
    })
</script>

Vue 2.6.0更新后的具名插槽slot

<div id="app">
    <!--替换指定插槽的内容-->
    <!--<template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。-->
    <!--具名插槽的缩写为: #-->
    <my-cpn>
        <!-- 可缩写为<template #left> -->
        <template v-slot:left>
            <h2>替换左边插槽</h2>
        </template>
    </my-cpn>
    
    <my-cpn>
        <!-- 可缩写为<template #left> -->
        <template v-slot:left>
            <h2>替换左边插槽</h2>
        </template>
        
        <!-- 可缩写为<template #mid> -->
        <template v-slot:mid>
            <h2>替换中间插槽</h2>
        </template>
        
        <h2>我是替换内容</h2>
        <p>我也是替换内容</p>
        <!-- 可通过default明确替换默认内容 -->
        <!-- 可缩写为<template #default> -->
        <template v-slot:default>
            <h2>我是替换内容</h2>
            <p>我也是替换内容</p>
        </template>
        
        <!-- 可缩写为<template #right> -->
        <template v-slot:right>
            <h2>替换右边插槽</h2>
        </template>
    </my-cpn>
</div>


<template id="myCpn">
    <div>
        <slot name="left">左边插槽</slot>
        <slot name="mid">中间插槽</slot>
        <slot name="right">右边插槽</slot>
        <!--一个不带 name 的 <slot> 出口会带有隐含的名字“default”。-->
        <slot>无名插槽</slot>
    </div>
</template>

<script>
    Vue.component('my-cpn', {
        template: '#myCpn'
    })
    
    let app = new Vue({
        el: '#app'
    })
</script>

注意: v-slot 只能添加在 <template> 上 (只有一种例外情况↓).

Vue 2.6.0更新后的作用域插槽slot

独占默认插槽的缩写语法

当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用,这样就可以直接把v-slot用在组件上。

<my-cpn v-slot="slotProps">
    <ul>
        <li v-for="item in slotProps.data">{{item}}</li>
    </ul>
</my-cpn>

<template id="myCpn">
    <span>
        <!--绑定在 <slot> 元素上的 attribute 被称为插槽 prop-->
        <slot :data="pLanguage">
        </slot>
    </span>
</template>

<script>
    Vue.component('my-cpn', {
        template: '#myCpn',
        data() {
            pLanguage: ['JS', 'HTML', 'CSS']
        }
    })
    
    let app = new Vue({
        el: '#app'
    })
</script>

注意: 默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确

<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
  {{ slotProps.user.firstName }}
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</current-user>

Vue 2.6.0 新增动态插槽名

<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>
</base-layout>