10行代码可减轻您的Vue项目的痛苦

...或熟悉Vue JS插件作为集成事件总线的示例


关于...的几句话


大家好!我会马上预订。我真的很喜欢VueJS,我已经在VueJS上写了两年多了,并且我认为在VueJS上进行开发不会在很大程度上伤害它:)
另一方面,我们一直在努力寻找通用的解决方案,这将有助于减少机械工作时间,而将更多的时间花在真正有趣的事情上。有时,解决方案特别成功。我想与您分享其中之一。将讨论的10条线(破坏者:最后会有更多)是在Cloud Blue-Connect项目的工作过程中诞生的,该项目是具有400多个组件的相当大的应用程序。我们发现的解决方案已经集成到系统的各个点中,并且半年多来它从未需要进行过更正,因此可以安全地认为已成功对其进行了稳定性测试。

还有最后一个。在直接进行解决方案之前,我想对Vue组件之间的三种交互类型进行更多的介绍:单向流的原理,商店的模式和事件总线。如果您不需要(或无聊)此说明,请直接转到解决方案部分-一切都尽可能简短和技术化。

关于Vue组件如何相互通信的一点点


编写第一部分的人可能会提出的第一个问题是,他将如何接收工作数据,以及如何将其接收的数据“向外”传输。Vue JS框架中采用的交互原理称为...

单向数据流


简而言之,这个原理听起来像是“属性-关闭,事件-打开”。也就是说,为了从外部(“从上方”)接收数据,我们在组件内部注册了一个特殊属性,框架在其中写入了必要的信息,如有必要,我们的数据是从“外部”接收的。为了将数据“向上”传输到组件内部的正确位置,我们调用了特殊的$ generate框架方法,该方法将数据传递给父组件的处理程序。同时,在Vue JS中,我们不能只是将事件“广播”到无限深度(例如,在Angular 1.x中)。它仅“弹出”到直接父级的一个级别。事件也是如此。为了将它们转移到下一个级别,对于它们每个,您还需要注册一个特殊的接口-属性和事件,这些接口将进一步传递我们的“消息”。

这可以描述为办公楼,其中工人只能从其楼层移动到相邻楼层-上下移动。因此,为了将“签名文件”从第五层转移到第二层,将需要由三名工人组成的链条,将其从第五层转移到第二层,然后再由三名工人将其转移回第五层。

“但是这很不方便!”当然,从开发的角度来看,这并不总是很方便,但是通过查看每个组件的代码,我们可以看到它传递给谁以及传递给谁。我们不需要牢记应用程序的整个结构即可了解我们的组件是否正在“进行”该事件。我们可以从父组件中看到这一点。

尽管这种方法的优点是可以理解的,但它也具有明显的缺点,即组件的高内聚性。简而言之,为了将某些组件放置在结构中,我们需要用必要的接口将其覆盖以管理其状态。为了减少这种连接,他们经常使用“状态管理工具”。也许Vue最受欢迎的工具是...

Vuex(侧面)


Vuex Stor继续与办公大楼类似,是内部邮政服务。想象一下,在办公室的每个楼层上都有一个用于收发包裹的窗口。在第五层,他们将11号文件移交给签名,在第二层,他们定期问:“是否有任何文件签名?”,在现有文件上签名并交还。在第五行,他们还问:“有签署人吗?”同时,员工可以移动到其他楼层或其他房间-邮件工作时工作原理不会改变。

大约根据此原理,称为存储的模式也可以使用。使用Vuex接口,可以注册和配置全局数据仓库,并且组件可以订阅它。而且,无论在什么层次上进行上诉,商店都将始终提供正确的信息。

看来,所有这些问题都已经得到解决。但是在我们的隐喻大楼的某个时刻,一位员工想打电话给另一位员工吃午餐……或报告某种错误。在这里,奇怪的事情开始了。消息本身不需要发送。但是,为了使用邮件,您需要传输一些东西。然后,我们的员工提出了一个代码。一个绿色的球-去吃午餐,两个红色的立方体-发生应用程序错误E-981273,三个黄色的硬币-检查您的邮件等等。

很容易猜到,在这个笨拙的隐喻的帮助下,我描述了当我们需要确保组件对另一个组件中发生的事件的响应时的情况,该事件本身并不以任何方式与数据流连接。新项目的保存完成-您需要重新获取收藏集。发生403未经授权的错误-您需要启动用户注销等。在这种情况下,通常的做法(与最佳做法相去甚远)是在存储内创建标志或间接解释存储的数据及其更改。这很快导致商店本身及其周围组件逻辑的污染。

