This is the 16th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

preface

There are several ways to communicate with front-end components: pass parent props on each other, promote public state, and use Context. However, these general methods often rely on the API of the framework itself, or are limited by the relationships between components. So instead of frameworks, two independent components, how do we get them to talk to each other?

Imagine a scenario where you have two tabs open: TabA and TabB, TabA has A button that asks for an interface and then closes the current Tab. TabB asks for an interface and displays A dialog box. The component that can control the closure of A Tab is Tab Container C.

Relationship analysis

There are three objects:

class component TabA,

class component TabB,

The class component TabContainerC,

There are three more key methods:

Function leave(){},

TabContainerC closeTab and

The firm openDialog.

graph LR
id1["tabA.leave()"] --param: 'A'--> id2["tabC.closeTab(param)"] --> id3["tabB.openDialog()"]

The management and invocation of the three functions should be on the component instance in which they reside, and the code would be complex and difficult to maintain if the common state and methods were pushed.

If the three components could call each other, the code would be very simple.

Hijacking component instances

With TS decorators, we can easily hijack component instances

const ins = {};
function colleague(colleagueName: string) {
    return function<T extends {new(... args:any[]): {}}>(target: T) {
        // Hijack the constructor to get this
        let colleague: Colleague<any> = null;
        return class extends target {
            constructor(. arg:any[]) {
                super(... arg);// Get this and put it in the collection
                ins[target.displayName] = this
            }
            componentWillUnmount() {
                // Pseudo-code to clear hijacked instances when uninstalled
                delete ins[target.displayName]
            }
        };
    };
},

Copy the code

Use decorators

@colleague(a)class TabContainerC extends React.Component<PropsTypes> {
    close(param:string){... }}Copy the code

TabA call TabContainerC

class TabA extends React.Component<PropsTypes> {
    leave(){
        ins['TabContainerC']? .close('A')}}Copy the code

The mediator pattern

The above method seems to solve the problem, but it has several drawbacks, the biggest being unordered calls.

graph TD
A --> B;
A --> C --> B
C --> D --> A
D --> B --> A
D --> C --> E --> D

What if TabContainerC does not want to expose a method because of some extra arguments?

If you have 1000 project components, how do you control the calls between them, such as a little console.log

This is where the intermediary design pattern can be considered

graph LR
A --> X;
B --> X 
C --> X 
D --> X 

X --> A;
X --> B
X --> C 
X --> D 
X --> E 

As a mediator, X is responsible for communicating with other members, so X can control the communication of all members, such as logging, fault tolerance, filtering, etc

Conceptual design and Api design

Two roles

  • It was nothing more than a Mediator
    • Used to manage messaging between members. A Colleague registers, and a Mediator receives information about the member. A Colleague can be notified to find a Colleague when a Colleague’s method needs to be called
  • Members of the Colleague:
    • A description of a component instance, including the instance of the component, and the methods exposed by the component. The unique identifier is the member name, which is the name of the component by default and can also be customized

Three of the API

  • @colleague(colleagueName): Registered as a member, parameter colleagueName isWill chooseThe member name
  • @colleagueAction(actionName): Declares the method to be exposed. ActionName is the method flag
  • @notifyMediator(colleagueName, actionName,params?): Notifying the mediation that a member named colleagueName executes the exposed actionName method and can pass the params parameter

Usage:

@colleague('TabContainerC')
class TabContainerC extends React.Component<PropsTypes> {...@colleagueAction('closeTab')
    close(params:string){... }... }@colleague('TabA')
class TabA extends React.Component<PropsTypes> {
    @notifyMediator('TabContainerC'.'closeTab'.'A')
    leave(newAccount: AccountManageModel.Account){... }}Copy the code

Some details

  • reflect-metadataThe: method decorator executes in front of the class decorator, resulting in a public method whose member has not yet been generated and which needs to be identified with reflection-metadata and then iterated when the constructor is overridden
  • The class name changes after packaging, so the registered member is. The parameter colleagueName is the mandatory member name
  • Asynchronous invocation of other instances is supported when a promise is returned

Reference code

The directory structure is as follows:

