Wah – soalan berbahaya, bukan? Pemahaman kita tentang perkara yang penting pasti berubah dengan pemahaman kita tentang masalah yang perlu diselesaikan. Jadi saya tidak akan berbohong - intipati apa yang saya fahami setahun yang lalu malangnya tidak lengkap kerana saya pasti apa yang saya akan tulis telah bersama saya selama hampir 6 bulan sekarang. Jadi, artikel ini adalah gambaran sekilas tentang penemuan saya tentang beberapa perkara penting untuk berjaya menggunakan corak pemesejan pihak pelanggan dalam JavaScript.
1.) Fahami perbezaan antara pengantara dan pemerhati
Kebanyakan orang suka menggunakan "penerbit/pelanggan" (pub/sub) apabila menerangkan mana-mana mekanisme acara/mesej - tetapi saya fikir istilah ini tidak sesuai dengan abstraksi. Pada asasnya, sudah tentu, sesuatu melanggan acara yang diterbitkan oleh sesuatu yang lain. Tetapi tahap di mana penerbit dan pelanggan dikapsulkan boleh menjadikan corak yang baik membosankan. Jadi, apa bezanya?
Pemerhati
Corak pemerhati melibatkan objek yang diperhatikan oleh seorang atau lebih pemerhati. Biasanya, objek ini merekodkan kesan semua pemerhati, biasanya menggunakan senarai untuk menyimpan kaedah panggil balik yang didaftarkan oleh pemerhati, yang dilanggan oleh pemerhati untuk menerima pemberitahuan. NOTA: (Oh, sebenarnya, betapa saya menyayangi mereka) (Nota Penterjemah: Perhatikan Perhatikan, nota)
var observer = { listen : function() { console.log("Yay for more cliché examples..."); } }; var elem = document.getElementById("cliche"); elem.addEventListener("click", observer.listen);
Beberapa perkara yang perlu diambil perhatian ialah:
* n sebenarnya bukan tak terhingga, tetapi untuk tujuan perbincangan ini, ia merujuk kepada had yang tidak boleh kita capai
Perantara
Corak Pengantara memperkenalkan "pihak ketiga" antara objek dan pemerhati - dengan berkesan memisahkan kedua-duanya dan merangkum cara mereka berkomunikasi. API pengantara mungkin semudah "terbit", "langgan", "nyahlanggan", atau pelaksanaan seluruh domain mungkin disediakan untuk menyembunyikan kaedah ini di sebalik beberapa semantik yang lebih bermakna. Kebanyakan pelaksanaan bahagian pelayan yang saya gunakan cenderung untuk seluruh domain dan bukannya lebih mudah, tetapi tidak ada sebarang peraturan yang mengehadkan pengantara generik! Tidak jarang, terdapat idea bahawa perantara sejagat ialah broker maklumat. Dalam kedua-dua kes, hasilnya adalah sama - objek khusus dan pemerhati tidak lagi mengenali satu sama lain secara langsung:
// 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);
Jika mana-mana data adalah pilihan, tiada ujian di sekelilingnya. Tetapi ia memerlukan kurang kod, ia perlu lebih diperluaskan, dan lazimnya ia perlu dijelaskan sendiri (terima kasih kepada nama ahli) untuk dapat melakukan jenis ujian yang dilakukan pada objek kerana ia diserahkan kepada kaedah panggilan balik pelanggan satu demi satu. Saya masih menggunakannya di mana saya perlu menggunakan "parameter 0-n", tetapi jika terpulang kepada saya, ia adalah untuk sentiasa menggunakan pendekatan sampul surat - kedua-duanya untuk penghantar acara dan bas mesej saya. (Ini menunjukkan bahawa saya berat sebelah. Monolog dan pos berkongsi struktur data sampul surat yang sama, dan saluran yang tidak digunakan oleh monolog dialih keluar)
Oleh itu - mesti diakui bahawa struktur yang digunakan untuk menghantar data kepada pelanggan adalah sebahagian daripada "kontrak". Dalam arah pembalut, anda boleh menerangkan acara dengan metadata tambahan (tanpa menambah parameter tambahan) - ini memastikan tandatangan kaedah (yang merupakan sebahagian daripada kontrak) konsisten untuk setiap acara dan pelanggan. Anda juga boleh membuat versi struktur mesej dengan mudah (atau menambah mesej peringkat sampul surat lain jika perlu). Jika anda pergi ke arah ini, pastikan anda menggunakan struktur sampul surat yang konsisten.
6.) Mesej "topologi" adalah lebih penting daripada yang anda fikirkan
Tiada peluru perak di sini. Tetapi anda perlu berfikir dengan teliti tentang cara anda menamakan topik dan saluran, dan cara anda menstrukturkan muatan mesej anda. Saya cenderung untuk memetakan model saya salah satu daripada dua cara: Dengan saluran data tunggal, topik diawali dengan nama model, diikuti dengan id uniknya, yang kemudiannya diproses oleh operasinya ({modelType.id.operation}) , atau untuk saluran model sendiri, topiknya ialah {id.operation}. Amalan berterusan adalah untuk bertindak balas secara automatik kepada tingkah laku ini apabila model meminta data. Tetapi tidak semua operasi di dalam bas adalah permintaan. Mungkin terdapat acara ringkas yang disiarkan ke apl. Adakah anda menamakan tema untuk menerangkan peristiwa (sebaik-baiknya)? Atau adakah anda telah terjerumus ke dalam perangkap menamakan topik untuk menerangkan kemungkinan tingkah laku kecenderungan pelanggan? Contohnya, mesej yang mengandungi topik "route.changed" atau "show.customer.ui". Satu menunjukkan acara, satu lagi pesanan. Fikirkan dengan teliti semasa membuat keputusan ini. Perintah tidak buruk, tetapi anda akan terkejut melihat berapa banyak peristiwa yang boleh diterangkan sebelum anda memerlukan permintaan/tindak balas atau arahan.