在此阶段,我们开始考虑如何绕过整个组件链直接传递事件。而且,一些Google或在文档中翻阅,我们遇到了一种模式...

活动巴士


从技术角度来看,事件总线是一个对象,它允许使用一种特殊的方法来启动“事件”并使用另一种方法对其进行预订。换句话说,订阅eventA事件时,此对象将传入的处理函数存储在其结构内部,当在应用程序中的某个位置调用带有eventA键的launch方法时,它将调用该函数。对其进行签名或运行就足以通过导入或引用来访问它,您已完成。

隐喻地,在我们的“建筑物”中,公共汽车是信使中的普通聊天。组件订阅其他组件向其发送消息的“常规聊天”。一旦组件已订阅的“聊天”上出现“消息”,处理程序就会启动。

创建事件总线有很多不同的方法。您可以自己编写它,也可以使用现成的解决方案-相同的RxJS,它提供了用于处理整个事件流的强大功能。但是最常见的是,在使用VueJS时,他们使用VueJS本身。通过构造函数(新的Vue())创建的Vue实例提供了一个精美简洁的事件接口,如官方文档所述。

在这里,我们接近下一个问题...

我们想要什么?


我们想在我们的应用程序中构建事件总线。但是我们还有两个附加要求:

  1. 在每个组件中都应该易于访问它。对数十个组件中的每个组件分别进行导入对我们来说似乎是多余的。
  2. 它必须是模块化的。我们不想记住所有事件名称,以避免“项目创建”事件在整个应用程序中触发处理程序时的情况。因此,我们希望能够轻松地将组件树的一小部分分成一个单独的模块,并在其内部而不是外部广播其事件。

为了实现如此令人印象深刻的功能,我们使用了VueJS提供给我们的功能强大的插件接口。你可以用它进行更详细了解这里与官方文档页面上。

让我们先注册我们的插件。为此,在Vue应用程序的初始化点之前(调用Vue。$ Mount()之前),我们放置以下块:

Vue.use({   
  install(vue) { }, 
});

实际上,Vue插件是在整个应用程序级别扩展框架功能的一种方法。插件接口提供了几种集成到组件中的方法,但是今天我们将介绍mixin接口。此方法接受一个对象,该对象在开始应用程序的生命周期之前扩展每个组件的描述符。(我们编写的组件代码很可能不是组件本身,而是对其行为的描述以及框架在其生命周期各个阶段使用的逻辑的某些部分的封装。插件初始化在组件生命周期之外,因此要在其之前进行,因此我们我们说的是“描述符”,而不是组件,以强调确切地说,写入文件中的代码(而不是框架工作成果的某些实体)将转移到插件的mixin部分)

Vue.use({
  install(vue) {     
    vue.mixin({}); // <--
  }, 
});

正是这个空对象将包含我们组件的扩展。但是对于初学者来说,又是一站。在我们的例子中,我们想创建一个接口,用于在每个组件级别访问总线。让我们在描述符中添加“ $ broadcast”字段,它将存储到我们总线的链接。为此,请使用Vue.prototype:

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; // <--
    vue.mixin({}); 
  }, 
});

现在我们需要创建总线本身,但首先让我们回想一下模块化的要求,并假设在组件描述符中,我们将使用带有文本值的“ $ module”字段声明一个新模块(稍后将需要它)。如果在组件本身中指定了$模块字段,我们将为其创建一个新的总线;否则,我们将通过$父字段将链接传递给父节点。请注意,描述符字段将通过$ options字段提供给我们。

我们将尽早在总线上创建总线-在beforeCreate挂钩中。

Vue.use({
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() {  // <--
        if (this.$options.$module) {  // <--
         
 	} else if (this.$parent && this.$parent.$broadcast) {  // <--
         
        } 
      }, 
    }); 
  }, 
});

最后,让我们填写逻辑分支。如果描述符包含一个新的模块声明,请创建一个新的总线实例,否则,请从$ parent获得链接。

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null; 
    vue.mixin({
      beforeCreate() { 
        if (this.$options.$module) {
          this.$broadcast = new Vue();  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast;  // <--
        } 
      }, 
    }); 
  }, 
});

正如我所承诺的那样,我们考虑了... 1,2,3,4 ... 10行,因此放弃了该插件的公告!

我们可以做得更好吗?


我们当然可以。此代码很容易扩展。例如,在我们的示例中,除了$ broadcast之外,我们还决定添加$ rootBroadcast接口,该接口可以访问整个应用程序的一条总线。用户在$广播总线上运行的事件在$ rootBroadcast总线上重复,因此您可以预订特定模块的所有事件(在这种情况下,事件名称将作为第一个参数传递给处理程序),或者通常所有应用程序事件(然后模块名称将与第一个参数一起传递给处理程序,事件名称与第二个参数一起传递,事件传递的数据将与以下参数一起传递。这种设计将使我们能够在模块之间建立交互,并将单个处理程序挂在不同模块的事件上。

// This one emits event  
this.$broadcast.$emit(‘my-event’, ‘PARAM_A’); 
// This is standard subscription inside module 
this.$broadcast.$on(‘my-event’, (paramA) => {…}); 
// This subscription will work for the same event 
this.$rootBroadcast.$on(‘my-event’, (module, paramA) => {…}); 
// This subscription will also work for the same event 
this.$rootBroadcast.$on(‘*’, (event, module, paramA) => {…});

让我们看一下如何实现这一点:

首先,创建一条总线,该总线将通过$ rootBroadcast进行组织,并使用链接将其本身包含在字段中:

const $rootBus = new Vue(); // <--

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus; // <--
        if (this.$options.$module) {
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

现在我们需要每个组件中的模块成员身份,因此让我们扩展模块化的定义,如下所示:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;  // <--
          this.$broadcast = new Vue(); 
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;  // <--
          this.$broadcast = this.$parent.$broadcast; 
        } 
      }, 
    }); 
  }, 
});

接下来,我们需要使事件在模块化本地总线上以我们需要的方式反映到根。为此,我们首先必须创建一个简单的代理接口,并将总线本身放置在$ bus的有条件私有属性中:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  // <--
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };  // <--
        } 
      }, 
    }); 
  }, 
});

最后,向对象添加代理方法-因为现在$ broadcast字段不提供对总线的直接访问:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 
        // >>>
        this.$broadcast.$emit = (…attrs) => {
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };
        // <<<
      }, 
    }); 
  }, 
});

好吧,作为最后一点,让我们记住,通过关闭可以访问总线,这意味着一次添加的处理程序将不会被组件清除,而是在使用应用程序的整个过程中都存在。这可能会导致令人不快的副作用,因此让我们在组件生命周期结束时向总线添加侦听器清除功能:

const $rootBus = new Vue();

Vue.use({   
  install(vue) { 
    vue.prototype.$broadcast = null;
    vue.mixin({
      beforeDestroy() {                               // <--
        this.$broadcast.$off(this.$broadcastEvents);  // <--
      },

      beforeCreate() { 
        vue.prototype.$rootBroadcast = $rootBus;
        this.$broadcastEvents = [];  // <--
        if (this.$options.$module) {
          this.$module = this.$options.$module;
          this.$broadcast = { $bus: new Vue() };  
        } else if (this.$parent && this.$parent.$broadcast) { 
          this.$module = this.$parent.$module;
          this.$broadcast = { $bus: this.$parent.$broadcast.$bus };
        } 

        this.$broadcast.$emit = (…attrs) => {
          this.$broadcastEvents.push(attrs[0]);   // <--
          this.$broadcast.$bus.$emit(…attrs);           
          const [event, …attributes] = attrs; 
          this.$rootBroadcast.$emit(event, this.$module, …attributes)); 
          this.$rootBroadcast.$emit(‘*’, event, this.$module, …attributes)
        };
        
        this.$broadcast.$on = (…attrs) => {           
          this.$broadcast.$bus.$on(…attrs);
        };

        this.$broadcast.$off =: (...attrs) => {  // <--
          this.$broadcast.$bus.$off(...attrs);   // <--
        };
      }, 
    }); 
  }, 
});

因此,尽管不太简洁,但此选项提供了更有趣的功能。使用它,您可以实现组件之间替代通信的完整系统。而且,他完全在我们的控制之下,不会将外部依赖项引入我们的项目中。

我希望阅读后获得或刷新对Vue插件的了解,也许下次您需要向应用程序中添加一些通用功能时,您可以更有效地实现它-而不添加外部依赖项。

All Articles