详解JavaScript中的客户端消息框架设计原理

哇——是个危险的题目,对吗?我们对于什么是本质的理解当然会随着我们对要解决问题的理解而变化。因此我不会说谎——一年前我所理解的本质很不幸并不完整,因为我确信我将要写的已经快伴随我有6个月之久。所以,这篇文章是我在发现JavaScript中成功的运用客户端消息模式的一些关键要点时的一个掠影。

1.) 理解中介者与观察者的区别 
 大多数人在描述任何事件/消息机制的时候喜欢套用“发布者/订阅者”(pub/sub)——但我认为这个术语不能很好的与抽象建立联系。当然,从根本上说,一些东西订阅了另一些东西发布的事件。但是发布者与订阅者在何等层次上封装在一起有可能使一个好的模式变得暗淡无光。那么,区别在什么地方呢?

观察者

观察者模式包括了被一个或多个观察者所观察的某个对象。典型的,该对象记录下所有观察者的痕迹,通常是用一个list来存储观察者注册的回调方法,这些是观察者为了接收通知而订阅的。 注意: (哦,双关语,我有多爱他们啊)(译者注:Observe 观察、注意)

var observer = {
 listen : function() {
  console.log("Yay for more cliché examples...");
 }
};
var elem = document.getElementById("cliche");
elem.addEventListener("click", observer.listen);

一些需要注意的事情是:

  • 我们必须获得对此对象的直接引用
  • 此对象必须保持一些内部的状态,保存观察者的回调痕迹
  • 有时侦听者不会利用由此对象返回的任何参数,理论上来说,有可能有 0-n*个参数 (更多是取决于以后会变得多有趣)

* n事实上不是无限的,但为了讨论的目的,它指我们永远也达不到的极限

中介者

中介者模式在一个对象与一个观察者之间引入了一个“第三方”——有效的将二者解耦而且将他们之间如何通信封装起来。一个中介者的API可能像“发布”、“订阅”、“取消订阅”一样简单,或者某个领域范围内的实现可能被提供用来隐藏这些方法于某些更有意义的语义之中。大多数我用过的服务器端的实现更倾向于领域范围而不是更简单,但是并没有对一个通用的中介者有任何规则限制!并不罕见,有种想法认为一个通用的中介者是一种信息经纪人。无论何种情形,结果都一样——特定对象与观察者之间不再互相直接知晓:

// It's fun to be naive!
var mediator = {
 _subs: {},
 // a real subscribe would at least check to make sure the
 // same callback instance wasn't registered 2x.
 // Sheesh, where did they find this guy?!
 subscribe: function(topic, callback) {
  this._subs[topic] = this._subs[topic] || [];
  this._subs[topic].push(callback);
 },
 // lolwut? No ability to pass function context? :-)
 publish : function(topic, data) {
  var subs = this._subs[topic] || [];
  subs.forEach(function(cb) {
   cb(data);
  });
 }
}
var FatherTime = function(med) { this.mediator = med; };
FatherTime.prototype.wakeyWakey = function() {
 this.mediator.publish("alarm.clock", {
  time: "06:00 AM",
  canSnooze: "heck-no-get-up-lazy-bum"
 });
}
var Developer = function(mediator) {
 this.mediator = mediator;
 this.mediator.subscribe("alarm.clock", this.pleaseGodNo);
};
Developer.prototype.pleaseGodNo = function(data) {
 alert("ZOMG, it's " + data.time + ". Please just make it stop.");
}
var fatherTime = new FatherTime(mediator);
var developer = new Developer(mediator);
fatherTime.wakeyWakey();

你可能会想,除了特别纯粹的中介者实现,特定对象不再负有保存订阅者列表的责任,而且“时光老人”(FatherTime)与“开发者”(Developer)实例永远没法真正互相知道。他们只是共享了一个信息——将如我们今后所见,这是一个很重要的合约。 “很好,Jim。这对我而言仍然是发布者/订阅者,那么重点呢?我选择某个方向真的会有区别吗?”哦,继续吧,亲爱的读者们,继续吧。

2.) 了解什么时候使用中介者和观察者

