Maison >interface Web >js tutoriel >Explication détaillée des principes de conception du cadre de messages côté client dans les connaissances JavaScript_Basic
Wow, c'est une question dangereuse, non ? Notre compréhension de ce qui est essentiel change certainement avec notre compréhension du problème à résoudre. Je ne vais donc pas mentir, l'essentiel de ce que j'ai compris il y a un an est malheureusement incomplet car je suis sûr que ce que je m'apprête à écrire m'accompagne depuis presque 6 mois maintenant. Cet article est donc un aperçu de ma découverte de certains des points clés pour utiliser avec succès le modèle de messagerie côté client en JavaScript.
1.) Comprendre la différence entre médiateurs et observateurs
La plupart des gens aiment utiliser « éditeur/abonné » (pub/sub) pour décrire un mécanisme d'événement/message - mais je pense que cette terminologie ne correspond pas bien à l'abstraction. Fondamentalement, bien sûr, quelque chose souscrit aux événements publiés par autre chose. Mais le niveau auquel les éditeurs et les abonnés sont encapsulés peut rendre un bon modèle ennuyeux. Alors, quelle est la différence ?
Observateur
Le modèle d'observateur implique qu'un objet soit observé par un ou plusieurs observateurs. Généralement, cet objet enregistre les traces de tous les observateurs, généralement en utilisant une liste pour stocker les méthodes de rappel enregistrées par l'observateur, auxquelles l'observateur s'abonne afin de recevoir des notifications. REMARQUE : (Oh, jeu de mots, combien je les aime) (Note du traducteur : Observer Observer, note)
var observer = { listen : function() { console.log("Yay for more cliché examples..."); } }; var elem = document.getElementById("cliche"); elem.addEventListener("click", observer.listen);
Certaines choses à noter sont :
* n n'est pas réellement infini, mais pour les besoins de cette discussion, il fait référence à une limite que nous ne pourrons jamais atteindre
Intermédiaire
Le modèle Mediator introduit un « tiers » entre un objet et un observateur – découplant efficacement les deux et encapsulant la manière dont ils communiquent. L'API d'un médiateur peut être aussi simple que « publier », « s'abonner », « se désabonner », ou une implémentation à l'échelle du domaine peut être fournie pour cacher ces méthodes derrière une sémantique plus significative. La plupart des implémentations côté serveur que j'ai utilisées ont tendance à être à l'échelle du domaine plutôt que plus simples, mais il n'y a aucune règle limitant un médiateur générique ! Il n’est pas rare que l’idée selon laquelle un intermédiaire universel soit un courtier d’informations existe. Dans les deux cas, le résultat est le même : l'objet spécifique et l'observateur ne se connaissent plus directement :
// 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 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);
Si des données sont facultatives, aucun test ne sera effectué autour d'elles. Mais il nécessite moins de code, il doit être plus extensible et, généralement, il doit être explicite (grâce aux noms des membres) pour pouvoir effectuer le type de test effectué sur un objet lorsqu'il est transmis au méthodes de rappel des abonnés une par une. Je l'utilise toujours là où je dois utiliser des "paramètres 0-n", mais si cela ne tenait qu'à moi, ce serait de toujours utiliser l'approche enveloppe - à la fois pour mes expéditeurs d'événements et mes bus de messages. (Cela montre que je suis partial. Monologue et postal partagent la même structure de données d'enveloppe, et les canaux que monologue n'utilise pas sont supprimés)
Par conséquent, il faut reconnaître que la structure utilisée pour transmettre les données aux abonnés fait partie du « contrat ». Dans le sens wrapper, vous pouvez décrire l'événement avec des métadonnées supplémentaires (sans ajouter de paramètres supplémentaires) - cela maintient la signature de la méthode (qui fait partie du contrat) cohérente pour chaque événement et abonné. Vous pouvez également facilement versionner une structure de message (ou ajouter d'autres messages au niveau de l'enveloppe si nécessaire). Si vous allez dans cette direction, assurez-vous d’utiliser une structure d’enveloppe cohérente.
6.) Le message « topologie » est plus important que vous ne le pensez
Il n’y a pas de solution miracle ici. Mais vous devez réfléchir attentivement à la manière dont vous nommez les sujets et les canaux, ainsi qu’à la manière dont vous structurez la charge utile de vos messages. J'ai tendance à mapper mes modèles de deux manières : avec un seul canal de données, le sujet est préfixé par le nom du modèle, suivi de son identifiant unique, qui est ensuite traité par son opération ({modelType.id.operation}) , ou pour le propre canal du modèle, le sujet est {id.operation}. Une pratique constante consiste à répondre automatiquement à ce comportement lorsque le modèle demande des données. Mais toutes les opérations dans le bus ne sont pas des demandes. Des événements simples peuvent être publiés sur l'application. Nommez-vous des thèmes pour décrire les événements (idéalement) ? Ou êtes-vous tombé dans le piège de nommer des sujets pour décrire le comportement probablement prédisposé d'un abonné ? Par exemple, les messages contenant les sujets « route.changed » ou « show.customer.ui ». L’un indique un événement, l’autre un ordre. Réfléchissez bien lorsque vous prenez ces décisions. Les commandes ne sont pas mauvaises, mais vous seriez surpris du nombre d'événements qui peuvent être décrits avant d'avoir besoin d'une requête/réponse ou de commandes.