[Vue.js启航]——多组件应用构建

本文介绍如何使用Vue.js构建多组件应用,重点讲解组件之间的通信方法,包括自定义事件的监听与触发,以及非父子组件间的数据传递技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue.js启航——英雄编辑器(三 )

简介

 上一篇文章写到了主从结构的英雄编辑器,其中大概用到了两个组件,英雄列表组件和英雄详情组件,但我们并没有把他们分开来,这一篇文章我们就来将他们解构,把一整个组件分成英雄列表组件和英雄详情组件。

多组件的英雄编辑器

 组件复用是软件工程第一种重要的思想,通过组件设计使得组件具有良好的复用性,可以减少重复性代码,降低测试难度。多组件的英雄编辑器和主从结构的英雄编辑器的实现效果是一样的,具体实现效果如下

主从结构的英雄编辑器

英雄列表组件

 首先我们来看一下英雄列表组件,跟主从结构中的英雄列表组件大同小异

  //英雄列表组件
Vue.component("hero-list",{
    props:["title","heroes"],
    template:'\
    <div>\
        <h1>{{title}}</h1>\
        <h2>My Heroes</h2>\
        <ul class="heroes">\
            <li v-for="hero in heroes"\
                v-on:click="onSelect(hero)"\
                v-bind:class="{selected:hero===selectedHero}">\
                <span class="badge">{{hero.id}}</span>{{hero.name}}\
            </li>\
        </ul>\
    </div> \
    ',
    methods:{
        onSelect:function(hero){
            this.selectedHero=hero
            bus.$emit("selectHero",this.selectedHero);
        }
    },
    data:function(){
        return {selectedHero:""};
    }
})

不同点就在于 onSelect 方法中多了 bus.$emit()bus 是我们用于非父子组件中通信所定义的实例,而 $emit 是Vue中用于触发事件的函数,关于非父子组件的通信会在下面讲到。

英雄列表组件与英雄详情组件的通信

 非父子组件的通信在官方指南里有提到

有时候两个组件也需要通信(非父子关系)。在简单的场景下,可以使用一个空的 Vue 实例作为中央事件总线。在复杂的情况下,我们应该考虑使用专门的 状态管理模式.

所谓使用空的Vue实例作为中央事件总线就是我们现在使用的 bus 实例,在 bus 实例中定义监听事件 selectHero ,然后在英雄列表组件中触发该事件。将需要传递的数据存放在 bus 实例中,然后再在英雄详情组件中引用该数据。在这里我的实现方法是在具体实例中使用 computed 选项来即时改变数据,然后使用 v-bind:hero="hero" 将数据绑定到英雄详情组件中。具体流程如下

数据流动图

这里我们使用到 computed 选项,computed 选项能够在相关一来发生改变时重新求值。这是什么意思呢,例如在这个例子中,具体实例中的 computed 选项如下

computed:{
    hero:function(){
        return bus.hero
    }
}

在这个时候,当 bus.hero 这个依赖值发生改变时,会促使具体实例中 hero 数据值的变化。

接下来再简单介绍一下Vue中自定义事件的应用。官方指南中对于Vue事件的解释

每个 Vue 实例都实现了事件接口(Events interface),即:

  • 使用 $on(eventName) 监听事件
  • 使用 $emit(eventName) 触发事件

在这个例子中我们在 bus 定义 selectHero 事件的代码如下

created:function(){
    this.$on("selectHero",this.selectHero)
}

created 选项是Vue定义的一个生命周期钩子,这段代码具体的意思是在实例被创建之后就监听 selectedHero 事件,事件具体的实现方法是 this.selectHero , this.selectHero,是 bus 实例中定义的方法,具体代码如下

methods:{
    selectHero:function(hero){
        this.hero=hero;
    }
}

在触发了该事件之后会将 bus 实例中的 hero 数据改变。

再来看看触发事件的代码,触发事件实在英雄列表实例中,

onSelect:function(hero){
    ...
    bus.$emit("selectHero",this.selectedHero);
}

bus.$emit("selectHero",this.selectedHero),这段代码是指触发 bus 实例中的 selectHero 事件,并传入参数 this.selectedHero

经过事件监听和事件触发就能成功把 hero 实例中的 hero 数据改变为英雄列表中的 selectedHero 数据。然后再经过具体实例中的computed选项将数据传入到具体实例中,再接着使用 v-bind 指令就能够将数据传入到英雄详情组件中了。是不是感觉很复杂。以后我们会使用更加简洁,更加统一的方法实现非父子组件中的数据通信,那就是状态管理.

英雄详情组件

 如果说英雄列表组件尚且有不同,那么英雄详情组件可以说是完全相同了。

//英雄详情组件
Vue.component("hero-detail",{
   props:["hero"],
   template:'\
   <div v-if="hero">\
       <h2>{{hero.name}}</h2>\
       <div><label>id:</label>{{hero.id}}</div>\
       <div>\
           <label>name:</label>\
           <input v-model="hero.name" placeholder="name"/>\
       </div>\
   </div>\
   ',
})

