详解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”主题的消息。一个表明了事件,另一个表明了命令。做这些决定的时候要仔细思考。命令并不坏,但在你需要请求/响应或命令之前,你会为事件所能描述的数量而吃惊的。

以上是小编为您精心准备的的内容,在的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索javascript
客户端
javascript客户端检测、客户端javascript、javascript 客户端ip、javascript 富客户端、富客户端框架,以便于您获取更多的相关知识。

时间: 2024-09-11 19:00:50

详解JavaScript中的客户端消息框架设计原理_基础知识的相关文章

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

  这篇文章主要介绍了详解JavaScript中的客户端消息框架设计原理,包括客户端和服务器端的通信等方面的内容,需要的朋友可以参考下 哇--是个危险的题目,对吗?我们对于什么是本质的理解当然会随着我们对要解决问题的理解而变化.因此我不会说谎--一年前我所理解的本质很不幸并不完整,因为我确信我将要写的已经快伴随我有6个月之久.所以,这篇文章是我在发现JavaScript中成功的运用客户端消息模式的一些关键要点时的一个掠影. 1.) 理解中介者与观察者的区别 大多数人在描述任何事件/消息机制的时候

详解JavaScript中基于原型prototype的继承特性_基础知识

JavaScript 中的继承比较奇葩,无法实现接口继承,只能依靠原型继承. 原型链原型就是一个对象,通过构造函数创建出来的实例会有指针指向原型得到原型的属性和方法.这样,实例对象就带有构造函数的属性方法和原型的属性方法,然后将需要继承的构造函数的原型指向这个实例,即可拥有这个实例的所有属性方法实现继承. 看下面演示代码: //声明超类,通过构造函数和原型添加有关属性和方法 function Super(){ this.property = true; } Super.prototype.get

详解JavaScript中的事件流和事件处理程序_基础知识

事件流:分两种,IE的是 事件冒泡流 ,事件开始时从最具体的元素接收,逐级向上传播到较为不具体的节点(Element -> Document).与之相反的是 Netscape 的 事件捕获流 . DOM2级事件规定事件流包括三个阶段:事件捕获阶段.处于目标阶段和事件冒泡阶段. 大多数情况下都是将事件处理程序添加到事件流的冒泡阶段.一个 EventUtil 的栗子: var EventUtil = { addHandler: function(element, type, handler){ if

详解JavaScript中双等号引起的隐性类型转换_基础知识

引子 if语句应该是程序员用的比较多的语句,很多时候都要进行if判断,if语句一般用双等号来判断前后两个元素是否是一致的,假如是一致,那么返回是true,然后执行下面的语句,否则,执行别的语句.本文所说的隐性类型的转换,说的是==引起的转换.举个简单的例子,双等号不是全等号,全等号是"==="三个等号,语句"1"==1,那么一般情况下是前面的字符串"1"转换为数字1,然后进行比较.通过这个例子应该了解了什么是隐性类型的转换了吧! 隐性类型转换步骤

举例详解Python中smtplib模块处理电子邮件的使用_基础知识

在基于互联网的应用中,程序经常需要自动地发送电子邮件.如:一个网站的注册系统会在用户注册时发送一封邮件来确认注册:当用户忘记登陆密码的时候,通过邮件来取回密码.smtplib模块是python中smtp(简单邮件传输协议)的客户端实现.我们可以使用smtplib模块,轻松的发送电子邮件.下面的例子用了不到十行代码来发送电子邮件:   #coding=gbk import smtplib smtp = smtplib.SMTP() smtp.connect("smtp.yeah.net"

详解JavaScript语法对{}处理的坑爹之处_基础知识

JavaScript的语法有多坑,算是众人皆知了. 先来上张图 代码如下: 复制代码 代码如下: {} + [];    // 0[] + {};    // "[object Object]"{} + [] == [] + {};    // false({} + [] == [] + {});    // true 这么蛋疼的语法坑估计也只有 JavaScript 这样的奇葩才有. 相信对于绝大部分不研究 JavaScript 编译器的童鞋,根本无法理解.(至少我也是觉得不可思议)

详解JavaScript对W3C DOM模版的支持情况_基础知识

 本文档对象模型允许访问所有的文档内容和修改,由万维网联合会(W3C)规范.几乎所有的现代浏览器都支持这种模式. 在W3C DOM规范的大部分传统DOM的功能,而且还增加了新的重要的功能.除了支持forms[ ], images[ ]和文档对象的其它数组属性,它定义了方法,使脚本来访问和操纵的任何文档元素,而不只是专用元件状的表单和图像.文档属性在W3C DOM: 此模型支持所有传统DOM提供的属性.此外,这里是文档属性,可以使用W3C DOM访问列表:  文档方法在W3C DOM: 此模型支持

详解Node.js模块间共享数据库连接的方法_基础知识

这个标题本身就是一个命题,因为使用默认方式的情况下,一个 Node.js 应用里的各个模块都是共享的同一个数据库连接.但是如果姿势不对,可能会很丑陋,甚至可能会出错. 你可以忽略下面这部分,直接切入正题. 背景最近在做专业课程设计,题目是"机票预订管理系统".需求比较简单,就试着拿最近在学的 Node.js 来做了.本来还在调研用何种 Node.js 框架比较合适,看了几个框架之后发现这是杀鸡用牛刀,有看文档查资料的时间还不如直接动手写了.最后写完我会把代码放到 Github 上,欢迎

深入理解JavaScript的React框架的原理_基础知识

如果你在两个月前问我对React的看法,我很可能这样说:     我的模板在哪里?javascript中的HTML在做些什么疯狂的事情?JSX开起来非常奇怪!快向它开火,消灭它吧!  那是因为我没有理解它. 我发誓,React 无疑是在正确的轨道上, 请听我道来. Good old MVC 在一个交互式应用程序一切罪恶的根源是管理状态. "传统"的方式是MVC架构,或者一些变体. MVC提出你的模型是检验真理的唯一来源 - 所有的状态住在那里. 视图是源自模型,并且必须保持同步. 当模