Heim > Web-Frontend > js-Tutorial > Detaillierte Erläuterung der Designprinzipien des clientseitigen Nachrichtenframeworks in JavaScript_Grundkenntnisse

Detaillierte Erläuterung der Designprinzipien des clientseitigen Nachrichtenframeworks in JavaScript_Grundkenntnisse

WBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWBOYWB
Freigeben: 2016-05-16 15:53:08
Original
1150 Leute haben es durchsucht

Wow – das ist eine gefährliche Frage, oder? Unser Verständnis dessen, was wesentlich ist, ändert sich sicherlich mit unserem Verständnis des zu lösenden Problems. Ich werde also nicht lügen – der Kern dessen, was ich vor einem Jahr verstanden habe, ist leider unvollständig, denn ich bin mir sicher, dass das, was ich schreiben werde, schon seit fast sechs Monaten bei mir ist. Dieser Artikel gibt einen Einblick in meine Entdeckung einiger der wichtigsten Punkte für die erfolgreiche Verwendung des clientseitigen Nachrichtenmusters in JavaScript.

1.) Verstehen Sie den Unterschied zwischen Mediatoren und Beobachtern
Die meisten Leute verwenden gerne „Herausgeber/Abonnent“ (pub/sub), wenn sie einen Ereignis-/Nachrichtenmechanismus beschreiben – aber ich denke, dass diese Terminologie nicht gut mit der Abstraktion harmoniert. Grundsätzlich abonniert etwas natürlich Ereignisse, die von etwas anderem veröffentlicht werden. Aber die Ebene, auf der Herausgeber und Abonnenten gekapselt sind, kann ein gutes Muster langweilig machen. Was ist also der Unterschied?


Beobachter

Beim Beobachtermuster wird ein Objekt von einem oder mehreren Beobachtern beobachtet. Typischerweise zeichnet dieses Objekt Spuren aller Beobachter auf und verwendet dabei in der Regel eine Liste zum Speichern der vom Beobachter registrierten Rückrufmethoden, die der Beobachter abonniert, um Benachrichtigungen zu erhalten. HINWEIS: (Oh, Wortspiel beabsichtigt, wie sehr ich sie liebe) (Anmerkung des Übersetzers: Beobachten, Beobachten, Anmerkung)

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

Einige Dinge, die Sie beachten sollten, sind:

  • Wir müssen einen direkten Verweis auf dieses Objekt erhalten
  • Dieses Objekt muss einen internen Status aufrechterhalten und die Rückrufspuren des Beobachters speichern
  • Manchmal nutzt der Listener die von diesem Objekt zurückgegebenen Parameter nicht aus. Theoretisch ist es möglich, 0-n*-Parameter zu haben (mehr hängt davon ab, wie interessant es später wird)

* n ist eigentlich nicht unendlich, aber für den Zweck dieser Diskussion bezieht es sich auf eine Grenze, die wir niemals erreichen können


Vermittler

Das Mediator-Muster führt einen „Dritten“ zwischen einem Objekt und einem Beobachter ein – wodurch die beiden effektiv entkoppelt und die Art und Weise, wie sie kommunizieren, zusammengefasst werden. Die API eines Mediators könnte so einfach sein wie „Veröffentlichen“, „Abonnieren“, „Abbestellen“, oder es könnte eine domänenweite Implementierung bereitgestellt werden, um diese Methoden hinter einer aussagekräftigeren Semantik zu verbergen. Die meisten serverseitigen Implementierungen, die ich verwendet habe, sind eher domänenweit als einfacher, aber es gibt keine Regeln, die einen generischen Mediator einschränken! Nicht selten gibt es die Vorstellung, dass ein Universalvermittler ein Informationsvermittler sei. In beiden Fällen ist das Ergebnis dasselbe – das konkrete Objekt und der Beobachter kennen sich nicht mehr direkt:

// 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();
Nach dem Login kopieren

