What are design patterns?

  • Design patterns represent best practices and are typically adopted by experienced object-oriented software developers. Design patterns are solutions to common problems that software developers face during software development. These solutions are the result of trial and error by numerous software developers over a considerable period of time.
  • In 1994, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides co-authored a book titled Design Patterns - Elements of Reusable Object-Oriented Software (Chinese translation: Design Patterns - Reusable Object-Oriented Software). Elements) , which first mentioned the concept of design patterns in software development. The four authors are collectively called GOF (Gang of Four) .
  • A design pattern is actually a solution, a programming routine, and a problem-solving idea. It is not a specific code. The code is just its specific implementation. It has nothing to do with programming languages ​​or frameworks. It can be implemented in Go, you can use it in Vue, you can use it in React.
  • Special attention: "The purpose of using design patterns is to reusable code, make code easier to understand by others, ensure code reliability, and program reusability." If you can't meet this premise, then don't use it, you must use it according to the business scenario. Choose the right design pattern, don't rig it.
📢 Text All code is developed and run with node v17.8.0 version; pnpm package management is used for dependency management and installation;
🌏 Source address of this article: https://github.com/Jameswain/blog/tree/master/design-patterns/publish-subscribe

What is publish-subscribe?

  • Publish-subscribe is actually an event model. Usually, the event monitoring commonly used in front-end development is actually a publish-subscribe model; it collects a series of user monitoring (subscription) events, and then when the user clicks the monitoring (subscription) button, Just trigger (publish) all the listener (subscription) functions of the event;
  • Our official account is also a publish-subscribe model; follow (subscribe) an official account, when a new article is published on this official account, all users who follow this official account can receive the news of the new article!
  • Let's take a look at an example of the most common publish and subscribe in daily development:

     <!-- example/demo01-default-event.html-->
    <!doctype html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>发布订阅 - 事件监听</title>
    </head>
    <body>
    <div id="click-me" style="border: 1px solid green; width: 300px; height: 300px">点我</div>
    <script>
      const div = document.querySelector('#click-me');
      // 监听(订阅)点击事件,当用户点击这个元素时调用这个函数
      div.addEventListener('click', e => {
        console.log('点我', e)
      });
    
      // 监听(订阅)点击时间,多次订阅,点击时,会触发这个元素所有的订阅事件
      div.addEventListener('click', e => {
        console.log('点我 第2个订阅时间', e)
      })
    </script>
    </body>
    </html>
  • operation result:

image-20220425230355831

  • It can be found that publish and subscribe is actually the earliest design pattern that we have come into contact with on the front end, and it is also the most commonly used design pattern; like click or keydown this is defined inside the browser The event will only fire when the element is clicked or the keyboard is pressed!
  • What if I want to get an event that is not defined by the browser? Of course, because the browser supports custom events, the following example demonstrates how to use the browser's custom events:

     <!-- example/demo02-custom-event.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>发布订阅 - 浏览器自定义事件</title>
    </head>
    <body>
    <script>
      document.addEventListener('jameswain', function (e) {
        console.log('事件触发了', e, this);
      });
    
      document.addEventListener('jameswain', function (e) {
        console.log('第二次订阅,事件触发了', e, this);
      });
    
    
      // 控制台调用此函数触发
      function triggrtCustomEvent() {
        // 触发事件
        const event = new CustomEvent('jameswain', { detail: { tips: '我是事件自定义参数', age: 18 } });
        document.dispatchEvent(event);
      }
    </script>
    </body>
    </html>

    operation result:

image-20220425230411062

From the above two examples, it can be found that if you want to use the publish-subscribe mode in the browser, you can directly use the custom events of the browser without introducing a third-party library and building your own wheels; but the disadvantage is that the portability is poor, you can only use it in It can be used in browsers that support CustomEvent . You cannot port your code to Node environment or use it in a applet environment that does not support CustomEvent .
image-20220425230540918
Through caniuse , it can be found that major browser manufacturers have been supporting since 2011 CustomEvent by 2013, basically all mainstream browsers at that time were supported, and new browsers like Edge were launched in 2015. The first version is supported; 9 years from 2013 to 2022, so everyone can use it with confidence.

Publish and subscribe application scenarios

Through the above two small examples, we have basically understood what the publish and subscribe model is; let's forget those advanced concepts first, remember how to use publish and subscribe, and remember its usage in one sentence: "subscribe first (Monitoring) events, and then publishing (triggering) events" is as simple as that, but the above two small examples do not fully demonstrate the true power of publish and subscribe. Let me show the true power of publish and subscribe through a series of application scenarios.

Pass data across IFrames