使用本地的观察者和中介者,即写在组件当中的,而中介者看起来又像远程的组件间通信。不管怎样。我对待这种情况的原则虽然是——tl;dr(too long; don't read)(太长,不读了)。但无论如何,反正串联在一起最好。

要我简捷地说真是麻烦,就像把几个月来的细致体验压缩到装不下140个字的沟里。现实中回答这个问题肯定不简洁。所以有一个长版本的解释:

观察者除了关心数据映射之外还有必要引用别的项目吗?例如Backbone.View视图有各种理由直接引用它的模型。这是非常自然的关系,视图不仅要在模型改变时进行渲染,还需要调用模型的事件处理。如果段首的问题答案是”yes“,那观察者就是有意义的。
    如果观察者和观察对象的关系仅仅是依赖数据,那我愿意使用中介pub/sub方式。两个Backbone.View视图或模型之间的通信,用观察者是合适的。比如控制导航菜单的视图发出的信息,是面包屑(breadcrumb)挂件需要的(响应当前的层级)。挂件不需要引用导航视图,它只需要导航视图提供信息。更关键的,导航视图也许不是唯一的信息来源,别的视图可能也可以提供。此时,中介pub/sub模式是最理想的——而且自身扩展性良好。

看起来这样又好又全面,但是其实还有一个露点:如果我给对象定义一个本地事件,既想要观察者直接调用,又可以被订阅者间接访问到,怎么办?这就是我为什么说要串联在一起:你推送或者桥接本地事件到消息组去吧。需要些更多代码?很有可能——但是总比你把观察对象传递给所有观察者,一直紧耦合下去的情况好。然后,我们可以很好地继续以下两点...

3.) 选择性的“提交”本地事件到总线

最开始我几乎只用观察者模式来在JavaScript中触发事件。这是我们一次又一次遇到的模式,但更流行的客户端辅助库行为方式根本上来说是混合中介者的,给我们提供了就像它们是观察者模式的API。我最初写postal.js的时候,开始走进“为所有事物搭中介”的阶段。在我写的原型与构造函数中,分布各处的发布与订阅的调用并不罕见。当我从这个改变中自然的解耦受益时,非基础的代码开始似乎充满了相关于基础的部分。构造函数到处都要带上一个通道,订阅被当作新实例的一部分被创建,原型方法直接发布一个数值到总线(甚至本地的订阅者都不能直接的而必须监听总线以获得信息)。将这些明显关于总线的东西纳入app的这些部分,开始像是代码的味道。代码的“叙述”似乎总是被打断,如“噢,将这个向所有订阅者发布出去”,“等等!等等!监听这个通道那个事情。好,现在继续吧”。我的测试忽然开始需要依赖总线来做低层次的单元测试。而这感觉有点不对劲。

钟摆摆动的指向了中间,我认识到我应该保持一个“本地API”,并且在需要的时候通过一个中介者为应用扩展其可以触及的数据。 例如,我的backbone视图与模型,仍然用普通的Backbome.Events行为来给本地观察者发送事件(就是说,模型的事件被它相应的视图所观察)。当app的其它部分需要知道模型的变化时,我开始通过这些行将本地事件与总线桥接起来:

var SomeModel = Backbone.Model.extend({
 initialize: function() {
  this.on("change:superImportantField", function(model, value) {
   postal.publish({
    channel : "someChannel",
    topic : "omg.super.important.field.changed",
    data : {
    muyImportante: value,
    otherFoo: "otherBar"
    }
   });
  });
 }
});

重要的是要认识到,当有可能透明的推送事件到消息总线时,本地事件和消息必须被认为是分开的合约——至少概念上如此。换句话说,你要能够修改“内部的/本地的”事件而不破坏消息合约。这是要在脑海中记住的重要事实——否则你就是为紧耦合提供了一个新的途径,在一个方法上走反了!

所以理所当然,上述的模型是可以在没有消息总线的情况下被测试。而且如果我移去桥接在本地事件与总线之间的逻辑,我的视图与模型依然工作得毫无不畅。但是,这可是七行的例子(尽管格式化了)。 仅仅桥接四个事件就需要几乎三十行的代码。