你可能会想,除了特别纯粹的中介者实现,特定对象不再负有保存订阅者列表的责任,而且“时光老人”(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"
    }
   });
  });
 }
});
Nach dem Login kopieren

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

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

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

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.
});
Nach dem Login kopieren


显然你可以用几种不同的方式做这些——选择总线式的框架——这要比样板方式少很多无关内容,而且为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"
});
Nach dem Login kopieren

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

同样重要的是要注意到,如果我需要在上述的场景访问普通的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"
  }
 }
*/
Nach dem Login kopieren

经过一段时间,我发现封套方式比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);
Nach dem Login kopieren

Wenn Daten optional sind, werden keine Tests dazu durchgeführt. Aber es benötigt weniger Code, muss erweiterbarer sein und normalerweise muss es selbsterklärend sein (dank der Mitgliedsnamen), um die Art von Tests durchführen zu können, die für ein Objekt durchgeführt werden, wenn es an übergeben wird Rufen Sie die Teilnehmer-Rückrufmethoden nacheinander auf. Ich verwende es immer noch dort, wo ich „0-n-Parameter“ verwenden muss, aber wenn es nach mir ginge, würde ich immer den Umschlagansatz verwenden – sowohl für meine Ereignissender als auch für meine Nachrichtenbusse. (Es zeigt, dass ich voreingenommen bin. Monolog und Post haben dieselbe Umschlagdatenstruktur, und die Kanäle, die Monolog nicht verwendet, werden entfernt)

Daher muss anerkannt werden, dass die Struktur zur Übermittlung von Daten an Abonnenten Teil des „Vertrags“ ist. In Wrapper-Richtung können Sie das Ereignis mit zusätzlichen Metadaten beschreiben (ohne zusätzliche Parameter hinzuzufügen) – dadurch bleibt die Methodensignatur (die Teil des Vertrags ist) für jedes Ereignis und jeden Abonnenten konsistent. Sie können eine Nachrichtenstruktur auch problemlos versionieren (oder bei Bedarf weitere Nachrichten auf Umschlagebene hinzufügen). Wenn Sie in diese Richtung gehen, achten Sie darauf, dass Sie eine einheitliche Umschlagstruktur verwenden.


6.) Die Meldung „Topologie“ ist wichtiger als Sie denken

Hier gibt es kein Allheilmittel. Sie müssen jedoch sorgfältig darüber nachdenken, wie Sie Themen und Kanäle benennen und wie Sie Ihre Nachrichtennutzlasten strukturieren. Ich neige dazu, meine Modelle auf eine von zwei Arten abzubilden: Bei einem einzelnen Datenkanal wird dem Thema der Name des Modells vorangestellt, gefolgt von seiner eindeutigen ID, die dann von seiner Operation ({modelType.id.operation}) verarbeitet wird. oder für den eigenen Kanal des Modells lautet das Thema {id.operation}. Eine ständige Praxis besteht darin, automatisch auf dieses Verhalten zu reagieren, wenn das Modell Daten anfordert. Aber nicht alle Vorgänge im Bus sind Anfragen. Es können einfache Ereignisse in der App gepostet werden. Benennen Sie Themen, um Ereignisse zu beschreiben (idealerweise)? Oder sind Sie in die Falle getappt, Themen zu benennen, die das wahrscheinlich prädisponierte Verhalten eines Abonnenten beschreiben? Beispielsweise Nachrichten mit den Themen „route.changed“ oder „show.customer.ui“. Das eine weist auf ein Ereignis hin, das andere auf einen Befehl. Denken Sie sorgfältig nach, wenn Sie diese Entscheidungen treffen. Befehle sind nicht schlecht, aber Sie wären überrascht, wie viele Ereignisse beschrieben werden können, bevor Sie eine Anfrage/Antwort oder Befehle benötigen.

Verwandte Etiketten:
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Aktuelle Ausgaben
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage