如何在Web组件(本地UI)之间进行通信?

22

我正在尝试在我的UI项目中使用原生Web组件,而对于这个项目,我不使用任何框架或库(如Polymer等)。我想知道是否有最佳方式或其他方式来在两个Web组件之间进行通信,就像在angularjs / angular中所做的那样(例如消息总线概念)。

目前,在UI Web组件中,我使用dispatchevent发布数据,而对于接收数据,我则使用addeventlistener。例如,有2个Web组件ChatForm和ChatHistory。

// chatform webcomponent on submit text, publish chattext data 
this.dispatchEvent(new CustomEvent('chatText', {detail: chattext}));

// chathistory webcomponent, receive chattext data and append it to chat list
this.chatFormEle.addEventListener('chatText', (v) => {console.log(v.detail);});

请告知我还有哪些其他方法可用于此目的。任何像postaljs等好的库都可以轻松与原生UI Web组件集成。

6个回答

26
如果你把Web组件看作像内置组件,比如<div><audio>,那么你就可以回答自己的问题了。这些组件之间不会互相通信。
一旦你允许组件直接彼此通信,那么你实际上就不再有组件了,而是一个被紧密联系在一起的系统,你不能在没有组件B的情况下使用组件A。这样联系得太紧密了。
相反,在拥有这两个组件的父代码中,您可以添加代码,使您能够从组件A接收事件并在组件B中调用函数设置参数,反之亦然。
话虽如此,内置组件有两个例外:
  1. 标签<label>:它使用for属性来接收另一个组件的ID,并且如果设置正确,当你单击时,它会将焦点传递到其他组件。

  2. 标签<form>:它寻找子元素中的表单元素,以收集提交表单所需的数据。

但是这两个元素仍然没有和任何东西“绑定”。

<label> 元素会接收 focus 事件并且只有在 ID 设置正确时才将其传递给对应的元素,否则传递给第一个表单元素作为子级。而 <form> 元素不关心其子元素的存在或数量,只会查找所有后代元素中的表单元素并获取它们的 value 属性。

但通常情况下,应避免让一个兄弟组件直接与另一个兄弟组件通信。上述两个示例中的跨组件通信方法可能是唯一的例外。

相反,您的父代码应该监听事件并调用函数或设置属性。

是的,您可以将该功能封装在一个新的父组件中,但请避免使用混乱的代码。

通常情况下,我从不允许兄弟元素之间直接通信,它们只能通过事件与其父级通信。父级可以通过属性、属性和函数直接与其子级通信。但在其他情况下应尽量避免这样做。


1和2都可以重写为仅使用事件,但需要一些额外的工作,因为如果表单说“ALLMYCHILDREN”,它不知道要处理多少个响应;所以你需要某种定时来确定“最后的回复”。有点像学生进入我的教室,我不知道今天会有多少人或以什么顺序来。但我有一个严格的规定...在最后一个人进入后等待2分钟,然后锁门(是的,用钥匙)...这教给他们基于事件的编程 :-) - Danny '365CSI' Engelman
表单没有时间问题,因为它们的子元素在提交表单时已经存在。我包含了示例1和示例2来展示规则的两个唯一的例外情况。传统DOM元素中的所有其他内容都是通过事件处理和访问子元素属性和属性,或调用它们的函数来处理的。 - Intervalia
非常感谢@Intervalia提供的出色解释。我理解Web组件就像内置的Web组件,其行为应完全相同。我还学习了您提到的父级、属性、属性等概念,并尝试在我的项目中应用它们。 :) - Sandeep
@Intervalia 假设我有一个视图组件,其中包含两个子组件:列表和工具栏。在列表中选择项目(复选框)会触发自定义事件,该事件一直冒泡到父视图组件。如果选择列表中的项目,则工具栏应启用用户可以在列表上使用的某些工具。选项是让视图直接与工具栏交互,或传递事件以更新全局状态(类似于redux),在那里视图然后侦听状态的更改并更新工具栏。什么情况下使用其中之一比另一个更可取? - holmberd
如果让组件A与组件B通信,那么你就把它们绑在了一起。如果你需要用组件C替换组件B并且接口不同,那么你需要更改组件A以知道如何与C通信。相反,如果允许父级处理事件,则父级需要知道如何与C通信。但这是父级编写者的选择,而不是组件A的选择。因此,允许父级处理差异比让A与B或C一起工作更有意义。 - Intervalia

9

工作示例

在您的父代码(html/css)中,您应该订阅由<chat-form>发出的事件,并通过执行其方法(如下面的add)将事件数据发送到<chat-history>

// WEB COMPONENT 1: chat-form
customElements.define('chat-form', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `Form<br><input id="msg" value="abc"/>
      <button id="btn">send</button>`;

    btn.onclick = () => {
      // alternative to below code
      // use this.onsend() or non recommended eval(this.getAttribute('onsend'))
      this.dispatchEvent(new CustomEvent('send',{detail: {message: msg.value} }))
      msg.value = '';
    }
  }
})


// WEB COMPONENT 2: chat-history
customElements.define('chat-history', class extends HTMLElement {
  add(msg) {
    let s = ""
    this.messages = [...(this.messages || []), msg];
    for (let m of this.messages) s += `<li>${m}</li>`
    this.innerHTML = `<div><br>History<ul>${s}</ul></div>`
  }
})


// -----------------
// PARENT CODE 
// (e.g. in index.html which use above two WebComponents)
// Parent must just subscribe chat-form send event, and when
// receive message then it shoud give it to chat-history add method
// -----------------

myChatForm.addEventListener('send', e => {
  myChatHistory.add(e.detail.message)
});
body {background: white}
<h3>Hello!</h3>