In our daily work, we often encounter a situation where we need to embed a page under another domain name through an iframe in one system; at this time, the data communication between the two systems can also be carried out by publishing and subscribing. The following Demonstrate with a simple example:

  • Embedded iframe page:

     <!-- example/demo03-iframe/iframe.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>我是iframe页面</title>
      <style>
          body {
              text-align: center;
          }
          h4 {
              text-align: center;
          }
          #text {
              width: 100%;
          }
          #send-data {
              margin-top: 20px;
          }
      </style>
    </head>
    <body>
      <h4>我是iframe</h4>
      <textarea id="text" rows="4" ></textarea>
      <button id="send-data">发送数据到主index.html页面中</button>
      <script>
          const text = document.querySelector('#text');
          const btnSendData = document.querySelector('#send-data');
          btnSendData.addEventListener('click', () => {
              const content = text.value;
              top.document.dispatchEvent(new CustomEvent('iframe-data', { detail: { content } }));
          });
      </script>
    </body>
    </html>
  • home page:

     <!-- example/demo03-iframe/index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>主页面</title>
      <style>
          .main {
              width: 100%;
              border: 1px solid black;
              text-align: center;
          }
    
          .iframe {
              text-align: center;
              margin-top: 100px;
              width: 100%;
              border: 1px dotted orange;
          }
          .iframe iframe {
              width: 100%;
              height: 300px;
          }
      </style>
    </head>
    <body>
      <div class="main">
          <h4>我是主页面index</h4>
          <p>以下内容是从iframe中通过发布订阅模式传递过来的:</p>
          <div id="content"></div>
      </div>
      <div class="iframe">
          <iframe src="iframe.html"></iframe>
      </div>
      <script>
          const content = document.querySelector('#content');
          document.addEventListener('iframe-data', (e) => {
              content.innerHTML = e.detail.content;
          });
      </script>
    </body>
    </html>

    operation result:
    image.png

  • 通过上述代码可以发现, index.html iframe-data自定义事件, iframe.html页面中发布iframe-data事件Pass data to the main page via parameters!
  • At this time, some students will say that you can modify the content of the element through top.document.querySelector('#content').innerHTML ? Why use the publish-subscribe model? It is possible and simpler in the current example scenario; but if the index.html page is written using vue and the div#content rendering timing is uncertain, iframe.html page is written with React ; at this time, if you use top.document.querySelector('#content') , the element will not be found. The following is an example to demonstrate:

Embedded iframe page:

 <!-- example/demo04-iframe-async/iframe.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>我是iframe页面</title>
    <style>
        body {
            text-align: center;
        }
        h4 {
            text-align: center;
        }
        #text {
            width: 100%;
        }
        #send-data {
            margin-top: 20px;
        }
    </style>
</head>
<body>
    <h4>发布者:我是iframe,用React编写的</h4>
    <textarea id="text" rows="4" ></textarea>
    <button id="send-data">发送数据到主index.html页面中</button>
    <script>
        const text = document.querySelector('#text');
        const btnSendData = document.querySelector('#send-data');
        btnSendData.addEventListener('click', () => {
            const content = text.value;
            top.document.dispatchEvent(new CustomEvent('iframe-data', { detail: { content } }));
        });
    </script>
</body>
</html>

home page:

 <!--  example/demo04-iframe-async/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>主页面</title>
    <style>
        .main {
            width: 100%;
            border: 1px solid black;
            text-align: center;
        }

        .iframe {
            text-align: center;
            margin-top: 100px;
            width: 100%;
            border: 1px dotted orange;
        }
        .iframe iframe {
            width: 100%;
            height: 300px;
        }
    </style>
</head>
<body>
    <div class="main">
        <h4>订阅者:我是主页面index,用Vue写的,content元素10秒后才会被插入</h4>
        <p>以下内容是从iframe中通过发布订阅模式传递过来的:</p>
    </div>
    <div class="iframe">
        <iframe src="iframe.html"></iframe>
    </div>
    <script>
        const insertReault = new Promise(resolve => {
            // 10秒以后才会插入元素
            setTimeout(() => {
                const content = document.createElement('div');
                content.setAttribute('id', 'content');
                document.querySelector('.main').appendChild(content);
                resolve();
            }, 10 * 1000);
        });
        document.addEventListener('iframe-data', async (e) => {
            // 等content元素插入后,再更新内容
            await insertReault;
            const content = document.querySelector('#content');
            content.innerHTML = e.detail.content;
        });
    </script>
</body>
</html>

operation result:

Apr-10-2022 18-21-09.gif
Have you felt the power of publish and subscribe? 发布者 don't care about the business logic of 订阅者 , I don't care when your DOM elements are rendered, I just trigger the event to pass the data to you 订阅者 ; 订阅者 I don't care how or when you use this data, and I don't care about your business logic;