由于非父子组件通信的原因,hero 从外部传入,然后流程跟主从结构中的一模一样。因而没什么好说的。

总结

 这篇文章写的是多组件的应用,而使用多组件的核心就是组件间的通信问题。这篇文章中我是使用了一种很笨拙的方法实现组件间的通信。也来聊一聊我在实现这个例子时遇到的坑,对于非父子组件的通信官方指南只是一笔带过,并没有具体的例子。最后的实现只是靠我自己的不断摸索。一开始英雄详情中的 hero 数据我是这样写的

data:function(){
    return {hero:bus.hero}
}

然后发现我点击英雄列表时英雄详情并没有显示出来,但 bus.hero 已经是变了,而具体实例中的 hero 并没有跟着更新

数据不更新

其实是因为 data 选项的数据并不会根据依赖的改变而改变数据,要实现数据根据依赖来改变,可以通过 methodscomputedwatched 选项来设定,具体可以参考计算属性。这样就能实现数据根据依赖的改变而变动。但 computed 选项是实例的选项并不是组件的选项,所以只能在具体实例中定义 computed 选项。

这篇文章所用到的知识点:
- $on , $emit 实现自定义事件的监听和触发
- 非父子组件间通信的简单方法
- 实例中 computed 选项的使用
- 多组件构建思想

可运行代码

<!--hero.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="hero.css">
</head>
<body><!--
    <div id="host-guest-app">
        <host-guest v-bind:title="title"
            v-bind:heroes="heroes"
            ></host-guest>
    </div>
    -->

    <div id="hero">
        <hero-list v-bind:heroes="heroes" 
            v-bind:title="title"></hero-list>
        <hero-detail v-bind:hero="hero"></hero-detail>
    </div>



</body>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="hero.js"></script>
</html>
//hero.js

//十位英雄数据
const HEROES=[
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];

//多个组件

//事件总线
var bus=new Vue({
    data:{
        hero:""
    },
    created:function(){
        this.$on("selectHero",this.selectHero)
    },
    beforeDestroy:function(){
        this.$off("selectHero",this.selectHero)
    },
    methods:{
        selectHero:function(hero){
            this.hero=hero;
        }
    }
});

//英雄详情组件
Vue.component("hero-detail",{
    props:["hero"],
    template:'\
    <div v-if="hero">\
        <h2>{{hero.name}}</h2>\
        <div><label>id:</label>{{hero.id}}</div>\
        <div>\
            <label>name:</label>\
            <input v-model="hero.name" placeholder="name"/>\
        </div>\
    </div>\
    ',
})

//英雄列表组件
Vue.component("hero-list",{
    props:["title","heroes"],
    template:'\
    <div>\
        <h1>{{title}}</h1>\
        <h2>My Heroes</h2>\
        <ul class="heroes">\
            <li v-for="hero in heroes"\
                v-on:click="onSelect(hero)"\
                v-bind:class="{selected:hero===selectedHero}">\
                <span class="badge">{{hero.id}}</span>{{hero.name}}\
            </li>\
        </ul>\
    </div> \
    ',
    methods:{
        onSelect:function(hero){
            this.selectedHero=hero
            bus.$emit("selectHero",this.selectedHero);
        }
    },
    data:function(){
        return {selectedHero:""};
    }
})



var hero=new Vue({
    el:"#hero",
    data:{
        heroes:HEROES,
        title:"Tour of Heroes",
    },
    computed:{
        hero:function(){
            return bus.hero
        }
    },
})
//hero.css

h1 {
 color: #369;
 font-family: Arial, Helvetica, sans-serif;
 font-size: 250%;
}
h2, h3 {
 color: #444;
 font-family: Arial, Helvetica, sans-serif;
 font-weight: lighter;
}
.selected {
    background-color: #CFD8DC !important;
    color: white;
}
body {
    margin: 2em;
}
body, input[text] {
    color: #888;
    font-family: Cambria, Georgia;
}
.heroes {
    margin: 0 0 2em 0;
    list-style-type: none;
    padding: 0;
    width: 15em;
}
.heroes li {
    cursor: pointer;
    position: relative;
    left: 0;
    background-color: #EEE;
    margin: .5em;
    padding: .3em 0;
    height: 1.6em;
    border-radius: 4px;
}
.heroes li.selected:hover {
    background-color: #BBD8DC !important;
    color: white;
}
.heroes li:hover {
    color: #607D8B;
    background-color: #DDD;
    left: .1em;
}
.heroes .text {
    position: relative;
    top: -3px;
}
.heroes .badge {
    display: inline-block;
    font-size: small;
    color: white;
    padding: 0.8em 0.7em 0 0.7em;
    background-color: #607D8B;
    line-height: 1em;
    position: relative;
    left: -1px;
    top: -4px;
    height: 1.8em;
    margin-right: .8em;
    border-radius: 4px 0 0 4px;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

若即

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值