噢,你怎样才能二者兼顾呢—— 在适合直接观察者时本地通知,同时使涉及事件可以扩展,以便你的对象不必给所有对象都发送一圈——不需要代码膨胀。通知怎样才能很少的代码又有更多的味道呢?

4.)在你的构架中隐藏样板

这并不是说上面的例子中的代码 —— 将事件接入总线 —— 的语法或概念是错误的(假设你接受本地和远程/桥接事件的概念)。然而,这是一个很好的体现在代码基础之上培养良好习惯的作用的例子。有时我们会听到类似“代码实在太多了”的抱怨(特别是当 LOC 作为代码质量的唯一判定者时)。 当这种情况下,我表示赞同。 它是一个可怕的样板。  下面是我在桥接 Backbone 对象的本地事件到 postal.js 时使用的模式:

// the logic to wire up publications and subscriptions
// exists in our custom MsgBackboneView constructor
var SomeView = MsgBackboneView.extend({

 className : "i-am-classy",

 // bridging local events triggered by this view
 publications: {
 // This is the more common 'shorthand' syntax
 // The key name is the name of the event. The
 // value is "channel topic" in postal. So this
 // means the bridgeTooFar event will get
 // published to postal on the "comm" channel
 // using a topic of "thats.far.enough". By default
 // the 1st argument passed to the event callback
 // will become the message payload.
 bridgeTooFar : "comm thats.far.enough",

 // However, the longhand approach works like this:
 // The key is still the event name that will be bridged.
 // The value is an object that provides a channel name,
 // a topic (which can be a string or a function returning
 // a string), and an optional data function that returns
 // the object that should be the message payload.
 bridgeBurned: {
  channel : "comm",
  topic : "match.lit",
  data : function() {
   return { id: this.get("id"), foo: 'bar' };
  }
 },

 // This is how we subscribe to the bus and invoke
 // local methods to handle incoming messages
 subscriptions: {
  // The key is the name of the method to invoke.
  // The value is the "channel topic" to subscribe to.
  // So this will subscribe to the "hotChannel" channel
  // with a topic binding of "start.burning.*", and any
  // message arriving gets routed to the "burnItWithFire"
  // method on the view.
  burnItWithFire : "hotChannel start.burning.*"
 },

 burnItWithFire: function(data, envelope) {
  // do stuff with message data and/or envelope
 }

 // other wire-up, etc.
});

显然你可以用几种不同的方式做这些——选择总线式的框架——这要比样板方式少很多无关内容,而且为Backbone开发人员所熟知。当你同时控制事件发送器和消息总线的实现时,桥接要更容易。这里有个将monologue.js发送器桥接到postal.js的例子:

// using the 'monopost' add-on for monologue/postal:
// assuming we have a worker instance that has monologue
// methods on its prototype chain, etc. The keys are event
// topic bindings to match local events to, and if a match is
// found, it gets published to the channel specified in the
// value (using the same topic value)
worker.goPostal({
 "match.stuff.like.#" : "ThisChannelYo",
 "secret.sauce.*" : "SeeecretChannel",
 "another.*.topic" : "YayMoarChannelsChannel"
});

以不同的方式使用样板是令人愉快的好习惯。现在我可以分别独立的测试我的本地对象,桥接代码,甚至测试二者合一的生产&消费期待的消息过程等等。

同样重要的是要注意到,如果我需要在上述的场景访问普通的postal API,没有什么可以阻止我这么做。没有丢失灵活性这么就等于成功了


5.) 消息是合约——要明智的选择实现方式

有两种将数据传递给订阅者的方法——也许可以给他们贴上更“官方”的标签,我将如此描述他们:

  • “0-n 参数”
  • “封套” (或“单对象载荷“)

看看这些例子:

// 0-n args
this.trigger("someGuyBlogged", "Jim", "Cowart", "JavaScript");
// envelope style
this.emit("someGuyBlogged", {
 firstName: "Jim",
 lastName: "Cowart",
 category: "JavaScript"
});
/*
 In an emitter like monologue.js, the emit call above
 would actually publish an envelope that looked similar
 to this:
 {
  topic: "someGuyBlogged",
  timeStamp: "2013-02-05T04:54:59.209Z",
  data : {
   firstName: "Jim",
   lastName: "Cowart",
   category: "JavaScript"
  }
 }
*/

