美文网首页前端开发那些事儿
Vue实战(五) - 深入组件之依赖注入/事件总线(组件松耦合的

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

作者: ElliotG | 来源:发表于2021-05-11 20:22 被阅读0次

    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对象服务。
    

    效果如下:

    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

    相关文章

      网友评论

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

        本文链接:https://www.haomeiwen.com/subject/kzundltx.html