使用Backbone.Radio构建更具模块化的Web应用

Backbone Radio是什么,为什么要学习和使用它?这个问题,我们先看看官方的介绍

Backbone.Radio为开发Backbone应用提供了额外的通信模式(messaging patterns);

Backbone 自带有一个事件通信系统(event system)——实现广播订阅模式的Backbone.Events。Backbone.Radio 则增加两项额外的功能:

  • 第一个是Requests,一种request-reply的通信模式;
  • 第二个是Channels,一种划分通信逻辑边界的机制,可将程序的通信划分为不同的逻辑空间(namespaces)。

我们再看看 Marionette 的解释

In short, Radio is a global, namespaced, message bus system designed to allow two otherwise unrelated objects to communicate and share information.

简单的说,Radio是一个全局的、可划分逻辑通信空间的通信总线模块(message bus system ),目的是为了让程序中两个不太相关的对象进行通信和共享资源。

以上对Backbone Radio的解释,对于实际开发过Backbone/Marionette应用的人来说,是比较明显的,但是对于只了解Backbone,或者初学者来说,则比较难理解。什么是通信模式?总线是什么?通信空间?两个不相关的对象?

按照「方案、任务、技术和理论」的学习模式,以上官方解释只是Backbone Radio作为一种工具的技术一面,我们需要更多的在任务、和理论原律方面的解释,才能丰富的认识这个工具,掌握这个工具。下面略译《Building Modular Web Apps With Backbone.Radio》一文,补充这方面的信息。

Building Modular Web Apps With Backbone.Radio

模块化单V(MVC),与模块化Page

1. 将Pape(UI)分割多个逻辑独立的模块(view)来开发

Backbone 作为一支MVC框架,能引导我们使用模块化方式构建应用,例如复杂的MVC组件。我们可说一支App由多张page组成,而 Pages 是由不同的view组成的。将Page分割为多个逻辑的view,可简化开发和维护。

2. 没有适当的通信方式,UI代码将变得臃肿(复杂的依赖关系)

然而,在实践中,面对复杂的UI,我们依然写出很乱的代码。主要原因不是程序员智力问题,而是存在复杂的依赖关系,需要一种工具,来进一步整理这些依赖。

3. 前面几篇讲了V,现在讲Page——多个V的通信

前面的系列(如下列表)我们已经介绍了如何创建一个单独的View,创建具有很好可维护性的 V组件。现在我们讲如何使用Backbone.Radio,沟通多个不同的view,构建复杂的UI。当然Backbone.Radio也可用作其它的程序对象的“沟通”。

复杂动态的UI需求,与message bus的提出

我们已经了解到模块化UI的一些好处:

  1. 粒度适中,更易的开发维护和测试;
  2. 更容易创建复合动态UI效果,因为更容易被动态替换;
  3. 代码复用

然而以上只是针对「单个MVC实例中的V注1而言的,当在Page中,不同的view有依赖关系时,又有新的问题。例如,Page 的一部分在功能上依赖了另一部分,例如一个按键打开一个对话框,而「按键所在V」与「对话框所在的V」不是同一个V,怎么调用?

事实上,数据事务集中的应用(GUI交互)的界面功能是很多样的,这种多样性和功能动态性表现在各种计算“关系”,而组件间有更多的依赖关系,组件之间的通信问题就变得更明显了。

面对这个问题,你可以有多种方案,例如直接引用相关的View,或者使用事件机制(Backbone.Event)创建一个共享的数据对象进行关联。不过,我们有人已经借鉴了后端开发中的最佳实践,创建一种类似通信总线的 message bus的是中间模块,统一管理通信逻辑。

通用的通信总线的理论和概念

理论上,通信总线(message bus)是一种「为两个系统提供互讯功能」的独立系统或工具。通讯总线的思想(原律)是说,两个系统可以通过一个中间的对象(mediator object)来通信,而不必直接引用/结合在一起。

在Backbone中创建一种简单的message bus

Backbone.Events 本身是提供两个对象通信的功能,性质像是一个私有通信关系,只需改造一下,变通用的公共通信总线。

Backbone.Events 的使用是mix into 通信双方的,我们可以改mix into一个独立的对象:

var mediator = _.extend({},Backbone.Events);

然后 Models and Views就是可以通过这个中间对象(mediator)进行通信。

这是一个很棒的设计模式,非常有价值,然而有一些局限性。例如,在中间对象里,所有的事件是全局公开的,任何对象都可以访问(监听),故你还得避免名字冲突(naming collisions),例如两个对象发布了相同名字的事件。

一种通信总线的实现 Backbone.Radio

