Vue实战(五) - 深入组件之依赖注入/事件总线(组件松耦合的
0. 背景
众所周知,组件之间的通信特别是父子组件之间传递数据,我们是通过props属性进行的。
其实,除了这种方式之外,我们还有别的替代方式,我们称之为依赖注入。
依赖注入使组件之间不必紧耦合在一起。
除此之外,我们还有类似事件总线(event bus)的方式来增强组件之间的通信,
这些我们稍后会一一介绍。
1. 一些关键概念
- 依赖注入(Dependency Injection)
依赖注入允许组件可以提供服务给任何它的子组件(后代组件)。
这些服务可以是值,对象或者函数。
依赖注入通过provide属性来注入服务。
- 事件总线(Event Bus)
事件总线建立在依赖注入功能的基础上。
它用于提供一种机制来发送和接收自定义事件。
事件通过$emit和$on方法来发送和接收事件。
2. 准备工作
2-1) 新建项目和基本设置
a. 新建项目
vue create productapp --default
b. 基本设置
package.json
"eslintConfig": {
...
"rules": {
"no-console": "off",
"no-unused-vars": "off"
},
...
}
c. 基本依赖
npm install bootstrap@4.0.0
3. 产品主页面
3-1) 新建产品显示页面
在src/components文件夹下,新建ProductDisplay.vue
<template>
<div>
<table class="table table-sm table-striped table-bordered">
<tr>
<th>ID</th><th>Name</th><th>Price</th><th></th>
</tr>
<tbody>
<tr v-for="p in products" v-bind:key="p.id">
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.price | currency }}</td>
<td>
<button class="btn btn-sm btn-primary" v-on:click="editProduct(p)">
Edit
</button>
</td>
</tr>
</tbody>
</table>
<div class="text-center">
<button class="btn btn-primary" v-on:click="createNew">
Create New
</button>
</div>
</div>
</template>
<script>
export default {
data: function () {
return {
products: [
{ id: 1, name: "Kayak", price: 275 },
{ id: 2, name: "Lifejacket", price: 48.95 },
{ id: 3, name: "Soccer Ball", price: 19.50 },
{ id: 4, name: "Corner Flags", price: 39.95 },
{ id: 5, name: "Stadium", price: 79500 }]
}
},
filters: {
currency(value) {
return `$${value.toFixed(2)}`;
}
},
methods: {
createNew() {
},
editProduct(product) {
}
}
}
</script>
3-2) 新建通用编辑组件
在src/components文件夹下,新建EditorField.vue
<template>
<div class="form-group">
<label>{{label}}</label>
<input v-model.number="value" class="form-control" />
</div>
</template>
<script>
export default {
props: ["label"],
data: function () {
return {
value: ""
}
}
}
</script>
代码解释:
该组件是一个通用的编辑组件,它由一个标签和一个文本框组成。
我们等下会在稍后的产品编辑组件中引用它。
3-3) 新建产品编辑组件
在src/components文件夹下,新建ProductEditor.vue
<template>
<div>
<editor-field label="ID" />
<editor-field label="Name" />
<editor-field label="Price" />
<div class="text-center">
<button class="btn btn-primary" v-on:click="save">
{{ editing ? "Save" : "Create" }}
</button>
<button class="btn btn-secondary" v-on:click="cancel">Cancel</button>
</div>
</div>
</template>
<script>
import EditorField from "./EditorField";
export default {
data: function () {
return {
editing: false,
product: {
id: 0,
name: "",
price: 0
}
}
},
components: { EditorField },
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {
id: 0,
name: "",
price: 0
};
},
save() {
// TODO - process edited or created product
console.log(`Edit Complete: ${JSON.stringify(this.product)}`);
this.startCreate();
},
cancel() {
this.product = {};
this.editing = false;
}
}
}
</script>
代码解释:
我们通过editor-field来调用之前定义的通用编辑组件。
4. 依赖注入
4-1) 定义服务
在App.vue中定义服务,如下:
<template>
<div class="container-fluid">
<div class="row">
<div class="col-8 m-3">
<product-display></product-display>
</div>
<div class="col m-3">
<product-editor></product-editor>
</div>
</div>
</div>
</template>
<script>
import ProductDisplay from "./components/ProductDisplay";
import ProductEditor from "./components/ProductEditor";
export default {
name: 'App',
components: { ProductDisplay, ProductEditor },
provide: function() {
return {
colors: {
bg: "bg-secondary",
text: "text-white"
}
}
}
}
</script>
代码解释:
通过provide属性提供一个服务,该服务暴露一个对象,提供背景和前景色。
4-2) 通过依赖注入消费服务
修改src/components文件夹下的EditorField.vue如下:
<template>
<div class="form-group">
<label>{{label}}</label>
<input v-model.number="value" class="form-control"
v-bind:class="[colors.bg, colors.text]" />
</div>
</template>
<script>
export default {
props: ["label"],
data: function () {
return {
value: ""
}
},
inject: ["colors"]
}
</script>
代码解释:
inject属性注入之前在父组件中通过provide定义的colors对象服务。
效果如下:
5. 事件总线
事件总线(Event Bus)实际上是通过依赖注入的方式传递一个Vue对象。
5-1) 添加总线
在src文件夹下的main.js添加事件总线,如下:
...
new Vue({
render: h => h(App),
provide: function () {
return {
eventBus: new Vue()
}
}
}).$mount('#app')
5-2) 通过总线发送事件
修改ProductDisplay.vue文件夹下的ProductDisplay.vue,如下:
...
<script>
export default {
...
methods: {
createNew() {
this.eventBus.$emit("create");
},
editProduct(product) {
this.eventBus.$emit("edit", product);
}
},
inject: ["eventBus"]
}
</script>
代码解释:
1. 注入之前在main.js中添加的eventBus的Vue对象来发送事件。
2. $emit方法发送事件,引号中是事件名,还可以带上额外参数。
3. 事件名在整个应用程序中必须保持唯一。
5-3) 通过总线接收事件
修改src/components文件夹下的ProductEditor.vue,如下:
...
<script>
...
export default {
...
methods: {
startEdit(product) {
this.editing = true;
this.product = {
id: product.id,
name: product.name,
price: product.price
}
},
startCreate() {
this.editing = false;
this.product = {
id: 0,
name: "",
price: 0
};
},
save() {
this.eventBus.$emit("complete", this.product);
this.startCreate();
console.log(`Edit Complete: ${JSON.stringify(this.product)}`);
},
...
},
inject: ["eventBus"],
created() {
this.eventBus.$on("create", this.startCreate);
this.eventBus.$on("edit", this.startEdit);
}
}
</script>
代码解释:
1. $on方法接收事件,第一个参数是事件名,第二个是调用的本地方法。
2. save方法中我们通过$emit方法继续向下级组件发送事件。
修改src/components文件夹下的ProductDisplay.vue,如下:
...
<script>
import Vue from "vue";
export default {
...
methods: {
...
processComplete(product) {
let index = this.products.findIndex(p => p.id == product.id);
if (index == -1) {
this.products.push(product);
} else {
Vue.set(this.products, index, product);
}
}
},
inject: ["eventBus"],
created() {
this.eventBus.$on("complete", this.processComplete);
}
}
</script>
代码解释:
a. $on方法接收完成事件,然后调用processComplete方法处理。
效果如下:
6. 本地总线
可以创建本地总线的方式只对应于某个组件的服务。
修改ProductEditor.vue,如下:
<script>
...
export default {
data: function () {
return {
...
localBus: new Vue()
}
},
...
methods: {
...
inject: ["eventBus"],
provide: function () {
return {
editingEventBus: this.localBus
}
},
created() {
this.eventBus.$on("create", this.startCreate);
this.eventBus.$on("edit", this.startEdit);
this.localBus.$on("change",
(change) => this.product[change.name] = change.value);
},
watch: {
product(newValue, oldValue) {
this.localBus.$emit("target", newValue);
}
}
}
}
</script>
修改src/components文件夹下的EditorField.vue,如下:
<script>
export default {
props: ["label", "editorFor"],
inject: {
...
editingEventBus: "editingEventBus"
},
watch: {
value(newValue) {
this.editingEventBus.$emit("change",
{ name: this.editorFor, value: this.value});
}
},
created() {
this.editingEventBus.$on("target",
(p) => this.value = p[this.editorFor]);
}
}
</script>
效果如下:
image.png