Hi – Bus is developed in the project I publish subscribe message message Bus system, similar to common message Bus system, is implements the subscribe to news and publishing news, but I use the TypeScript decorator to simplify the subscribe and publish operation, through the use of the actual project feel very convenient, so release out to share with you.

Source code I published in Github interested friends can have a look, all the code is not much can be handwritten oh.

So I’m going to give you some ideas about how to design these devices, and if you’re interested, I hope it’ll help you learn about decorators and proxies.

Without further ado, let’s start with an example code to see how it works

// Import Hi-Bus, you can use any word instead 'HiBus'
import HiBus, {Bus, Publish, Subscribe} from 'hi-bus'

// First we must create a class with class '@bus'
// greet '@bus' is used to collect subscribe functions
// it will not work without '@Bus’ when you subscribe message in class

@Bus
class Test {  

  // Subscribe topic "Handshake" with wow @Subscribe
  // You only focus the function logic
  @Subscribe("handshake")
  handshake() {
    console.log (" Handshake "); }// When you call function 'pulbishHandshake'
  // it will publish topic ‘handshake’ automatic
  // the function must return something to trigger publish
  // if return nothing or return void, it will not trigger publish
  @Publish("handshake")
  publishHandshake() {
    return{}; }}const test = new Test();

test.publishHandshake();
/ / output:
// Handshake
Copy the code

As you can see, hi-Bus subscribes and publishes messages using only modifier declarations, eliminating the need to write subscriptions by hand, which reduces spelling errors and repeated lookups.

How do I implement message subscription

Message subscription is one of the most important parts of Hi-Bus, and it uses both Proxy and Reflect, both of which are already part of the new JavaScript standard, so you can use them with confidence.

The first is how to implement the @subscribe decorator, which is actually very simple. Add a new attribute to the target method, and add the topic to the new attribute. Don’t wonder why methods can add attributes, but in JavaScript everything is an object, including methods, and now we’re used to writing a class like this, right

class Test {
  hello(){...}
}
Copy the code

It’s just a syntactic sugar, and it’s actually going to be treated this way

const Test = {}
Test.prototype.hello = function () {... }Copy the code

The hello method is actually a property on the prototype chain of the Test object, so you can add properties to it

const Test = {}
Test.prototype.hello = function() {... } Test.prototype.hello.topic ='topic'
Copy the code

It’s perfectly ok to add attributes to method Hello like this, and that’s the first step in implementing @Subscribe.

The next thing to do is simply to add the topic attribute to the method annotated by the decorator to indicate that this is a message callback method, and the subscribed message is in the topic attribute.

export const Subscribe = (topic: string) :MethodDecorator= > {
  return (target, propertyKey, descriptor) = > {
    Reflect.set(descriptor.value, BusMeta.Subscrible, topic); }}Copy the code

Here I’m using the new standard Reflect to add attributes to the target method, which could have been done with Object.defineProperty. A bit of fun for Microsoft here, because Microsoft is going to add meta meta properties to TypeScript, which means methods, properties, and objects. JavaScript already does that, so why bother?

Note that the parameters in the method modifier signature function are described in detail in the MDN documentation, but here I will only give an explanation of the code I wrote

It’s intuitive that target is supposed to represent the method that’s being modified, but actually target refers to the Class itself that defines the method, and to get the method that’s being modified is to get it from the descriptor, so why is it so counterintuitive, I think it’s for flexibility, I won’t go into that here.

Once you add the necessary attributes to the subscribed methods, it’s time to add them to the message callback queue, using the @bus decorator. The implementation code is as follows.

export const Bus = function (target: any) {

  // Get descriptiors of target's prototype
  const props = Object.getOwnPropertyDescriptors(target.prototype)

  // Create proxy class
  const proxy = new Proxy(target, {
    construct(C, args) {

      const instance = newC(... args);Object.values(props).forEach(prop= > {
        const method = prop.value;

        // Check if there are [message subscribe] and [Publish message] properties
        const topicSubscribe = Reflect.get(method, BusMeta.Subscrible);

        // Determine if it is a subscription message
        if (typeoftopicSubscribe ! = ='undefined') {
          const proxyMethod = new Proxy(method, {
            apply(_method, _this, args){ _method.apply(instance, args); } }) busInstance.subscribe(topicSubscribe, proxyMethod); }})returninstance; }})// Return the instance object of the proxy
  return proxy;
}
Copy the code

How do I push callback methods into the callback message queue in a Bus decorator

In the @bus decorator, I succeeded in intercepting the instantiation method of the target Class by creating a Proxy object. I was shocked when Proxy was used. In the intercept instantiation method, I first create an instance of the target Class

const instance = newC(... args);Copy the code

Why should I create a new instance? The reason is that when I use apply I need to point this to an instance of the target Class, but the target in the Class decorator is actually the target Class, so it’s easier to understand what I’m writing as a prototype

const Test = {};
/ / ^
// |
// target
Copy the code

I can’t get the instance object by using the target parameter of the class modifier. Then I went to the Proxy to see if there was any way to get the instance object, but the result was also negative. So ON a whim I tried to see if it was possible to create an instance of an object in constructor and then return that instance object, and it turned out that it worked perfectly, just like a normal instance object.

Now with the instance objects, will have a huge step forward, the next step is to get the target class in the class is marked subscribe message method, problem again, the parameters of the target class decorator didn’t we defined method, in fact, it is an empty object, that is wrong, I define methods are where to go? Think about how JavaScript implements object orientation.

const Test = {}
Test.prototype.hello = function () {... }Copy the code

That’s right! All properties and methods defined in the target Class are defined in the target Class prototype chain, so we need to get the target. Prototype. The following code

// Get descriptiors of target's prototype
const props = Object.getOwnPropertyDescriptors(target.prototype)
Copy the code

Now that I have access to all the methods, it’s easy to just go through and see which methods have subscribed messages in them and you’re done. The implementation code is as follows

Object.values(props).forEach(prop= > {
    const method = prop.value;

    // Check if there are [message subscribe] and [Publish message] properties
    const topicSubscribe = Reflect.get(method, BusMeta.Subscrible);

    // Determine if it is a subscription message
    if (typeoftopicSubscribe ! = ='undefined') {
      const proxyMethod = new Proxy(method, {
        apply(_method, _this, args){ _method.apply(instance, args); } }) busInstance.subscribe(topicSubscribe, proxyMethod); }})Copy the code

Here I create a new proxy for the subscription method. The purpose of this is to point this to the instance object I just created, so that the user can write the subscription method as if it were used in a class, regardless of where this points.

conclusion

Above is the Hi – I am in the Bus how to implement the @ the Subscribe and @ Bus the thinking of the two decorator, write this article is to make everyone in the use of third-party libraries is not only can use, but also to understand how this library is created, when you project safety requirements of high cannot use third-party libraries, can manual for yourself to achieve them.