Backbone.Radio 是通信总线理念的一种具体的实现,与「理念上」的「单一中间通信对象」相对,Backbone.Radio 「实现上」提供基本「通讯总线」的同时,实现了两点:

  • 第一,提出了逻辑信道(channels)概念,解决了事件名冲突;你可为程序的不同page或功能模块创建不同的独立的channels;
  • 第二,提供三种通信模式(API):Events、Commands和 Requests(后来将Commands抽象为Requests的一种特例),满足不同的通信需求;

Backbone.Radio API

Events

Radio.Events的实现上可以简单的理解为上面介绍的,将Backbone.Event mix into一个独立的对象,是一种event aggregator。

在使用上,Radio Events 与 Backbone.Event极类似,只是你的「事件触发和监听」都要通过一个中间Radio.channel, 代码例子看这里

Backbone Radio  Events

Functions Provided - on, off, trigger, once, listenTo, listenToOnce, stopListening

Commands

Radio Commands的功能 和 Radio.Events 是类似的,只是在Radio.Events的技术之上提供了“命令语义”,也限定的通信的模式——只有一个客户对象响应“命令”事件(comply)。Radio Commands算是一种特殊的 Events 通信,对某种些开发场景特别有价值(带有语义,提高开发和维护性)。

Backbone Radio  Commands

由于某些原因,Radio Commands已经被抽象为Radio Requests的一种特例,Commands只是一种没有「返回值」的Requests,更新旧代码只需将Commands改为Requests,comply改为reply。

代码例子在这里

Functions Provided - comply, stopComplying, command, complyOnce

Requests

第三种模式是「请求模式」,Radio Requests。Radio Requests 的性质或功能 和 Radio.Events 有一定的区别了。Requests 和 Commands都是一对一通信,但是 Requests 会要求响应对象返回东西(Events中响应对象不返回任何东西)。Requests 通信模式对于实现「一种资源的松散式提供」非常有价值,例如,View对象可能依赖其它的View或数据对象的资源,它可以通过Requests来使用这些资源,而不必知道这些资源实现细节。

Backbone Radio  Requests

Functions Provided - reply, stopReplying, request, replyOnce

Channels

三种通信模式(技术实现上只有两种)必须有一个通信载体,一条message bus,这个载体就是Radio Channels 。前面已经介绍了,我们可以为程序的不同逻辑部分创建独立的Channels,这有点真的通信信道一样,为通信划出逻辑频段。这相对于只使用一个全局message bus,大大降低了通信冲突的可能性,也提高了代码的可读性,和可维护性。

在技术上,Channel只是一“混入”了Backbone.Events 和 Radio.Requests 的独立的对象,一种支持两种通信模式的通信总线。

A Channel is simply an object that has Backbone.Events and Radio.Requests mixed into it: it's a standalone message bus comprised of both systems.

https://github.com/marionettejs/backbone.radio#channels

Backbone.Radio 的应用实例

Radio的 API相当“简单”的,它的强大在于为我们解耦「应用程序的功能组件」注2提供了一种设计/开发模式,或是设计语言,提高了开发中可维护和开发效率。下面就我的经验,介绍下它在实际开发中(主要是协助开发模块化的UI)的力量。

1. 用Events解耦两种不同的View

面向对象开发中,解耦的需求和例子很多;例如在Backbone或其它GUI开发中,不同Views之间必须解耦,也就是说Page上是两个不太相关的Views尽可能的独立,不要了解对方。在我的经验当中,嵌套的父views可以直接引用它的子views,但是并列的两个views,我一般用Radio解耦它们。

Radio Events是一种广播订阅的耦合模式,非常适用两个相对独立的对象进行协作,例如两个相关的views。想像下一种经典的UI界面:主内容区( main content)边上有一侧边栏(sidebar)用来显示内容区的上下文信息(具体的例子像Gmail这样的邮件程序的界面,在sidebar 上会显示当前邮件的联系人contact 信息)。sidebar和main content是相关的,但也是功能上相对独立的views。我们可以使用Radio Events来保持sidebar和main content的数据同步,代码类似于:

var Mn = require('backbone.marionette');
var Radio = require('backbone.radio');

var inboxChannel = Radio.channel('inbox');

var ContactView = Mn.ItemView.extend({

    template: '#contact-template',
    
    initialize: function() {
        this.listenTo(inboxChannel, 'show:email', this.showContact);
        this.listenTo(inboxChannel, 'show:inbox', this.showAd);
    },
    
    showContact: function(emailObject) {
        //show the contact for the emailObject
    },
    
    showAd: function() {
        //when we don't have a contact to show, show an ad instead
    }

});

module.exports = ContactView;

以上 sidebar View 订阅了 show:email 事件,这个事件可以由任何对象触发,它可以是inbox view ,navigation View等,sidebar是不必关心的。

