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-metadata
The: 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.