React passes data across components

image-20220426164205969

  • Before we talk about data transfer across components, let's take a look at the above organizational chart. The emperor wants to issue a message, and it is passed to officials at all levels through the prime minister; if the pavilion wants to report a message to the emperor, he has to Report layer by layer, in order to reach the emperor's ears. The biggest problem with this method is that the message transmission link is very long, and the message may be hijacked and not reported, and the transmission will be lost. Let's demonstrate the above effect through code:
 // example/demo05-passing-data-across-components/src/donot-use-publish-subscribe.jsx
// @ts-nocheck
import React from "react";
import './app.css'

function 亭长(props) {
  return <div className="col">
    亭长{props.shengzhi ? ': 卑职明白' : ''}
    { props.shengzhi ? '' : <span className="shangzouze" onClick={() => props.上奏折('皇上,郎中令那个王八蛋抢了我老婆!')}>上奏折</span> }
  </div>
}

function 乡(props) {
  return <div>
    <div className="col">乡{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <亭长 {...props} />
    </div>
  </div>
}

function 县令(props) {
  return <div>
    <div className="col">县令{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <乡 {...props} />
    </div>
  </div>
}

function 郡守(props) {
  return <div>
    <div className="col">郡守{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <县令 {...props} />
    </div>
  </div>
}

function 少府(props) {
  return <div className="col">少府{props.shengzhi ? ': 卑职明白' : ''}</div>
}

function 郎中令(props) {
  return <div>
    <div className="col">郎中令{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <郡守 {...props} />
    </div>
  </div>
}

function 宗正(props) {
  return <div className="col">宗正{props.shengzhi ? ': 卑职明白' : ''}</div>
}


function 御史大夫(props) {
  return <div className="col">御史大夫{props.shengzhi ? ': 卑职明白' : ''}</div>
}

function 丞相(props) {
  return <div>
    <div className="col">丞相{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <少府 {...props} />
      <郎中令 {...props} />
      <宗正 {...props} />
    </div>
  </div>
}

function 太尉(props) {
  return <div className="col">太尉{props.shengzhi ? ': 卑职明白' : ''}</div>
}

export default class 皇帝 extends React.Component {
  state = {
    shengzhi: '',
    zhouze: ''
  }

  render() {
    return  <>
      <div className="col huangdi">
        皇帝
        { this.state.zhouze ? ': 大胆郎中令,你竟敢干出如此伤天害理之事!' : '' }
        <span className="shengzhi" onClick={this.下圣旨}>下圣旨</span>
      </div>
      <div className="row">
        <御史大夫 {...this.state} />
        <丞相 {...this.state} 上奏折={this.上奏折} />
        <太尉 {...this.state} />
      </div>
    </>
  }

  下圣旨 = () => {
    this.setState({ shengzhi: '朕要大赦天下' });
  }

  上奏折 = (奏折内容) => {
    this.setState({ zhouze: 奏折内容 })
  }
}
📢 Note: The above code is to allow everyone to see the organizational structure more clearly, so the Chinese naming is deliberately used. In actual development, please do not use Chinese naming to challenge the bottom line of JavaScript.

​ Through the above code, we can find that the data transfer between each component is carried out through props, there are many props , if there is a little negligence, the props is lost, Then our component will not receive data. image-20220426165247428

​ And according to this layer-by-layer reporting mechanism, there will be a problem in the organizational structure of the above-mentioned ancient officials. 亭长 He wants to report with 皇帝 郎中令 If this beastly act of robbing his wife is reported layer by layer, it will definitely go through this 郎中令 , to 郎中令 Here only he doesn't want to die, then he will definitely not be there passed on. What should the pavilion do if the pavilion chief still wants to sue the beasts of Langzhongling? Another way is to sue the emperor, which is similar to our publishing and subscription. It is equivalent to establishing an exclusive channel between the pavilion and the emperor. The pavilion can go directly to the emperor. Let's look at the code below:

 // example/demo05-passing-data-across-components/src/use-publish-subscribe.jsx
// @ts-nocheck
import React from "react";
import './app.css'

function 亭长(props) {
  return <div className="col">
    亭长{props.shengzhi ? ': 卑职明白' : ''}
    { props.shengzhi ? '' : <span className="shangzouze" onClick={() => document.dispatchEvent(new CustomEvent('告御状', { detail: { zhouze: '皇上,郎中令那个王八蛋抢了我老婆!' }}))}>上奏折</span> }
  </div>
}

function 乡(props) {
  return <div>
    <div className="col">乡{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <亭长 {...props} />
    </div>
  </div>
}

function 县令(props) {
  return <div>
    <div className="col">县令{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <乡 {...props} />
    </div>
  </div>
}

function 郡守(props) {
  return <div>
    <div className="col">郡守{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <县令 {...props} />
    </div>
  </div>
}

function 少府(props) {
  return <div className="col">少府{props.shengzhi ? ': 卑职明白' : ''}</div>
}

function 郎中令(props) {
  return <div>
    <div className="col">郎中令{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <郡守 {...props} />
    </div>
  </div>
}

function 宗正(props) {
  return <div className="col">宗正{props.shengzhi ? ': 卑职明白' : ''}</div>
}


function 御史大夫(props) {
  return <div className="col">御史大夫{props.shengzhi ? ': 卑职明白' : ''}</div>
}

function 丞相(props) {
  return <div>
    <div className="col">丞相{props.shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <少府 {...props} />
      <郎中令 {...props} />
      <宗正 {...props} />
    </div>
  </div>
}

function 太尉(props) {
  return <div className="col">太尉{props.shengzhi ? ': 卑职明白' : ''}</div>
}

export default class 皇帝 extends React.Component {
  state = {
    shengzhi: '',
    zhouze: ''
  }

  componentDidMount() {
     // 接收御状消息
     document.addEventListener('告御状', (e) => {
       this.setState({ zhouze: e.detail.zhouze })
    });
  }

  render() {
    return  <>
      <div className="col huangdi">
        皇帝
        { this.state.zhouze ? ': 大胆郎中令,你竟敢干出如此伤天害理之事!' : '' }
        <span className="shengzhi" onClick={this.下圣旨}>下圣旨</span>
      </div>
      <div className="row">
        <御史大夫 {...this.state} />
        <丞相 {...this.state} />
        <太尉 {...this.state} />
      </div>
    </>
  }

  下圣旨 = () => {
    this.setState({ shengzhi: '朕要大赦天下' });
  }
}

image-20220426172812491

  • Through the above code, we can find that using the publish-subscribe mode, the pavilion directly transmits the memorial message to the emperor without going through any other components, which is the power of publish-subscribe;

The difference between redux and publish subscribe

​ Some students here will ask that a redux thing can also share data across components. What is the difference between this and publish and subscribe? In fact, the most essential difference is that redux is a cross-component 共享数据 . The data needed by each component is put into the store in a unified way. All components can obtain the data in this and are also eligible to modify this data inside.

​ Publish and subscribe only pass data. It does not have the ability to store or modify data. It can help you ignore the hierarchical relationship between various components and pass the data directly; you only need to know the event name. Modifying the data is still in the component itself. Let's take a look at the cross-component sharing of data using redux to implement the above organizational structure through code:

 // example/demo05-passing-data-across-components/src/use-react-redux.jsx
// @ts-nocheck
import React from "react";
import { Provider, useSelector, connect } from 'react-redux';
import store from './store';
import { reduxShangZhouZe, reduxXiaShengZhi } from './store/action';
import './app.css'

function 亭长() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div className="col">
    亭长{shengzhi ? ': 卑职明白' : ''}
    { shengzhi ? '' : <span className="shangzouze" onClick={() => reduxShangZhouZe('皇上,郎中令那个王八蛋抢了我老婆!')}>上奏折</span> }
  </div>
}

function 乡() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div>
    <div className="col">乡{shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <亭长 />
    </div>
  </div>
}

function 县令() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div>
    <div className="col">县令{shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <乡 />
    </div>
  </div>
}

function 郡守() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div>
    <div className="col">郡守{shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <县令 />
    </div>
  </div>
}

function 少府() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div className="col">少府{shengzhi ? ': 卑职明白' : ''}</div>
}

function 郎中令() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div>
    <div className="col">郎中令{shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <郡守 />
    </div>
  </div>
}

function 宗正() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div className="col">宗正{shengzhi ? ': 卑职明白' : ''}</div>
}


function 御史大夫() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div className="col">御史大夫{shengzhi ? ': 卑职明白' : ''}</div>
}

function 丞相() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div>
    <div className="col">丞相{shengzhi ? ': 卑职明白' : ''}</div>
    <div className="row">
      <少府 />
      <郎中令 />
      <宗正 />
    </div>
  </div>
}

function 太尉() {
  const shengzhi = useSelector(state => state.shengzhi)
  return <div className="col">太尉{shengzhi ? ': 卑职明白' : ''}</div>
}

@connect(state => state)
class 皇帝 extends React.Component {
  render() {
    return <>
      <div className="col huangdi">
        皇帝
        { this.props.zhouze ? ': 大胆郎中令,你竟敢干出如此伤天害理之事!' : '' }
        <span className="shengzhi" onClick={this.下圣旨}>下圣旨</span>
      </div>
      <div className="row">
        <御史大夫 />
        <丞相 />
        <太尉 />
      </div>
    </>
  }

  下圣旨 = () => {
    reduxXiaShengZhi('朕要大赦天下');
  }
}

export default () => <Provider store={store}>
  <皇帝 />
</Provider>

running result:

​ In terms of running effect, there is no difference between publish and subscribe, but the biggest impression to us is that the amount of code has increased a lot, especially when redux is initialized, 5 files are added; if you have never been exposed to redux, this is for you. This is indeed a little difficult, and it is more simple to publish and subscribe; in addition to this change, there is another change that our state data has been put into redux, and the props layer is no longer needed between components. The layer is passed, and when the data changes, you can directly modify the data in redux. The component display data is also taken from redux, and any component can operate and modify the data in redux.

Passing data between CocosCreator and React across frameworks

image-20220426175402144

  • Passing data across components is just a small test of the publish-subscribe model. Passing data across frameworks can truly demonstrate its power. Students who have played Cocos know that CocosCreator software must be used to develop Cocos games, and it is also very similar to the React framework. The difference between the two, how to achieve low coupling and low intrusion to achieve mutual data communication between the two is particularly important; because in practice, Cocos game development and React front-end development are usually two teams, and they usually don't know much about each other's frameworks. Therefore, the decoupling of communication between frameworks is very important.
  • I have encountered such a scenario in my working life. There is a function written and implemented in cocos (the game team is responsible for the development and they do not understand React), but it needs to be embedded in the front-end page written in React (the front-end team They are responsible for developing the page (they don't understand Cocos), and need to control some behaviors in cocos in the front-end page; how can I do it without knowing the other party's framework and logic details, and can control some behaviors of the other party's framework? Using publish and subscribe can perfectly solve this problem. Let me simulate this scene through a demo.

  • The picture above is the development interface of CocosCreator. Suppose this is a small game developed by the game team. If they do not understand React, they will use CocosCreator to develop the game. , but the front-end pages need to operate the two functions in cocos 关闭音乐 and 亢龙有悔 ; and Cocos games and React front-end pages are independent warehouses and independent deployments.

  • The above picture is the source code and resources packaged and compiled by CocosCreator. It can be found that the resources and source code have been split and compressed, and the readability is very low. The Cocos game we want to embed in the front end is also a compiled code. If we start from this entry point It is obviously not advisable to start.
  • If you use publish and subscribe, it is very simple. The developers of the Cocos game team only need to add two lines of code to subscribe to the event. The code is as follows:
 // 订阅 亢龙有悔 事件
document.addEventListener('onKangLongYouHui', this.onKangLongYouHui.bind(this));
// 订阅 关闭音乐 事件
document.addEventListener('onCloseMusic', this.onCloseMusic.bind(this));
  • Cocos developer doesn't need to care how the front end embeds his game? How to trigger his event? He only needs to listen to the two events when they are triggered, and do the relevant processing;
  • Front-end developers don't need to care about how the music is turned off inside Cocos? How to send out the 亢龙有悔 skill; the front end only needs to know that when I trigger the onCloseMusic event is to turn off the music and trigger the onKangLongYouHui event is to trigger the 亢龙有悔 skill---6d62cf103bc0 , As for how your cocos internal logic is implemented, how to write code, I don't need to care, it is completely decoupled, let's take a look at the code implementation:
  • First of all, you do not need to install CocosCreator related environment and software, you only need to enter /blog/design-patterns/publish-subscribe/example/demo06-cocos-across-react/Kingdoms/build/web-mobile to start an http service in this directory, for example:

  • After the server is started, it can be accessed in the browser. The effect of running is as follows:

  • The demo of the Cocos game has been running smoothly, but we cannot operate the game characters; what if our project triggers 亢龙有悔 skill? In fact, you can directly trigger the subscribed events through the console, for example:

  • It can be seen that as long as we publish (trigger) onKangLongYouHui this event in the console, we can operate the character in Cocos to send out 亢龙有悔 this skill, we don't need to know Cocos or understand it at all How is this skill implemented logic and its animation controlled, isn't it very decoupled? However, our front-end programmer can trigger it through the console. If it is used by the user, we can't be so violent. Next, we need to embed the game into our page through a page, and then trigger it through a button. Look at the code:
 // example/demo06-cocos-across-react/react-with-cocos/src/app.jsx
import { useCallback } from 'react';
import cs from './app.module.scss';

function App() {
  const onCloseMusic = useCallback(() => {
    document.querySelector('iframe').contentWindow.document.dispatchEvent(new CustomEvent('onCloseMusic'));
  }, []);

  const onKangLongYouHui = useCallback(() => {
    document.querySelector('iframe').contentWindow.document.dispatchEvent(new CustomEvent('onKangLongYouHui'))
    console.log(document.querySelector('iframe').contentWindow.document)
  }, []);

  return (
    <div className={cs.app}>
      <iframe className={cs.iframe} src='./web-mobile/index.html'></iframe>
      <h1 className={cs.h1}>下面是React框架里面的代码</h1>
      <div className={cs.tool}>
        <div className={cs.btn} onClick={onCloseMusic}>关闭音乐</div>
        <div className={cs.btn} onClick={onKangLongYouHui}>亢龙有悔</div>
      </div>
    </div>
  )
}

export default App

  • Through the running effect, we can find that we have successfully controlled some behaviors in the Cocos game in the React framework by publishing and subscribing, and we do not need to install the development environment related to CocosCreator. Of course, this is just to demonstrate the communication between React and Cocos. If you are Vue or Angular, you can do it. This thing has nothing to do with the framework language. The core is the idea.

  • The decoupling of Cocos game logic and front-end React logic can be achieved through the publish-subscribe model. Both parties only need to agree on the function of the event name. As for how the other party's logic is implemented, they do not need to care about each other; the Cocos game team subscribes onKangLongYouHui event, I don't care when your front-end triggers, when you like it; I only pay attention to when you trigger, I will give you skills.

Client (Android) and front-end (JavaScript) communication

​ Students who have developed hybrid should know that the front-end and the client have a communication method called bridge. Through the bridge, the mutual communication between the client and the front-end can be realized, thereby expanding the capabilities of the front-end. In fact, whether the web pages we develop are mobile or PC, our front-end code is not only directly running on the system, the PC is running on a browser software on the operating system, and the mobile is running on the operating system. It is on the Webview component in an APP, so our front-end does not have many APIs that can directly go to the bottom layer of the operating system. Below, we will demonstrate how to use publish and subscribe to achieve data communication between the front-end and customers through two demos.

​ Before talking about the communication between the Android client and the front end, let's first understand the principle of the client calling the front end. In fact, the way the webview.loadUrl client calls the front end is very webview.evaluateJavascript . --Function, this is the same principle as we call the window.call webview.evaluateJavascript directly in the browser console:

image-20220529142230502

  • First, let's look at the first case. When the volume of the Android client changes and when the user presses the back button, the front-end function needs to be called to notify the front-end:
 // example/demo08-android-call-javascript/src/case1/index.jsx
import { useState, useEffect } from 'react'
import Other from './other';
import cs from './index.module.scss';

function Case1() {
  const [text, setText] = useState('');

  useEffect(() => {
    /** 客户端会调用这个函数 */
    window.callJs = function({ type, param }) {
      if (type === 'VOLUME_UP') {
        console.log('param: ', param);
        setText('音量调大了');
      } else if (type === 'BACK') {
        setText('用户按返回键');
      }
    }
  }, [])

  return (
    <div className={cs.app}>
      <h3>我是app组件</h3>
      <div>
        app组件的内容: {text}
      </div>
      <Other />
    </div>
  )
}

export default Case1
 // example/demo08-android-call-javascript/src/case1/other.jsx
import React from 'react';
import cs from './index.module.scss';

export default class Other extends React.PureComponent {
  state = {
    content: ''
  }
  
  render() {
    const { content } = this.state;
    return <div className={cs.other}>
      <h3>我是Other组件</h3>
      Other组件的内容: {content}
    </div>
  }

  componentDidMount() {
    /** 客户端会调用这个函数 */
    window.callJs = function({ type, param }) {
      if (type === 'VOLUME_DOWN') {
        this.setState({ content: '音量调小了' });
      }
    }
  }
}

In the above code, we call the client by mounting the callJs function on the window, and then pass different types to represent the different operations of the user, and we mount the callJs function on the window in both components, the consequences of this That is, the callJs function in the Case1 component will overwrite the callJs function mounted in other.jsx. Let's see the running effect:

 title=

  • Now that we know that the callJs function mounted later will overwrite the callJs logic mounted by the previous component, how should we solve such a problem? Now it's time to publish and subscribe again. First, we use the publish-subscribe model to collect the user's operation logic through events, and only mount callJs once globally. Let's look at the code below:
 // example/demo08-android-call-javascript/src/bridge.js
const bridge = {
  MAP_CALLJS: {},
  /**
   * 监听事件
   */
  on(type, fn) {
    if (this.MAP_CALLJS[type]) {
      this.MAP_CALLJS[type].push(fn)
    } else {
      this.MAP_CALLJS[type] = [fn];
    }
    console.log('this.MAP_CALLJS: ', this.MAP_CALLJS);
  },
  /**
   * 触发函数
   * @param {*} type 
   */
  emit(type, param) {
    if (!this.MAP_CALLJS[type]) return;
    this.MAP_CALLJS[type].forEach(fn => fn(param));
  }
}

window.callJs = ({ type, param }) => bridge.emit(type, param);

export default bridge;
  • Use bridge to subscribe to events:
 // example/demo08-android-call-javascript/src/case2/index.jsx
import { useState, useEffect } from 'react'
import bridge from '../bridge';
import Other from './other';
import cs from './index.module.scss';

function Case2() {
  const [text, setText] = useState('');

  useEffect(() => {
    // 订阅相关事件,等待客户端调用
    bridge.on('VOLUME_UP', param => {
      setText(param.msg);
    });
    // 订阅相关事件,等待客户端调用
    bridge.on('BACK', param => {
      setText(param.msg);
    });

  }, [])

  return (
    <div className={cs.app}>
      <h3>我是app组件</h3>
      <div>
        app组件的内容: {text}
      </div>
      <Other />
    </div>
  )
}

export default Case2
 // example/demo08-android-call-javascript/src/case2/other.jsx
import React from 'react';
import bridge from '../bridge';
import cs from './index.module.scss';

export default class Other extends React.PureComponent {
  state = {
    content: ''
  }
  
  render() {
    const { content } = this.state;
    return <div className={cs.other}>
      <h3>我是Other组件</h3>
      Other组件的内容: {content}
    </div>
  }

  componentDidMount() {
    bridge.on('VOLUME_DOWN', param => {
      this.setState({ content: '音量调小了' });
    });

    bridge.on('VOLUME_UP', param => {
      this.setState({ content: param.msg });
    });
  }
}

It can be found from the above code that we collect the user's event operation logic through the publish-subscribe mode, and mount it only once globally window.callJs and then when the client calls the front-end callJs function, according to The event type loop calls the event just listened to by the front end and passes the parameters. Let's look at the execution result:

 title=

By running the results, it is found that the logic has not been overwritten, and we can listen to related bridge events in any component.

Node.js event triggers

In node.js, there is a built-in module called event trigger events , this module is a standard publish and subscribe implementation, let's feel its function and use through the code:

 // example/demo09-node-event/demo01.mjs
import EventEmitter from 'events';

const emitter = new EventEmitter();

emitter.once('a', e => {
  console.log('1once.a', e);
});

emitter.on('a', e => {
  console.log('a1', e);
});

function callback() {
  console.log('a callback');
}
emitter.on('a', callback);
emitter.on('a', callback);
emitter.once('a', callback);
emitter.off('a', callback);

emitter.once('a', e => {
  console.log('2once.a', e);
});

emitter.once('a', e => {
  console.log('3once.a', e);
});

emitter.on('a', e => {
  console.log('a2', e);
});

emitter.emit('a', 'emit发出事件1');
console.log('==================');
emitter.emit('a', 'emit发出事件2');
console.log('==================');
emitter.emit('a', 'emit发出事件3')
console.log('==================');
emitter.emit('a', 'emit发出事件4');
console.log('==================');
emitter.emit('a', 'emit发出事件5');
console.log('==================');
emitter.emit('a', 'emit发出事件6');

operation result:

image-20220426093003134

📢 The node version used here is v17.8.0

Implementation principle of publish and subscribe

  • After feeling the power and application scenarios of publish and subscribe, we need to understand the implementation principle of this design pattern, how to implement a publish and subscribe? We take the function of the event trigger of node.js as a reference, and use 70 lines of code to implement a general publish and subscribe function.
  • A basic publish and subscribe has three main functions:

    • Monitor (on) => Collect event handlers, classify them and store them in an array.
    • Trigger (emit) => Execute the collected data processing function; first in, first out, the event that is listened first will be triggered first.
    • Remove (off) => remove the collected data processing function; last-in, first-out, the events monitored later will be removed first.
📢 subscribe (listen); publish (trigger)
  • After understanding the basic functions, we only need to remember one sentence to launch the implementation of the publish-subscribe design pattern: "Subscription is actually collecting event processing functions, classifying them and storing them in an array; publishing is actually processing the collected events. function to call and execute", so that's why you need to subscribe first and then publish.
 // example/demo09-node-event/events.mjs
/**
 * 事件函数检查
 */
 function checkListener(listener) {
  if (typeof listener !== 'function') {
    throw Error('你传入的不是一个函数');
  }
}

class Events {
  constructor() {
    /** 事件记录 */
    this.MAP_EVENTS = {};
  }
  /**
   * 订阅事件
   * @param {String} key 事件名称
   * @param {Function} listener 事件回调函数
   * @param {Boolean} once 是否触发一次后移除事件
   */
  on(key, listener, once = false) {
    checkListener(listener);
    if (this.MAP_EVENTS[key]) {
      this.MAP_EVENTS[key].push({ listener, once });
    } else {
      this.MAP_EVENTS[key] = [{ listener, once }];
    }
    return this.MAP_EVENTS;
  }
  /**
   * 订阅事件 - 只有第一次触发事件时被回调
   * @param {String} key
   * @param {Function} listener
   */
  once(key, listener) {
    checkListener(listener);
    this.on(key, listener, true);
  }
  /**
   * 取消订阅
   * @param {String} key 事件名称
   * @param {Function} listener 事件回调函数,匿名函数无效
   */
  off(key, listener) {
    checkListener(listener);
    const arrEvents = this.MAP_EVENTS[key] || [];
    if (arrEvents.length) {
      // 移除事件是后进先出,后监听的事件会被先移除
      const index = arrEvents.lastIndexOf(e => e.listener === listener);
      this.MAP_EVENTS[key].splice(index, 1);
    } else {
      console.log(`你从来都没有订阅过${key}事件,所以你取消个🥚`);
    }
  }
  /**
   * 触发事件
   * @param {String} key 事件名称
   * @param  {...any} args 事件参数
   */
  emit(key, ...args) {
    const arrEvents = this.MAP_EVENTS[key] || [];
    // 执行事件是先进先出;先监听的事情会被先执行
    arrEvents.forEach(e => e.listener.call(this, ...args));
    // 第一次触发后需要把once事件全部移除掉
    this.MAP_EVENTS[key] = arrEvents.filter(e => !e.once);
  }
}

// 默认事件对象,挂载在静态函数上
const defaultEvent = new Events();
Events.on = Events.prototype.on.bind(defaultEvent)
Events.once = Events.prototype.once.bind(defaultEvent)
Events.off = Events.prototype.off.bind(defaultEvent)
Events.emit = Events.prototype.emit.bind(defaultEvent)

export default Events;
  • Then we can replace the built-in node events used in the previous example with our own implementation; we can find that the results obtained are exactly the same, which proves that our implementation of publish and subscribe is correct.
 // example/demo09-node-event/demo02.mjs
import EventEmitter from './events.mjs';    // 其他逻辑代码没有变化,只需要把这个换成自己实现发布订阅即可

image-20220426094017180

  • Careful students will find that we have implemented it by ourselves events.mjs there is such a piece of code:
 // 默认事件对象,挂载在静态函数上
const defaultEvent = new Events();
Events.on = Events.prototype.on.bind(defaultEvent)
Events.once = Events.prototype.once.bind(defaultEvent)
Events.off = Events.prototype.off.bind(defaultEvent)
Events.emit = Events.prototype.emit.bind(defaultEvent)
  • The main purpose of this design is to prepare for 懒人 , because some people do not want to use new directly, then you can use it like the following:
 // example/demo09-node-event/demo03.mjs
import Events from './events.mjs';

Events.on('coding', (...param) => {
  console.log('懒人 on coding => ', ...param);
})

Events.once('coding', (...param) => {
  console.log('懒人 once-coding => ', ...param);
})

Events.emit('coding', '张三');
console.log('============================');
Events.emit('coding', '李四');
console.log('============================');
Events.emit('coding', '王五');
console.log('============================');


// 除了使用使用静态函数,还可以创建一个新的事件对象,事件名称一样也不会冲突,互相隔离
const e1 = new Events();
e1.on('coding', (name) => {
  console.log('e1-on-coding', name);
});

e1.once('coding', (name) => {
  console.log('e1-once-coding', name);
});

e1.emit('coding', 'Ice King');
console.log('============================');
e1.emit('coding', 'Simon King');
console.log('============================');
e1.emit('coding', 'Alex King');
console.log('============================');

operation result:

image-20220426122742322

Through the running results, it can be found that although the event names between objects are the same, they are isolated from each other, so you can create different event objects according to your business scenarios, and manage them by classification, which can effectively avoid the problem of the same name conflict of event names.

at last

​ The implementation of the publish-subscribe design pattern is not particularly complicated. I think the most important thing is not its code implementation, but its design ideas and application scenarios; only in the right application scenarios can it play its real role. Power; in this article, I just simulated and demonstrated some of the more common application scenarios, and it also has many variant branches and usage skills that are also very clever.


Jameswain
332 声望12 粉丝