<chat-form id="myChatForm"></chat-form>

<div>Type something</div>

<chat-history id="myChatHistory"></chat-history>


5

同意其他两个答案,事件是最好的,因为这样组件之间的耦合性较弱。


另请参阅:https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/


请注意,在自定义事件的 detail 中,您可以发送任何内容。

基于事件驱动的函数执行:

因此,我使用(伪代码):

定义纸牌游戏 Solitaire/Freecell 的元素:

-> game Element
  -> pile Element
    -> slot Element
      -> card element
  -> pile Element
    -> slot Element
      -> empty

当用户需要移动卡片到另一个牌堆时,它会发送一个事件(通过DOM冒泡到游戏元素)。
    //triggered by .dragend Event
    card.say(___FINDSLOT___, {
                                id, 
                                reply: slot => card.move(slot)
                            });    

注意:reply是一个函数定义

因为所有的堆栈都被告知在游戏元素上监听___FINDSLOT___事件...

   pile.on(game, ___FINDSLOT___, evt => {
                                      let foundslot = pile.free(evt.detail.id);
                                      if (foundslot.length) evt.detail.reply(foundslot[0]);
                                    });


只有一个与evt.detail.id匹配的堆会做出反应:
通过执行在evt.detail.reply中发送的函数card,来实现!!!
技术方面:该函数在pile范围内执行!
(上述代码是伪代码!)
为什么?!
看起来可能很复杂;
重要的是:pile元素与card元素中的.move()方法没有耦合
唯一的耦合是事件名称:___FINDSLOT___!!!
这意味着card始终控制,并且相同的事件(名称)可用于:
- 卡牌可以走到哪里? - 什么是最佳位置? - 在河流中,哪张牌组成了Full-House? - ...
在我的E-ments代码中,pile也没有与evt.detail.id耦合, CustomEvents只发送函数

.say().on()是我自定义方法(每个元素都有),用于dispatchEventaddEventListener 现在我有了一些可以用来创建任何纸牌游戏的E-lements
不需要任何库,编写自己的“消息总线” 我的element.on()方法只是几行代码包装在addEventListener函数周围,因此它们可以轻松删除:
    $Element_addEventListener(
        name,
        func,
        options = {}
    ) {
        let BigBrotherFunc = evt => {                     // wrap every Listener function
            if (evt.detail && evt.detail.reply) {
                el.warn(`can catch ALL replies '${evt.type}' here`, evt);
            }
            func(evt);
        }
        el.addEventListener(name, BigBrotherFunc, options);
        return [name, () => el.removeEventListener(name, BigBrotherFunc)];
    },
    on(
        //!! no parameter defintions, because function uses ...arguments
    ) {
        let args = [...arguments];                                  // get arguments array
        let target = el;                                            // default target is current element
        if (args[0] instanceof HTMLElement) target = args.shift();  // if first element is another element, take it out the args array
        args[0] = ___eventName(args[0]) || args[0];                 // proces eventNR
        $Element_ListenersArray.push(target.$Element_addEventListener(...args));
    },

.say( )是一个单行命令:

    say(
        eventNR,
        detail,             //todo some default something here ??
        options = {
            detail,
            bubbles: 1,    // event bubbles UP the DOM
            composed: 1,   // !!! required so Event bubbles through the shadowDOM boundaries
        }
    ) {
        el.dispatchEvent(new CustomEvent(___eventName(eventNR) || eventNR, options));
    },

3

定制事件是最好的解决方案,如果你想处理松散耦合的自定义元素。

相反,如果一个自定义元素通过引用知道另一个元素,它可以调用它的自定义属性或方法

注:Original Answer翻译成"最初的回答"不符合上下文语境,因此未进行翻译。
//in chatForm element
chatHistory.attachedForm = this
chatHistory.addMessage( message )
chatHistory.api.addMessage( message )

在上面的最后一个例子中,通信是通过通过api属性公开的专用对象进行的。
您还可以根据自定义元素的链接方式,使用事件(单向)和方法(另一种方式)的混合。
最后,在某些情况下,当消息很基本时,您可以通过HTML属性来传递(字符串)数据:
chatHistory.setAttributes( 'chat', 'active' )
chatHistory.dataset.username = `$(this.name)`

0

对于父母和子代互相知道的情况,例如在烤面包机示例中。

<toaster-host>
  <toast-msg show-for='5s'>Success</toast-msg>
</toaster-host>

有很多选择,但是: 父组件向子组件传递数据 -> 使用基本类型的属性或observedAttributes。如果需要传递复杂对象,则可以公开一个函数或属性即可。如果domProperty需要响应更新,可以将其包装在代理中。

子组件向父组件传递数据 -> 可以使用事件,也可以使用.closest('toaster-host')查询父组件并调用函数或设置属性。我更喜欢查询并调用函数。Typescript对这种方法有所帮助。

在像toaster示例这样的情况下,toaster-host和toast-item将始终一起使用,因此关于松耦合的争论最多只是学术上的。它们是不同的元素,主要是因为它们有不同的职责。如果您想要交换toast-msg实现,可以在定义自定义元素时进行操作,甚至可以通过更改导入语句指向不同的文件来进行操作。


0

我遇到了相同的问题,由于找不到合适的库,所以我决定自己写一个。

你可以在这里找到它:https://www.npmjs.com/package/seawasp

SeaWasp 是一个WebRTC数据层,允许组件(或框架等)之间进行通信。

你只需导入它,注册一个连接(也称为触手😉),然后你就可以发送和接收消息了。

我正在积极地开发它,如果你有任何反馈/需要的功能,请告诉我 :)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接