Detailed description of the design principle of the client message framework in JavaScript
This article describes the design principle of the client message framework in JavaScript, including the communication between the client and the server. For more information, see
Wow -- it's a dangerous question, right? Our understanding of what is essential will certainly change with our understanding of the problem to be solved. So I won't lie-what I understood a year ago was unfortunately incomplete, because I was sure that what I was about to write had been around for six months. Therefore, this article is a glimpse of some key points in JavaScript's successful use of the client message mode.
1) understand the differences between the intermediary and the observer
Most people prefer to apply the "Publisher/subscriber" (pub/sub) when describing any event/message mechanism-but I don't think this term can be well associated with abstraction. Of course, basically, some things subscribe to other things publishing events. However, the layers at which publishers and subscribers are encapsulated may make a good model dark. So what is the difference?
Observer
The observer mode includes an object observed by one or more observers. Typically, this object records all the traces of the observer. Generally, a list is used to store the callback method registered by the observer. These are subscribed by the observer to receive the notification. Note: (Oh, I love them a lot)
?
1 2 3 4 5 6 7 |
Var observer = { Listen: function (){ Console. log ("Yay for more clich é examples ..."); } }; Var elem = document. getElementById ("cliche "); Elem. addEventListener ("click", observer. listen ); |
Note the following:
We must obtain a direct reference to this object.
This object must maintain some internal status and save the callback trace of the observer.
Sometimes the listener does not use any parameters returned from this object. Theoretically, there may be 0-N * parameters (more depends on how interesting it will be in the future)
* N is actually not infinite, but for the purpose of discussion, it refers to the limit we never reach.
Intermediary
The intermediary mode introduces a "third party" between an object and an observer-effectively decouples the two and encapsulates how they communicate. An intermediary's API may be as simple as "publish", "subscribe", and "Unsubscribe, or implementations within a certain domain scope may be provided to hide these methods in some more meaningful semantics. Most of the server-side implementations I have used are more inclined to the domain rather than the simpler ones, but there is no rule restriction on a common intermediary! It is not uncommon. There is an idea that a common intermediary is an information broker. Regardless of the situation, the results are the same-the specific object and the observer are no longer directly aware of each other:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
// It's fun to be naive! Var mediator = { _ Subs :{}, // A real subscribe wowould at least check to make sure // 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 (); |
You may think that, in addition to the pure intermediary implementation, a specific object is no longer responsible for saving the subscriber list, and the FatherTime and Developer) instances can never really know each other. They just shared a piece of information-as we will see in the future, this is a very important contract. "Good, Jim. This is still the Publisher/subscriber for me. What is the focus? Is there a difference when I select a certain direction ?" Oh, continue, dear readers. Continue.
2) understand when to use the intermediary and observer
Use the local observer and intermediary, that is, to write in the component, and the intermediary looks like a remote communication between components. Either way. My principle for this situation is -- tl; dr (too long; don't read) (too long to read ). But in any case, it is best to connect them together.
To put it simply, it is really troublesome. It is like compressing the meticulous experience of a few months into a trench with no less than 140 words. In reality, it is certainly not concise to answer this question. So there is a long version explanation:
Does the observer need to reference other projects besides data ing? For example, the Backbone. View directly references its model for various reasons. This is a natural relationship. A view not only needs to be rendered when the model changes, but also needs to call the event processing of the model. If the answer to the first question is "yes", the observer is meaningful.
If the relationship between the observer and the observed object is only dependent on data, I am willing to use the mediation pub/sub method. It is appropriate to use an observer for communication between two Backbone. View views or models. For example, the information sent from the view that controls the navigation menu is required by breadcrumb (in response to the current level ). The pendant does not need to reference the navigation view. It only needs the navigation view to provide information. More importantly, the navigation view may not be the only source of information, and other views may also be available. In this case, the mediation pub/sub mode is ideal-and its scalability is good.
It looks so good and comprehensive, but there is actually a dew point: if I want to define a local event for the object, both directly called by the observer and indirectly accessed by the subscriber, what should I do? That's why I want to connect them together: You can push or bridge local events to the message group. Need more code? It is very likely-but it is always better to keep coupling than to pass the observed object to all the observers. Then, we can continue with the following two points...
3) Select "Submit" local events to the bus
At first, I used almost only the observer mode to trigger events in JavaScript. This is the pattern we met again and again, but the more popular client-side auxiliary library behavior is basically a mixed intermediary, providing us with APIs like they are observer patterns. When I first wrote postal. js, I began to enter the stage of "building intermediary for all things. In the prototype and constructor I wrote, it is not uncommon to publish and subscribe calls from different places. When I naturally decouple from this change, the non-basic code starts to seem to be full of the underlying components. The constructor carries a channel everywhere, and the subscription is created as part of a new instance, the prototype method directly publishes a value to the bus (even local subscribers cannot directly listen to the bus to obtain information ). Incorporating these obvious bus-related things into these parts of the app begins to look like code. The "narration" of the Code seems to be interrupted, for example, "Oh, publish this to all subscribers", "and so on! Wait! Listen to this channel. Okay. Continue now ". My tests suddenly started to rely on bus for low-level unit tests. This seems a bit wrong.
The swing points to the middle. I realized that I should keep a "Local API" and expand the data that can be reached by an intermediary for the application as needed. For example, my backbone view and model still use common Backbome. Events behavior to send Events to local observers (that is, the model event is observed by the corresponding view ). When other parts of the app need to know the model changes, I start to bridge the local events with the bus through these lines:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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" } }); }); } }); |
It is important to realize that local events and messages must be considered separate contracts when transparent events are pushed to the Message bus-at least conceptually. In other words, you must be able to modify "Internal/local" events without damaging the message contract. This is an important fact to remember in your mind-otherwise, you will provide a new way for tight coupling and reverse it in a method!
Therefore, the above model can be tested without a message bus. And if I move to bridge the logic between the local event and the bus, my views and models still work well. However, this is an example of seven rows (although formatted ). Almost thirty lines of code are required to bridge only four events.
Oh, how can you take both of them into consideration-local notifications when appropriate for direct observers, and event-related extensions, so that your object does not have to send a circle to all objects-no code expansion is required. How can we make few codes more appealing to notifications?
4.) Hide the template in your architecture
This does not mean that the syntax or concept of the Code in the above example-connecting events to the bus-is incorrect (assuming you accept the concept of local and remote/Bridge events ). However, this is an example of the effect of cultivating good habits based on the code. Sometimes we hear complaints like "too many code" (especially when LOC is the only judge of code quality ). In this case, I agree. It is a terrible model. The following is the mode I used to bridge the local events of the Backbone object to postal. js:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// 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. // 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 shocould be the message payload. BridgeBurned :{ Channel: "comm ", Topic: "match. mismatch ", 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. // 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. }); |
Obviously, you can do this in several different ways -- select a bus-based framework -- this is much less irrelevant than the sample method and is well known to Backbone developers. Bridging is easier when you control the implementation of event senders and message bus at the same time. Here is an example of bridging monologue. js transmitters to postal. js:
?
1 2 3 4 5 6 7 8 9 10 11 |
// 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 // Value (using the same topic value) Worker. goPostal ({ "Match. stuff. like. #": "ThisChannelYo ", "Secret. sauce. *": "SeeecretChannel ", "Another. *. topic": "YayMoarChannelsChannel" }); |
Using templates in different ways is a good habit. Now, I can independently test my local objects, bridge code, and even test the production and consumption of the expected message processes.
It is also important to note that if I need to access a common postal API in the above scenario, nothing can prevent me from doing so. Without losing flexibility, this is a success.
5.) message is a contract-You must select an implementation method wisely.
There are two ways to pass data to subscribers-maybe they can be tagged with more "official" and I will describe them as follows:
"0-n parameter"
"Envelope" (or "single object load")
Let's look at these examples:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 0-n args This. trigger ("someGuyBlogged", "Jim", "coprocessor", "JavaScript "); // Envelope style This. emit ("someGuyBlogged ",{ FirstName: "Jim ", LastName: "coprocessor ", Category: "JavaScript" }); /* In an emitter like monologue. js, the emit call above Wocould actually publish an envelope that looked similar To this: { Topic: "someGuyBlogged ", TimeStamp: "2013-02-05T04: 54: 59.209Z ", Data :{ FirstName: "Jim ", LastName: "coprocessor ", Category: "JavaScript" } } */ |
After a while, I found that the encapsulation method is much less troublesome (and code) than the 0-n parameter method ). The challenge of the "0-n parameter" approach is mainly due to two reasons (in my experience): first, it is typical that "when an event is triggered, do you still remember which parameter to pass? Don't remember? Well, I think I will look at the trigger source ". Isn't it a really good way, right? But it can interrupt the normal process of the Code. You can use a debugging tool to check the parameter values under the execution conditions and infer the "tag" based on these values. But which one is simpler? See A "1.21" parameter value, confused about its meaning, or detect an object and find {Gigabit Watt: 1.21 }. The second reason is that the optional data is transmitted along with the event, and the pain that arises when the method signature gets longer.
"To be honest, Jim, you are in a taxi room. "Maybe yes, but for a while, I have been seeing that the foundation of the Code is being expanded and deformed. A simple primitive event that contains one or two parameters, the data becomes malformed after it contains optional parameters:
?
1 2 3 4 5 6 7 8 9 |
// This was initially the case. This. trigger ("someEvent", "a string! ", 99 ); // One day, it will contain everything This. trigger ("someEvent", "string", 99, {sky: "blue"}, [1, 2, 4], true, 0 ); // But so on -- the 4th and 5th parameters are optional, so it may also be passed: This. trigger ("someEvent", "string", 99, [1, 2, 4], true, 0 ); // Oh, do you still check the true/false values of the 5th parameters? // Alas! It's an earlier parameter ...... This. trigger ("someEvent", "string", 99, true, 0 ); |
If any data is optional, there will be no tests around it. But it requires less code and more scalability. In particular, it can be self-interpreted (thanks to the member names) so that when the callback method is sent to the subscriber one by one, perform that test on an object. I still use it where I have to use the "0-n parameter", but if I decide, I will always use the envelope method-my event senders and message bus are like this. (It indicates that I am biased. monologue and postal share the data structure of the same envelope and remove the channels not used by monologue)
Therefore, we must acknowledge that the structure used to transmit data to subscribers is a part of the "contract. In this direction of the envelope method, you can use additional metadata to describe the event (no additional parameters are required)-This maintains the method signature (this is part of the contract) each event is consistent with the subscriber. You can also easily compile a version for an information structure (or add other information at the envelope level as necessary ). If you do this, make sure that the same envelope structure is used.
6.) The message "Topology" is more important than you think.
There is no silver bullet here. But you need to think carefully about how to name theme and channel, and how to design the message load structure. I tend to map my model using one of the two methods: using a single data channel, the topic prefix uses the model name, followed by its unique id, then, through its operations ({modelType. id. operation}) processing, or giving the model its own channel, the topic is {id. operation }. A constant habit is to automatically respond to this behavior when the model requests data. However, not all bus operations are requests. Some simple events may be published to the app. Do you want to name a topic to describe the event (ideally )? Or did you fall into this trap and use the naming topic to describe the possible preference of a subscriber? For example, a message that contains the topic "route. changed" or "show. customer. ui. One indicates the event and the other indicates the command. Think carefully when making these decisions. The command is not bad, but before you need a request/response or command, you will be surprised by the number of events that can be described.