经过一段时间,我发现封套方式比0-n参数方式要少很多很多麻烦(与代码)。"0-n参数"途径的挑战主要在于两个原因(就我的经验而言):第一,很典型的是“当事件触发时,你还记得要传递哪一个参数吗?不记得?好,我想我会看看触发的源头”。不是一个真正意义上的好方法,对吗?但它可以打断代码的正常流程。你可以用一个调试工具,检测执行条件下的参数值并由此推断基于这些数值的”标签“,但哪个更简单呢——看到一个”1.21“的参数值,困惑于它的意义,或者检测一个对象并发现{千兆瓦:1.21}。第二个原因是由于伴随事件传送可选的数据,以及当方法签名变得更长带来的痛苦。

"说实话,Jim,你这是在搭车棚。"或许是的,但是一段时间以来我一直看到代码的基础在扩充与变形,简单的包含一两个参数的原始事件,在其间包含了可选的参数以后开始变得畸形:

// 最开始是这样的
this.trigger("someEvent", "a string!", 99);
// 有一天, 它变得包含了一切
this.trigger("someEvent", "string", 99, { sky: "blue" }, [1,2,3,4], true, 0);
// 可是等等——第4和第5个参数是可选的,因此也可能传的是:
this.trigger("someEvent", "string", 99, [1,2,3,4], true, 0);
// 噢,你还检查第5个参数的真/假吗?
// 哎呦!现在是早先的参数了……
this.trigger("someEvent", "string", 99, true, 0);

如果有任何数据是可选的,将没有围绕它的测试。但需要更少的代码,需要能更具扩展性,特别典型的是能自解释(感谢这些成员名字)以便能在逐一传送给订阅者回调方法时,对一个对象进行那种测试。我仍然在不得不用"0-n参数"的地方用它,但如果由我决定,将是一直用封套的方法——我的事件发送者和消息总线都是这样。(说明我存在偏见,monologue与postal共享同一个封套的数据结构,去掉了monologue不用的通道)

因此——得承认用来给订阅者传输数据的结构是”合约“的一个部分。在封套方式这个方向,你可以用额外的元数据描述事件(不需要增加额外的参数)——这保持了方法签名(这就是合约的一个部分)对每个事件和订阅者一致。你也能很容易的为一个信息结构编制版本(或在必要的时候增加其他封套层级的信息)。如果你沿着这个方向做的话,请确保用的是一致的封套结构。

6.) 消息”拓扑“比你想的还重要

这里没有银弹。但是你要对如何命名主题与通道,以及如何设计消息载荷的结构深思熟虑。我倾向于用两种方法之一映射我的模型:用一个单一的数据通道,主题的前缀采用模型的名字,后跟其唯一的id,然后通过它的操作({modelType.id.operation})处理,或者给模型的自身通道,主题就是{id.operation}。一个恒定的习惯是在模型请求数据的时候自动响应这个行为。但并不是所有总线上的操作都是请求。可能有简单的事件发布到app。你是否命名主题来描述事件(理想条件下)?或者你是否掉进了这样的陷阱,通过命名主题来描述某个订阅者可能的倾向行为?例如,包含“route.changed” 抑或 “show.customer.ui”主题的消息。一个表明了事件,另一个表明了命令。做这些决定的时候要仔细思考。命令并不坏,但在你需要请求/响应或命令之前,你会为事件所能描述的数量而吃惊的。

(0)