├ ─ ─ ─ Colleague. Ts ├ ─ ─ ─ but ts ├ ─ ─ ─ Mediator. Ts └ ─ ─ ─ the readme, mdCopy the code

Colleague.ts:

/** * The member class * contains the member name and source instance ** /
export default class Colleague<T> {
    actions = new Map<string.(payload? :any) = > void> ();constructor(public name: string.private instance: T) {}
    setInstance(instance: T) {
        this.instance = instance;
    }
    getInstance() {
        return this.instance;
    }

    setAction(actionName: string, fn: (payload? :any[]) = >void) {
        this.actions.set(actionName, fn);
    }

    // Execute a specific method
    performAction(actionName: string, payload? :any) {
        const action = this.actions.get(actionName);
        if(! action) {console.warn('there is no action names ' + actionName + ' in colleague' + this.name);
        } else {
            return action.apply(this.instance, payload); }}}Copy the code

Mediator. Ts:

import Colleague from './Colleague';
/** * The mediator class is used to collect members * forward messages between members */
export default class Mediator {
    // Registered members of the intermediary office
    colleagues = new Map<string, Colleague<any> > ();// Register members
    registColleague(colleague: Colleague<any>) :void {
        this.colleagues.set(colleague.name, colleague);
    }
    // Unpack members
    unRegistColleague(colleague: Colleague<any>) :void {
        this.colleagues.delete(colleague.name);
    }
    / / notice
    notify(colleagueName: string.actionName: string, payload? :any[]) :void {
        const colleague = this.colleagues.get(colleagueName);
        if(! colleague) {console.warn('there is no colleague names ' + colleagueName);
        } else{ colleague.performAction(actionName, payload); }}}Copy the code

index.ts:

import Mediator from './Mediator';
import Colleague from './Colleague';
import 'reflect-metadata';

function mediatorFactory() {
    const mediator = new Mediator();
    console.log(mediator);
    // Create closure
    return {
        // Class decorator
        colleague: function (colleagueName: string) {
            return function <T extends {new(... args:any[]): {}}>(target: T) {
                    // Hijack the constructor to get this
                    let colleague: Colleague<any> = null;
                    return class extends target {
                            static displayName = (target as any).displayName + '$Colleague';
                            constructor(. arg:any[]) {
                                    super(... arg); colleague =new Colleague(colleagueName, this);
                                    // Retrieve the saved public method from the metadata
                                    Reflect.ownKeys(target.prototype).forEach(key= > {
                                            if (typeof key === 'number') {
                                                    key = key.toString();
                                            }
                                            const actionMethod = Reflect.getMetadata('action', target.prototype, key);
                                            if(actionMethod) { colleague.setAction(actionMethod.name, actionMethod.value); }}); mediator.registColleague(colleague); }componentWillUnmount() {
                                    mediator.unRegistColleague(colleague);
                                    let willUnMount = target.prototype.componentWillUnmount;
                                    willUnMount && willUnMount.call(this); }}; }; },// Method decorator, open method
        colleagueAction: function (actionName: string) {
            return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
                // Put the open method into the mediation before executing the class decorator, which needs to be saved
                Reflect.defineMetadata('action', {name: actionName, value: descriptor.value}, target, propertyKey);
            };
        },
        // Method decorator
        notifyMediator: function (colleagueName: string, actionName: string. payload:any[]) {
            return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
                // Hijack method, notify the intermediary
                return {
                    writable: true.enumerable: true.configurable: true.value: function (. args:any[]) {
                        // tslint:disable-next-line:no-invalid-this
                        let result = descriptor.value.apply(this, args);
                        if (result instanceof Promise) {
                            result.then(function () {
                                mediator.notify(colleagueName, actionName, payload);
                            });
                        } else {
                            mediator.notify(colleagueName, actionName, payload);
                        }
                        returnresult; }}; }; }}; }export const {colleague, colleagueAction, notifyMediator} = mediatorFactory();

Copy the code

At the end

Instance-hijacking is a simple API and clear thinking approach, but it clearly cannot be used without UI component instances (such as hooks). Next we’ll look at another method: HOT Events.