前端开发那些事儿

Vue实战(五) - 深入组件之依赖注入/事件总线(组件松耦合的

2021-05-11  本文已影响0人  ElliotG

0. 背景

众所周知,组件之间的通信特别是父子组件之间传递数据,我们是通过props属性进行的。
其实,除了这种方式之外,我们还有别的替代方式,我们称之为依赖注入。
依赖注入使组件之间不必紧耦合在一起。
除此之外,我们还有类似事件总线(event bus)的方式来增强组件之间的通信,
这些我们稍后会一一介绍。

 

1. 一些关键概念

依赖注入允许组件可以提供服务给任何它的子组件(后代组件)。
这些服务可以是值,对象或者函数。

依赖注入通过provide属性来注入服务。


事件总线建立在依赖注入功能的基础上。
它用于提供一种机制来发送和接收自定义事件。

事件通过$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对象服务。

效果如下:

image.png

 

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方法处理。

效果如下:

image.png

 

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
上一篇下一篇

猜你喜欢

热点阅读