相关推荐

  • 仿vs实现WPF好看的进度条

    为了界面友好,一般的操作时间较长时,都需要增加进度条提示.由于WPF自带的进度条其实不怎么好看,而且没啥视觉效果.后来,装VS2012时,发现安装过程中进度条效果不错,于是上网查了资料.学习了ModernUI(开源的),地址:https://github.com/firstfloorsoftware/mui. 后来,做了尝试写了个Demo,效果不错.另外,专门录制了tif文件,方便大家看到效果.废话不多说,先展示效果: 一.效果展示 A.VS2012安装界面图: B.个人尝试Demo效果图: 二

  • WPF绑定实例详解

    本文详细讲述了WPF绑定的用法,分享给大家供大家参考.具体用法分析如下: 1.WPF绑定使用的源属性必须是依赖项属性,这是因为依赖项属性具有内置的更改通知支持,元素绑定表达式使用了Xaml扩展标记,WPF绑定一个控件是使用Binding.ElementName,绑定非控件对象时使用Source,RelativeSource,DataContext属性(WPF特有,而非XAML),只能绑定对象的共有字段. 下边是部分Binding 属性名,完整列表参考 :http://msdn.microsoft

  • WPF自动隐藏的消息框的实例代码

    (鼠标放上去将一直显示,移开动画继续),提供normal和error两种边框. 介绍:传统的确定,取消,OK,CANCAL之类的对话框太繁琐了,由于项目需要而诞生的仿手机式提示对话框.当然传统的对话框项目中也有,这里就不做介绍了. 出场和退场动画做得很简单,就用Blend随便鼓捣了一番,将就用吧. 预览效果如下: 思路其实很简单:将窗体透明化->布局和样式设计->后台传值调用. 准备工作:Microsoft.Expression.Interactions.dll和System.Windows.

  • WPF实现渐变淡入淡出的登陆窗口效果

    本文实例讲述了WPF实现渐变淡入淡出的登陆窗口效果的方法.分享给大家供大家参考.具体实现方法如下: 1.实现原理 ① 利用UIElement.OpacityMask属性,用于改变对象区域的不透明度的画笔.可以使元素的特定区域透明或部分透明,从而实现比较新颖的效果. ② OpacityMask属性接受任何画刷,可利用LinearGradientBrush线性渐变画刷,通过对渐变画刷中各颜色点加以动画处理即可. 2.渐变淡入实现 渐变淡入效果,可通过事件触发器触发Loaded事件实现,所以可以仅用前

  • 用vbscript实现在消息框中显示一个超链接

    问: 嗨,Scripting Guy!可以在消息框中包含指向某个 Web 页面的超链接吗? -- CB 答: 嗨,CB.听起来您好像想要向用户显示一个消息框,并且希望在消息框中包含一个超链接,以便用户可以点击这个链接,从而获得更多信息.例如,假设某个用户试图访问一些资源,但被拒绝了.这种情况下,您可以弹出一个消息框,告诉用户访问被拒绝,然后向他们提供一个链接,这个链接指向的页面会告诉用户访问所需的权限.一般说来,这种办法会比简单地说"访问被拒绝"好得多. 那么,是否可以用脚本编写语言实

  • c++中的消息框messagebox()详细介绍及使用方法

    1.MessageBox("这是一个最简单的消息框!"); 2.MessageBox("这是一个有标题的消息框!","标题"); 3.MessageBox("这是一个确定 取消的消息框!","标题", MB_OKCANCEL ); 4.MessageBox("这是一个警告的消息框!","标题", MB_ICONEXCLAMATION ); 5.MessageBox(&

  • 详解JavaScript中的客户端消息框架设计原理

    哇--是个危险的题目,对吗?我们对于什么是本质的理解当然会随着我们对要解决问题的理解而变化.因此我不会说谎--一年前我所理解的本质很不幸并不完整,因为我确信我将要写的已经快伴随我有6个月之久.所以,这篇文章是我在发现JavaScript中成功的运用客户端消息模式的一些关键要点时的一个掠影. 1.) 理解中介者与观察者的区别   大多数人在描述任何事件/消息机制的时候喜欢套用"发布者/订阅者"(pub/sub)--但我认为这个术语不能很好的与抽象建立联系.当然,从根本上说,一些东西订阅了

  • 详解JavaScript中的链式调用

    链模式 链模式是一种链式调用的方式,准确来说不属于通常定义的设计模式范畴,但链式调用是一种非常有用的代码构建技巧. 描述 链式调用在JavaScript语言中很常见,如jQuery.Promise等,都是使用的链式调用,当我们在调用同一对象多次其属性或方法的时候,我们需要多次书写对象进行.或()操作,链式调用是一种简化此过程的一种编码方式,使代码简洁.易读. 链式调用通常有以下几种实现方式,但是本质上相似,都是通过返回对象供之后进行调用. this的作用域链,jQuery的实现方式,通常链式调用

  • 详解php中流行的rpc框架

    什么是RPC框架? 通常我们调用一个php中的方法,比如这样一个函数方法: localAdd(10, 20),localAdd方法的具体实现要么是用户自己定义的,要么是php库函数中自带的,也就说在localAdd方法的代码实现在本地,它是一个本地调用! 远程调用原理 比如 A (client) 调用 B (server) 提供的remoteAdd方法: 1.首先A与B之间建立一个TCP连接: 2.然后A把需要调用的方法名(这里是remoteAdd)以及方法参数(10, 20)序列化成字节流发送

  • 详解JavaScript中哪一种循环最快呢

    了解哪一种 for 循环或迭代器适合我们的需求,防止我们犯下一些影响应用性能的低级错误. JavaScript 是 Web 开发领域的"常青树".无论是 JavaScript 框架(如 Node.js.React.Angular.Vue 等),还是原生 JavaScript,都拥有非常庞大的粉丝基础.我们来谈谈现代 JavaScript 吧.循环一直是大多数编程语言的重要组成部分,而现代 JavaScript 为我们提供了许多迭代或循环值的方法. 但问题在于,我们是否真的知道哪种循环或

  • 详解JavaScript中的变量命名规范

    目录 驼峰命名 根据变量类型来命名 普通变量/属性 布尔变量/属性 普通函数/方法 回调.钩子函数 类 注意一致性 介词一致性 顺序一致性 表里一致性 时间一致性 其他注意事项 避免使用不常用的缩写 避免使用容易混淆的字母和数字 避免变量命名过于抽象 驼峰命名 首先,和其他语言一样,大部分变量建议采用驼峰命名法. var articleTitle = 'javascript变量命名规范' 而对于常量,使用大写字母和下划线来组合命名. const COUNTRY_NAME = 'China' 根据

  • 详解JavaScript中的4种类型识别方法

    具体内容如下: 1.typeof [输出]首字母小写的字符串形式 [功能] [a]可以识别标准类型(将Null识别为object) [b]不能识别具体的对象类型(Function除外) [实例] console.log(typeof "jerry");//"string" console.log(typeof 12);//"number" console.log(typeof true);//"boolean" console

  • 详解JavaScript 中getElementsByName在IE中的注意事项

    详解JavaScript 中getElementsByName在IE中的注意事项 前言: 在IE5-9中是没有实现js的 getElementsByClassName()方法,但是实现了getElementsByName()方法,但是需要注意的是这个方法在IE5-9中也返回id属性匹配的指定元素,为了兼容,应该小心谨慎使用,不要将同样的字符串同时用作了名字和ID. 测试程序如下: <div id="log"> <div id="innerLog"&

  • 详解Javascript中prototype属性(推荐)

    在典型的面向对象的语言中,如java,都存在类(class)的概念,类就是对象的模板,对象就是类的实例.但是在Javascript语言体系中,是不存在类(Class)的概念的,javascript中不是基于'类的',而是通过构造函数(constructor)和原型链(prototype chains)实现的.但是在ES6中提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板.通过class关键字,可以定义类.基本上,ES6的class可以看作只是一个语法糖,它的绝大部分功能

  • 详解Javascript 中的 class、构造函数、工厂函数

    到了ES6时代,我们创建对象的手段又增加了,在不同的场景下我们可以选择不同的方法来建立.现在就主要有三种方法来构建对象,class关键字,构造函数,工厂函数.他们都是创建对象的手段,但是却又有不同的地方,平时开发时,也需要针对这不同来选择. 首先我们来看一下,这三种方法是怎样的 // class 关键字,ES6新特性 class ClassCar { drive () { console.log('Vroom!'); } } const car1 = new ClassCar(); consol

  • 详解JavaScript中的Object.is()与"==="运算符总结

    三重相等运算符 === 严格检查2个值是否相同: 1 === 1; // => true 1 === '1'; // => false 1 === true; // => false 但是,ES2015规范引入了 Object.is(),其行为与严格的相等运算符几乎相同: Object.is(1, 1); // => true Object.is(1, '1'); // => false Object.is(1, true); // => false 主要问题是:什么时

随机推荐