值得注意的,理论上原理是,事件是外在于对象的(show:email 外在于sidebar view),代表着程序状态的变更,订阅对象只管 reacting这个事件(响应这个事件),不管是谁触发了这个事件(产生这个程序状态的变更)。另外,订阅事件的对象可以是多个。

EM:异步事件原初就是针对大程序“异步计算”而设计,像GUI这种「大型多元数据应用程序」,这种「计算模式」——对象间的异步计算响应,是常见的,例如网络计算、用户交互计算等。

2. 用Commands制作一个“高层的”通用功能函数

Events 是针对异步计算而设计,而大规模应用程序中,还有其它的通信模式需求。Events模式中,事件触发是被动的,并且通信是无执行语义,它不适用于一个模块对象「想主动的」与另一个相关对象的通信的场景,这种模式就是 Commands/Requests 。

Commands模式的例子,我曾用来构建过一个独立的系统日志的模块(centralized event-logging module)。这个模块对象的功能是「收集系统执行过程的程序事件数据」,并保存在后端(backend API )或者发给 Google Analytics进行统计。如下是我使用commands实现的模块大概的样子:

var Radio = require('backbone.radio');

function log(event) {
    // Send event to the backend API and Google Analytics
}

Radio.channel('appEvents').comply('log',log);

有了这个日志模块,那我们可以在程序任务地方执行 Radio.channel('appEvents').command('log', event)来记录一次程序事件数据。

其实这个功能也可以用 events 模式来实现,但显然没有 commands 模式的直观,和语义适当。

3. 用 Requests 制作一个带返回数据功能的“高层的”通用函数

Commands模式里,发起“通信调用”的「主动组件」只是命令通知「被调组件」完成一个项计算工作,不用数据返回,如果主动组件希望返回数据呢?我们需要用Request模式。一个应用实例是,在内层嵌套的View之间传递数据。

在Marionette 或 ReactJS上开发深层嵌套View的一项挑战,就是在这些嵌套的V组件之间传数据。一种解决方案就是先将数据传给顶层的View,然后沿嵌套层次往下传,直到需要数据的那层View。这种方法优点就是处理流行清晰,缺点就是污染了目标View的所有父View的形式(传了与它不直接相关的数据),并且将目标子View与它的父View耦合在一起。

另一种方法就是将数据放在全局,这是公认有问题的做法。

Radio Requests 为这个问题提供一种优雅的解决。你可以「单独创建一个类对象」作为一种“data providers”,为View想要的数据;数据源可以是另一个UI组件,也可以是models 或 collections,定义它为Requests的「被调用组件」,然后内嵌的子View直接Requests,调用这个数据源,不用经过它的父View。

我做过这样的一个实例,我的应用界面上有一些「任务的View」(task views),这些task views可被分配关联一些users。而因为这些task views嵌入在UI内层,为它添加关联users时,我不是通过它的父View来传递。我先用一个application顶层的对象来保存user数据,再使用一个user-picker View来request这些users。为确保打开user-picker View前,users数据已经准备好,我使用了jQuery deferreds。代码像如下的样子:

//in app.js
var usersDeferred = (new UserCollection()).fetch();
resourceChannel.reply('userlist',function() {
    return usersDeferred;
});

//in task.js
var usersDeferred = resourceChannel.request('userlist');
usersDeferred.done(this.showUserPicker);

Backbone.Radio是Marionette的应用之一

同样值得指出的是,Radio并不依赖于Marionette,它可以用于任何 Backbone 或 Web项目。例如,使用Radio在Backbone和React的应用程序中实现 FLUX风格的分派器(dispatcher),可能是一个有趣有意思的事。Radio是通用的库工具,能为任何类型应用解耦 组件。

小结

Radio提供了两类三种中间总线的通信模式,它们都是为了“大程序”解耦的,只是它满足了三种不同的“高层”构件耦合关系:

  • Radio Events是满足一种广播订阅的一对多的耦合关系;与Backbone Events不同的是,它使用了中间Channel,适用于不太相关的对象进行解耦;
  • Radio Commands/Requests 则都是一对一的耦合,只是Commands无返回,Requests有;它们有点像是「普通的函数调用」,只是要通过一个是中间的总线对象来“调用”,降低两个对象的耦合程度。

参考


  1. UI Page MVC V等概念有微妙的关系。MVC结构是理论性的,而实践上,第一,当我们说构建V组件时,是默认V背后的M(虽V可以不依赖M,但很少);第二,当我们说UI,可以是指Page,也可以指一个V;第三,以上,当然我们说构建UI,可以说指构建一个Page,Page 由多个V组成,而这个V是指完整的MV(C)。
  2. MVC模式提供是一个纵向分解单个交互功能耦合的方法,公共总线则是横向的解耦,例如两个相关但不同功能的View。
裸男
Nakeman.cn 2023 Build by Gatsby and Tailwind, Deploy on Netlify.