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

Posted

技术标签:

【中文标题】如何在 Web 组件(原生 UI)之间进行通信?【英文标题】:How to communicate between Web Components (native UI)? 【发布时间】:2019-07-26 19:49:44 【问题描述】:

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

目前在 UI 网络组件中,我使用 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););

请告诉我还有哪些其他方法可以达到此目的。任何可以轻松与原生 UI Web 组件集成的优秀库,如 postaljs 等。

【问题讨论】:

【参考方案1】:

如果您将 Web 组件视为像 <div><audio> 这样的内置组件,那么您可以回答自己的问题。组件之间不相互通信。

一旦您开始允许组件直接相互通信,那么您实际上并没有组件,您拥有一个捆绑在一起的系统,并且您不能在没有组件 B 的情况下使用组件 A。这太紧密地捆绑在一起了。

相反,在拥有这两个组件的父代码中,您添加的代码允许您从组件 A 接收 事件调用函数设置参数 在组件 B 中,反之亦然。

话虽如此,内置组件的这条规则有两个例外:

    <label> 标签:它使用for 属性来获取另一个组件的 ID,如果设置并有效,那么当您单击 @987654325 时,它会将焦点传递给另一个组件@

    <form> 标记:这会查找子表单元素以收集发布表单所需的数据。

但是这两个仍然没有绑定到任何东西。 <label> 被告知focus 事件的接收者,并且仅在 ID 已设置且有效或作为子元素传递给第一个表单元素时才将其传递。而<form> 元素并不关心存在哪些子元素或有多少子元素,它只是通过其所有后代找到属于表单元素的元素并获取它们的value 属性。

但作为一般规则,您应该避免让一个兄弟组件直接与另一个兄弟组件对话。上面两个例子中的交叉通信方法可能是唯一的例外。

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

是的,您可以将该功能包装在一个新的父组件中,但请避免大量的麻烦,并避免使用意大利面条式代码。

作为一般规则,我绝不允许兄弟元素相互交谈,他们与父母交谈的唯一方式是通过事件。父母可以通过属性、属性和函数直接与孩子交谈。但在所有其他情况下都应避免。

【讨论】:

1. 和 2 都可以重写为仅使用事件,需要一些额外的工作,因为如果表单显示“ALLMYCHILDREN”,它不知道要处理多少响应;所以你需要某种时间来确定“最后的答复”。有点像学生进入我的教室,我不知道今天会有多少人或以什么顺序来。但是我有一个严格的规定。我在最后一个人进入后等待 2 分钟,然后锁上门(是的,用钥匙)...教他们基于事件的编程 :-) 表单没有时间问题,因为他们的孩子在提交表单时已经存在。我包括示例 1 和示例 2,以仅显示规则的两个例外。传统 DOM 元素中的所有其他内容都由事件处理并访问子属性和属性,或调用它们的函数。 非常感谢@Intervalia 对此的精彩解释。我理解 web-components 就像一个内置的 web 组件,它们的行为应该完全相同。正如您提到的,我还学习了父母、属性、属性等的概念,并尝试在我的项目中应用。 :) @Intervalia 假设我有一个包含两个子组件的视图组件:一个列表和一个工具栏。在列表(复选框)中选择一个项目会触发一个自定义事件,该事件会一直冒泡到父视图组件。如果选择了列表中的项目,工具栏应该启用用户可以在列表中使用的一些工具。选项是让视图直接与工具栏对话,或者传递事件以更新全局状态(想想 redux),然后视图在其中侦听状态的更改并更新工具栏。什么时候一个比另一个更可取? 如果您让组件 A 与组件 B 对话,那么您将两者联系在一起。如果您随后需要将 Component B 与 Component C 交换并且接口不同,那么您需要更改 Component A 以了解如何与 C 对话。如果您允许事件由父级处理,则父级需要知道如何与 C 对话。但这是由父组件而不是组件 A 编写的选择。因此,让父组件处理差异而不是让 A 与 B 或 C 一起工作更有意义。跨度> 【参考方案2】:

工作示例

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

// 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>

【讨论】:

【参考方案3】:

+1 对于其他两个答案,事件是最好的,因为那时组件很松散 耦合


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


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

事件驱动函数执行:

所以我使用(伪代码):

定义纸牌/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 匹配的一堆响应:

!!!通过执行函数card发送到evt.detail.reply

技术方面:函数在pile范围内执行!

(以上代码为伪代码!)

为什么?!

可能看起来很复杂; 重要的是pile 元素不耦合card 元素中的.move() 方法。

唯一的耦合是事件的名称:___FINDSLOT___!!!

这意味着card 始终处于控制之中,相同的事件(名称) 可用于:

卡可以去哪里? 最好的位置是什么? 河中的哪张牌pile 是满堂彩? ...

在我的电子元素代码中,pile 也没有耦合到 evt.detail.id

CustomEvents 只发送函数



.say().on()dispatchEventaddEventListener 的自定义方法(在每个元素上)

我现在有一些可用于创建任何纸牌游戏的电子元素

不需要任何库,自己写'Message Bus'

我的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));
    ,

【讨论】:

【参考方案4】: 如果您想处理松散耦合的自定义元素,

自定义事件是最佳解决方案。

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

//in chatForm element
chatHistory.attachedForm = this
chatHistory.addMessage( message )
chatHistory.api.addMessage( message )

在上面的最后一个示例中,通信是通过api 属性公开的专用对象完成的。

您还可以混合使用事件(以一种方式)和方法(以另一种方式),具体取决于自定义元素的链接方式。

最后,在某些基本消息的情况下,您可以通过 HTML 属性 传达(字符串)数据:

chatHistory.setAttributes( 'chat', 'active' )
chatHistory.dataset.username = `$(this.name)`

【讨论】:

以上是关于如何在 Web 组件(原生 UI)之间进行通信?的主要内容,如果未能解决你的问题,请参考以下文章

WEB UI 前端和 C++ 后端之间的通信?

如何为 react-native 本机 ui 组件创建打字稿类型定义?

Android -- 每日一问:两个 Fragment 之间如何进行通信 ?

如何解决反应原生 EventEmitterListener 警告

使用electron进行原生应用的打包---主进程与渲染进程之间的通信

如何在 2 个组件之间进行通信 - 兄弟组件(Angular 1.5.8)